feat(ui): M4 — agent activity, knowledge search, audit, correlation grouping
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:
63
web/src/lib/stores/context.ts
Normal file
63
web/src/lib/stores/context.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { writable, get } from 'svelte/store'
|
||||
import { fetchDashboardSummary, fetchApprovals, type DashboardSummary, type Approval } from '$lib/api'
|
||||
import { liveEvents, subscribeEvents, type OikosEvent } from './events'
|
||||
|
||||
// Shared operational context: dashboard summary + pending approvals,
|
||||
// refreshed on a slow poll and eagerly on relevant SSE events. Ref-counted
|
||||
// so the poll only runs while something on screen displays it.
|
||||
|
||||
export const summary = writable<DashboardSummary | null>(null)
|
||||
export const pendingApprovals = writable<Approval[]>([])
|
||||
|
||||
let refs = 0
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
let unsubscribeSSE: (() => void) | null = null
|
||||
let unsubscribeStore: (() => void) | null = null
|
||||
let lastSeenEventId = 0
|
||||
|
||||
export async function refreshContext() {
|
||||
const [s, approvals] = await Promise.all([fetchDashboardSummary(), fetchApprovals('pending')])
|
||||
if (s) summary.set(s)
|
||||
pendingApprovals.set(approvals)
|
||||
}
|
||||
|
||||
function onEvent(ev: OikosEvent) {
|
||||
if (ev.id <= lastSeenEventId) return
|
||||
lastSeenEventId = ev.id
|
||||
if (
|
||||
ev.type.startsWith('approval.') ||
|
||||
ev.type.startsWith('signal.') ||
|
||||
ev.type.startsWith('execution.') ||
|
||||
ev.type === 'health.changed'
|
||||
) {
|
||||
refreshContext()
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeContext(): () => void {
|
||||
refs++
|
||||
if (refs === 1) {
|
||||
refreshContext()
|
||||
pollTimer = setInterval(refreshContext, 30000)
|
||||
unsubscribeSSE = subscribeEvents()
|
||||
unsubscribeStore = liveEvents.subscribe((events) => {
|
||||
if (events[0]) onEvent(events[0])
|
||||
})
|
||||
}
|
||||
return () => {
|
||||
refs--
|
||||
if (refs === 0) {
|
||||
if (pollTimer) clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
unsubscribeSSE?.()
|
||||
unsubscribeSSE = null
|
||||
unsubscribeStore?.()
|
||||
unsubscribeStore = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function openSignalCount(s: DashboardSummary | null): number {
|
||||
if (!s) return 0
|
||||
return Object.values(s.signals_by_severity).reduce((a, b) => a + b, 0)
|
||||
}
|
||||
Reference in New Issue
Block a user