forked from wrenn/wrenn
feat: channel audit logging, name cleaning, message formatting, and dashboard UI
- Add audit log entries for channel create, update, rotate_config, delete - Clean channel names on create/update (trim, lowercase, spaces → hyphens, SafeName validation) - Format chat notifications with full event details (resource, actor, team, timestamp) instead of one-liners - Fix Discord split-line embeds by setting splitLines=No on shoutrrr URL - Add channels dashboard page and sidebar navigation
This commit is contained in:
72
frontend/src/lib/api/channels.ts
Normal file
72
frontend/src/lib/api/channels.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { apiFetch, type ApiResult } from '$lib/api/client';
|
||||
|
||||
export type Channel = {
|
||||
id: string;
|
||||
team_id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
events: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
secret?: string; // only present immediately after creation (webhook provider)
|
||||
};
|
||||
|
||||
export const PROVIDERS = [
|
||||
{ value: 'discord', label: 'Discord', fields: ['webhook_url'] },
|
||||
{ value: 'slack', label: 'Slack', fields: ['webhook_url'] },
|
||||
{ value: 'teams', label: 'Teams', fields: ['webhook_url'] },
|
||||
{ value: 'googlechat', label: 'Google Chat', fields: ['webhook_url'] },
|
||||
{ value: 'telegram', label: 'Telegram', fields: ['bot_token', 'chat_id'] },
|
||||
{ value: 'matrix', label: 'Matrix', fields: ['homeserver_url', 'access_token', 'room_id'] },
|
||||
{ value: 'webhook', label: 'Webhook', fields: ['url'] }
|
||||
] as const;
|
||||
|
||||
export const EVENT_TYPES = [
|
||||
{ value: 'capsule.created', group: 'Capsule' },
|
||||
{ value: 'capsule.running', group: 'Capsule' },
|
||||
{ value: 'capsule.paused', group: 'Capsule' },
|
||||
{ value: 'capsule.destroyed', group: 'Capsule' },
|
||||
{ value: 'template.snapshot.created', group: 'Template' },
|
||||
{ value: 'template.snapshot.deleted', group: 'Template' },
|
||||
{ value: 'host.up', group: 'Host' },
|
||||
{ value: 'host.down', group: 'Host' }
|
||||
] as const;
|
||||
|
||||
export async function listChannels(): Promise<ApiResult<Channel[]>> {
|
||||
return apiFetch('GET', '/api/v1/channels');
|
||||
}
|
||||
|
||||
export async function createChannel(
|
||||
name: string,
|
||||
provider: string,
|
||||
config: Record<string, string>,
|
||||
events: string[]
|
||||
): Promise<ApiResult<Channel>> {
|
||||
return apiFetch('POST', '/api/v1/channels', { name, provider, config, events });
|
||||
}
|
||||
|
||||
export async function updateChannel(
|
||||
id: string,
|
||||
name: string,
|
||||
events: string[]
|
||||
): Promise<ApiResult<Channel>> {
|
||||
return apiFetch('PATCH', `/api/v1/channels/${id}`, { name, events });
|
||||
}
|
||||
|
||||
export async function deleteChannel(id: string): Promise<ApiResult<void>> {
|
||||
return apiFetch('DELETE', `/api/v1/channels/${id}`);
|
||||
}
|
||||
|
||||
export async function rotateConfig(
|
||||
id: string,
|
||||
config: Record<string, string>
|
||||
): Promise<ApiResult<Channel>> {
|
||||
return apiFetch('PUT', `/api/v1/channels/${id}/config`, { config });
|
||||
}
|
||||
|
||||
export async function testChannel(
|
||||
provider: string,
|
||||
config: Record<string, string>
|
||||
): Promise<ApiResult<{ status: string }>> {
|
||||
return apiFetch('POST', '/api/v1/channels/test', { provider, config });
|
||||
}
|
||||
@ -22,7 +22,8 @@
|
||||
IconAudit,
|
||||
IconServer,
|
||||
IconShield,
|
||||
IconMetrics
|
||||
IconMetrics,
|
||||
IconBroadcast
|
||||
} from './icons';
|
||||
|
||||
let { collapsed = $bindable(false) }: { collapsed: boolean } = $props();
|
||||
@ -58,6 +59,7 @@
|
||||
|
||||
let managementItems = $derived<NavItem[]>([
|
||||
{ label: 'Keys', icon: IconKey, href: '/dashboard/keys' },
|
||||
{ label: 'Channels', icon: IconBroadcast, href: '/dashboard/channels' },
|
||||
{ label: 'Team', icon: IconMembers, href: '/dashboard/team' },
|
||||
{ label: 'Audit Logs', icon: IconAudit, href: '/dashboard/audit' },
|
||||
...(currentTeamIsByoc
|
||||
|
||||
22
frontend/src/lib/components/icons/IconBroadcast.svelte
Normal file
22
frontend/src/lib/components/icons/IconBroadcast.svelte
Normal file
@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
let { size = 18, class: className = '' }: { size?: number; class?: string } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.75"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
class={className}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="2" />
|
||||
<path d="M16.24 7.76a6 6 0 0 1 0 8.49" />
|
||||
<path d="M7.76 16.24a6 6 0 0 1 0-8.49" />
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14" />
|
||||
<path d="M4.93 19.07a10 10 0 0 1 0-14.14" />
|
||||
</svg>
|
||||
@ -27,3 +27,4 @@ export { default as IconServer } from './IconServer.svelte';
|
||||
export { default as IconGear } from './IconGear.svelte';
|
||||
export { default as IconShield } from './IconShield.svelte';
|
||||
export { default as IconMetrics } from './IconMetrics.svelte';
|
||||
export { default as IconBroadcast } from './IconBroadcast.svelte';
|
||||
|
||||
1378
frontend/src/routes/dashboard/channels/+page.svelte
Normal file
1378
frontend/src/routes/dashboard/channels/+page.svelte
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user