Files
oikos/web/src/pages/Signals.svelte
dtoro 873b00ac42
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled
style(web): fix prettier config, format entire web/ tree
.prettierrc.json was missing "semi": false, so prettier wanted to add
semicolons to a codebase written without them (763 semicolon-free
statements vs. 150 with, in hand-written .ts; zero hand-written .svelte
files use them at all). That's why prettier --check failed on 249 files
— not because the code was unformatted, but because the config didn't
match the actual house style. Added "semi": false; left printWidth/etc
as configured (printWidth barely moves the failure count: 218/213/212
files at 100/120/140).

Ran `prettier --write .` with the corrected config. Verified
semantics-preserving before and after:
- eslint: 142 problems both before and after, byte-identical
- build passes, 38/38 tests pass
- token-stream diff (whitespace/semicolons/quotes normalized) on all
  218 changed files: only 52 had any remaining token change, all either
  trailing-comma removal (matching trailingComma: "none") or import/
  ternary reflow — no semantic changes
- live smoke test: Knowledge, Tasks, Fleet map, and a chat window
  (AgentTrace, markdown, Scope graph, activity rail) all render
  correctly, no console errors

Most of the diff is shadcn/ui vendor files (lib/components/ui/) moving
from the CLI's own style (double quotes, tabs, semicolons) to house
style; re-running `shadcn-svelte add` on a component will need a
follow-up format pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:56:07 +02:00

163 lines
5.0 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte'
import { fetchSignals, ackSignal, resolveSignal, muteSignal, type Signal } from '$lib/api'
import { liveEvents, subscribeEvents } from '$lib/stores/events'
import * as Tabs from '$lib/components/ui/tabs'
import { Badge } from '$lib/components/ui/badge'
import * as Select from '$lib/components/ui/select'
import { toast } from 'svelte-sonner'
import DataTable from '$lib/components/data-table/DataTable.svelte'
import type { DataTableColumn } from '$lib/components/data-table/types'
import SignalActions from '$lib/components/data-table/renderers/SignalActions.svelte'
let signals = $state<Signal[]>([])
let severityFilter = $state('all')
let acting = $state<string | null>(null)
async function load() {
signals = await fetchSignals()
}
onMount(() => {
load()
const unsubscribe = subscribeEvents()
return unsubscribe
})
$effect(() => {
const ev = $liveEvents[0]
if (!ev || !ev.type.startsWith('signal.')) return
load()
})
async function ack(id: string) {
acting = id
const result = await ackSignal(id)
acting = null
if (result) {
toast.success('Signal acknowledged')
load()
} else {
toast.error('Acknowledge failed')
}
}
async function resolve(id: string) {
acting = id
const result = await resolveSignal(id)
acting = null
if (result) {
toast.success('Signal resolved')
load()
} else {
toast.error('Resolve failed')
}
}
async function mute(id: string) {
acting = id
const muteUntil = new Date(Date.now() + 60 * 60 * 1000).toISOString()
const result = await muteSignal(id, muteUntil)
acting = null
if (result) {
toast.success('Signal muted for 1h')
load()
} else {
toast.error('Mute failed')
}
}
function bySeverity(list: Signal[]) {
return severityFilter === 'all' ? list : list.filter((s) => s.severity === severityFilter)
}
const open = $derived(
bySeverity(signals.filter((s) => ['raised', 'acknowledged', 'acting'].includes(s.state)))
)
const muted = $derived(bySeverity(signals.filter((s) => s.state === 'muted')))
const resolved = $derived(
bySeverity(signals.filter((s) => ['resolved', 'failed'].includes(s.state)))
)
function makeColumns(showActions: boolean, actingVal: string | null): DataTableColumn<Signal>[] {
const base: DataTableColumn<Signal>[] = [
{
key: 'target',
header: 'Target',
class: 'font-mono text-xs',
width: '180px',
accessor: (s) => s.target ?? '—',
truncate: true
},
{ key: 'kind', header: 'Kind', width: '120px' },
{
key: 'severity',
header: 'Severity',
render: 'status-badge',
renderProps: { kind: 'severity' },
width: '100px'
},
{
key: 'state',
header: 'State',
render: 'status-badge',
renderProps: { kind: 'state' },
width: '110px'
},
{ key: 'occurrence_count', header: 'Occurrences', width: '100px', align: 'right' },
{ key: 'last_seen_at', header: 'Last seen', render: 'date', width: '170px' }
]
if (showActions) {
base.push({
key: '_actions',
header: '',
render: SignalActions,
renderProps: { acting: actingVal, onAck: ack, onMute: mute, onResolve: resolve },
align: 'right',
headerClass: 'text-right',
width: '220px'
})
}
return base
}
const columnsWithActions = $derived(makeColumns(true, acting))
const columnsWithoutActions = makeColumns(false, null)
</script>
<div class="flex h-full flex-col gap-4 p-2">
<div class="flex items-center justify-between">
<h1 class="text-lg font-semibold">Signals</h1>
<Select.Root type="single" bind:value={severityFilter}>
<Select.Trigger class="w-40">
{severityFilter === 'all' ? 'All severities' : severityFilter}
</Select.Trigger>
<Select.Content>
<Select.Item value="all">All severities</Select.Item>
<Select.Item value="critical">Critical</Select.Item>
<Select.Item value="warning">Warning</Select.Item>
<Select.Item value="info">Info</Select.Item>
</Select.Content>
</Select.Root>
</div>
<Tabs.Root value="open" class="flex flex-1 flex-col overflow-hidden">
<Tabs.List>
<Tabs.Trigger value="open">
Open {#if open.length}<Badge variant="destructive" class="ml-1">{open.length}</Badge>{/if}
</Tabs.Trigger>
<Tabs.Trigger value="muted">Muted</Tabs.Trigger>
<Tabs.Trigger value="resolved">Resolved</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="open" class="flex-1 overflow-auto">
<DataTable columns={columnsWithActions} data={open} />
</Tabs.Content>
<Tabs.Content value="muted" class="flex-1 overflow-auto">
<DataTable columns={columnsWithActions} data={muted} />
</Tabs.Content>
<Tabs.Content value="resolved" class="flex-1 overflow-auto">
<DataTable columns={columnsWithoutActions} data={resolved} />
</Tabs.Content>
</Tabs.Root>
</div>