feat(ui): M4 — agent activity, knowledge search, audit, correlation grouping
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Add three new pages completing the control-room web UI:
- Agent activity: polls /agent-activity every 5s, filterable by type/agent
- Knowledge search: FTS over /knowledge/search with snippet + entity links
- Audit trail: browseable audit log with actor/action/entity filters

Enhanced live events page with correlation-id clustering (Groups toggle).
Added fetchAgentActivity/searchKnowledge/fetchAudit to the API client.
11 nav items now cover all planned control-room views.
This commit is contained in:
2026-07-08 17:02:09 +02:00
parent cff05c0768
commit 2908b0a377
162 changed files with 7857 additions and 553 deletions

View File

@@ -0,0 +1,91 @@
<script lang="ts">
import { searchKnowledge, type KnowledgeHit } from '$lib/api'
import * as Card from '$lib/components/ui/card'
import { Badge } from '$lib/components/ui/badge'
import { Input } from '$lib/components/ui/input'
import { Button } from '$lib/components/ui/button'
import { ScrollArea } from '$lib/components/ui/scroll-area'
import SearchIcon from '@lucide/svelte/icons/search'
let query = $state('')
let results = $state<KnowledgeHit[]>([])
let loading = $state(false)
let searched = $state(false)
async function search() {
if (!query.trim()) return
loading = true
results = await searchKnowledge(query)
loading = false
searched = true
}
function typeVariant(type: string): 'default' | 'secondary' | 'outline' {
if (type === 'runbook') return 'secondary'
if (type === 'investigation') return 'default'
return 'outline'
}
</script>
<div class="flex h-full flex-col gap-4 p-6">
<h1 class="text-lg font-semibold">Knowledge search</h1>
<form
onsubmit={(e) => {
e.preventDefault()
search()
}}
class="flex gap-2"
>
<div class="relative flex-1 max-w-lg">
<SearchIcon class="absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search documents, runbooks, investigations…"
bind:value={query}
class="pl-8"
/>
</div>
<Button type="submit" disabled={loading || !query.trim()}>
{loading ? 'Searching…' : 'Search'}
</Button>
</form>
{#if searched}
<p class="text-sm text-muted-foreground">{results.length} result{results.length === 1 ? '' : 's'}{query ? ` for "${query}"` : ''}</p>
{/if}
<ScrollArea class="flex-1">
<div class="flex flex-col gap-3 pr-4">
{#each results as hit (hit.id)}
<Card.Root class="cursor-pointer transition-colors hover:bg-muted/50">
<Card.Header>
<div class="flex items-center gap-2">
<Card.Title class="text-sm">{hit.title}</Card.Title>
<Badge variant={typeVariant(hit.type)}>{hit.type}</Badge>
</div>
{#if hit.snippet}
<Card.Description class="text-xs">{@html hit.snippet}</Card.Description>
{/if}
{#if hit.linked_entities?.length}
<div class="mt-1 flex flex-wrap gap-1">
{#each hit.linked_entities as slug}
<button
type="button"
class="font-mono text-xs text-muted-foreground underline"
onclick={() => (location.hash = '#/entity/' + encodeURIComponent(slug))}
>
{slug}
</button>
{/each}
</div>
{/if}
</Card.Header>
</Card.Root>
{:else}
{#if searched && !loading}
<p class="py-12 text-center text-muted-foreground">No results found.</p>
{/if}
{/each}
</div>
</ScrollArea>
</div>