feat: redesign toward shadcn-svelte dashboard-01 (inset sidebar, gradient stat cards)
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:
@@ -49,7 +49,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold">Agent activity</h1>
|
||||
<span class="text-xs text-muted-foreground">{activities.length} entries</span>
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold">Audit trail</h1>
|
||||
<span class="text-xs text-muted-foreground">{entries.length} entries</span>
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold">Entities</h1>
|
||||
<span class="text-xs text-muted-foreground">{filtered.length} of {entities.length}</span>
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold">Live event feed</h1>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<h1 class="text-lg font-semibold">Knowledge search</h1>
|
||||
|
||||
<form
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
const decidedApprovals = $derived(approvals.filter((a) => a.status !== 'pending'))
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<h1 class="text-lg font-semibold">Operations ledger</h1>
|
||||
|
||||
<Tabs.Root value="approvals" class="flex flex-1 flex-col overflow-hidden">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex h-full flex-col gap-4 p-6">
|
||||
<div class="flex h-full flex-col gap-4 p-4 md:p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-lg font-semibold">Signals</h1>
|
||||
<Select.Root type="single" bind:value={severityFilter}>
|
||||
|
||||
Reference in New Issue
Block a user