feat: redesign toward shadcn-svelte dashboard-01 (inset sidebar, gradient stat cards)
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled

Problem: requested visual alignment with the shadcn-svelte dashboard-01
reference block (shadcn-svelte.com/blocks#dashboard-01) — the app's
sidebar/header shell and Overview stat cards looked plain by comparison.

Change: pulled the actual reference source (app-sidebar.svelte,
nav-main.svelte, site-header.svelte, section-cards.svelte from
huntabyte/shadcn-svelte) rather than approximating from screenshots.

- App.svelte: Sidebar.Root now uses variant="inset" (the floating,
  rounded, shadowed content panel — already fully built into the
  existing Sidebar.Inset component via peer-data selectors, just never
  enabled). Brand mark is now a proper Sidebar.MenuButton matching the
  reference's padding/hover treatment; "New chat" uses the reference's
  primary-colored button styling. Header matches the reference exactly:
  h-(--header-height) (48px, was 44px), vertical separator after the
  sidebar trigger, right-aligned actions group.
- Overview.svelte: stat cards rebuilt to match section-cards.svelte —
  gradient background, Card.Action badge, Card.Footer with a bold line
  + muted context line, tabular-nums, responsive @container grid
  (1/2/4 columns). Deliberately did NOT copy the reference's fake
  trend-percentage badges (Oikos doesn't track historical trends, and
  this project's whole thrust has been eliminating dishonest UI state —
  see 279549c). Badges instead reflect real current-state signals
  (healthy/degraded/down, clear/needs-review) computed from the actual
  dashboard summary.
- EntityDetailContent.svelte + Entities/Signals/Ops/Events/Agent/
  Audit/Knowledge pages: normalized root padding to p-4 md:p-6 (was a
  flat p-6) to match the reference's responsive py-4 md:py-6 convention.

Risk: reversible_low (UI-only, no data or behavior changes).

Verification: npx tsc --noEmit clean (excluding pre-existing unrelated
.svelte type-resolution warnings, same as prior commits). go build/vet
clean (backend untouched, sanity check only). Manually verified in the
browser preview at 1400px: inset sidebar's margin/rounded-corner/shadow
classes confirmed applied via computed styles; Overview cards render
with real live numbers from the now-fixed dashboard summary endpoint;
Signals/Ops pages confirmed visually consistent with the new spacing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 00:54:51 +02:00
parent 851b5dce67
commit cbfd09c5df
10 changed files with 153 additions and 55 deletions

View File

@@ -6,6 +6,9 @@
import { Badge } from '$lib/components/ui/badge'
import { Skeleton } from '$lib/components/ui/skeleton'
import { ScrollArea } from '$lib/components/ui/scroll-area'
import CircleCheckIcon from '@lucide/svelte/icons/circle-check'
import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert'
import OctagonXIcon from '@lucide/svelte/icons/octagon-x'
let summary = $state<DashboardSummary | null>(null)
let loading = $state(true)
@@ -47,9 +50,44 @@
function formatEventLabel(ev: OikosEvent) {
return ev.type
}
const totalEntities = $derived(
summary ? Object.values(summary.entities_by_type).reduce((a, b) => a + b, 0) : 0
)
const topTypes = $derived(
summary
? Object.entries(summary.entities_by_type)
.sort((a, b) => b[1] - a[1])
.slice(0, 3)
: []
)
const entityTypeCount = $derived(summary ? Object.keys(summary.entities_by_type).length : 0)
const totalMonitored = $derived(
summary
? summary.health.healthy + summary.health.degraded + summary.health.down + summary.health.unknown
: 0
)
const healthTone = $derived(
!summary ? 'ok' : summary.health.down > 0 ? 'down' : summary.health.degraded > 0 ? 'degraded' : 'ok'
)
const totalSignals = $derived(
summary ? Object.values(summary.signals_by_severity).reduce((a, b) => a + b, 0) : 0
)
const worstSeverity = $derived(
summary?.signals_by_severity.critical
? 'critical'
: summary?.signals_by_severity.warning
? 'warning'
: 'none'
)
const executionsRunning = $derived(summary?.executions_by_state.running ?? 0)
const executionsFailed = $derived(summary?.executions_by_state.failed ?? 0)
</script>
<div class="flex h-full flex-col gap-4 overflow-y-auto p-6">
<div class="@container/main flex h-full flex-col gap-4 overflow-y-auto p-4 md:p-6">
<h1 class="text-lg font-semibold">Overview</h1>
{#if loading}
@@ -59,58 +97,99 @@
{/each}
</div>
{:else if summary}
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
<Card.Root>
<div
class="*:data-[slot=card]:from-primary/5 *:data-[slot=card]:to-card dark:*:data-[slot=card]:bg-card grid grid-cols-1 gap-4 *:data-[slot=card]:bg-gradient-to-t *:data-[slot=card]:shadow-xs @xl/main:grid-cols-2 @5xl/main:grid-cols-4"
>
<Card.Root class="@container/card">
<Card.Header>
<Card.Description>Entities</Card.Description>
<Card.Title class="text-2xl">
{Object.values(summary.entities_by_type).reduce((a, b) => a + b, 0)}
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
{totalEntities}
</Card.Title>
<Card.Action>
<Badge variant="outline">{entityTypeCount} types</Badge>
</Card.Action>
</Card.Header>
<Card.Content class="flex flex-wrap gap-1 text-xs text-muted-foreground">
{#each Object.entries(summary.entities_by_type) as [type, count]}
<Badge variant="outline">{type}: {count}</Badge>
{/each}
</Card.Content>
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
<div class="line-clamp-1 flex flex-wrap gap-x-1.5 font-medium">
{#each topTypes as [type, count]}
<span class="text-muted-foreground">{type}: <span class="text-foreground">{count}</span></span>
{/each}
</div>
<div class="text-muted-foreground">Across the fleet</div>
</Card.Footer>
</Card.Root>
<Card.Root>
<Card.Root class="@container/card">
<Card.Header>
<Card.Description>Health</Card.Description>
<Card.Title class="text-2xl">{summary.health.healthy} healthy</Card.Title>
<Card.Description>Fleet health</Card.Description>
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
{summary.health.healthy} / {totalMonitored}
</Card.Title>
<Card.Action>
{#if healthTone === 'ok'}
<Badge variant="outline"><CircleCheckIcon class="text-success" />healthy</Badge>
{:else if healthTone === 'degraded'}
<Badge variant="outline"><TriangleAlertIcon class="text-warning" />degraded</Badge>
{:else}
<Badge variant="outline"><OctagonXIcon class="text-destructive" />down</Badge>
{/if}
</Card.Action>
</Card.Header>
<Card.Content class="flex flex-wrap gap-1 text-xs">
<Badge>healthy: {summary.health.healthy}</Badge>
<Badge variant="secondary">degraded: {summary.health.degraded}</Badge>
<Badge variant="destructive">down: {summary.health.down}</Badge>
<Badge variant="outline">unknown: {summary.health.unknown}</Badge>
</Card.Content>
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
<div class="line-clamp-1 flex gap-2 font-medium">
{summary.health.degraded} degraded · {summary.health.down} down · {summary.health.unknown} unmonitored
</div>
<div class="text-muted-foreground">Healthy entities as observed by the scheduler</div>
</Card.Footer>
</Card.Root>
<Card.Root>
<Card.Root class="@container/card">
<Card.Header>
<Card.Description>Open signals</Card.Description>
<Card.Title class="text-2xl">
{Object.values(summary.signals_by_severity).reduce((a, b) => a + b, 0)}
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
{totalSignals}
</Card.Title>
<Card.Action>
{#if worstSeverity === 'critical'}
<Badge variant="destructive"><TriangleAlertIcon />critical</Badge>
{:else if worstSeverity === 'warning'}
<Badge variant="secondary"><TriangleAlertIcon />warning</Badge>
{:else}
<Badge variant="outline"><CircleCheckIcon class="text-success" />clear</Badge>
{/if}
</Card.Action>
</Card.Header>
<Card.Content class="flex flex-wrap gap-1 text-xs">
{#each Object.entries(summary.signals_by_severity) as [severity, count]}
<Badge variant={severityVariant(severity)}>{severity}: {count}</Badge>
{/each}
</Card.Content>
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
<div class="line-clamp-1 flex flex-wrap gap-x-1.5 font-medium">
{#each Object.entries(summary.signals_by_severity) as [severity, count]}
<span class="text-muted-foreground">{severity}: <span class="text-foreground">{count}</span></span>
{/each}
</div>
<div class="text-muted-foreground">Unresolved right now</div>
</Card.Footer>
</Card.Root>
<Card.Root>
<Card.Root class="@container/card">
<Card.Header>
<Card.Description>Pending approvals</Card.Description>
<Card.Title class="text-2xl">{summary.approvals_pending}</Card.Title>
<Card.Title class="text-2xl font-semibold tabular-nums @[250px]/card:text-3xl">
{summary.approvals_pending}
</Card.Title>
<Card.Action>
{#if summary.approvals_pending > 0}
<Badge variant="destructive">needs review</Badge>
{:else}
<Badge variant="outline"><CircleCheckIcon class="text-success" />clear</Badge>
{/if}
</Card.Action>
</Card.Header>
<Card.Content class="flex flex-wrap gap-1 text-xs">
{#each Object.entries(summary.executions_by_state) as [state, count]}
<Badge variant="outline">{state}: {count}</Badge>
{/each}
</Card.Content>
<Card.Footer class="flex-col items-start gap-1.5 text-sm">
<div class="line-clamp-1 flex gap-2 font-medium">
{executionsRunning} running · {executionsFailed} failed
</div>
<div class="text-muted-foreground">Executions in the last 24h</div>
</Card.Footer>
</Card.Root>
</div>