
Dashboard Patterns
- 17 installs
- 1 repo stars
- Updated August 4, 2026
- cleanexpo/nodejs-starter-v1
Real-time dashboard patterns using a Status Command Centre component library, Supabase Realtime, spectral colours, and a Scientific Luxury design system.
About
Codifies real-time data-visualisation dashboards with a component library (MetricTile, DataStrip, ProgressOrb), Supabase Realtime integration, and design-system enforcement. A developer uses it when building dashboard pages, live metric displays, or monitoring UIs.
- Status Command Centre component library with timeline/orbital layouts
- Supabase Realtime, spectral colour mapping, and design-system rules
Dashboard Patterns by the numbers
- 17 all-time installs (skills.sh)
- Ranked #1,582 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cleanexpo/nodejs-starter-v1 --skill dashboard-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 4, 2026 |
| Repository | cleanexpo/nodejs-starter-v1 ↗ |
What it does
Real-time dashboard patterns using a Status Command Centre component library, Supabase Realtime, spectral colours, and a Scientific Luxury design system.
Files
Dashboard Patterns - Real-Time Data Visualisation
Codifies the project's existing dashboard architecture: the Status Command Centre component library, backend analytics APIs, and the Scientific Luxury design rules that govern every data surface. Enforces timeline layout, spectral colours, physics-based animations, and Australian locale formatting.
Description
Codifies real-time dashboard patterns for NodeJS-Starter-V1 including the Status Command Centre component library, timeline/orbital layouts, Supabase Realtime integration, spectral colour mapping, and Scientific Luxury design enforcement for all data visualisation surfaces.
---
When to Apply
Positive Triggers
- Building new dashboard pages or metric displays
- Adding real-time data visualisation components
- Integrating Supabase Realtime for live updates
- Creating loading skeletons or empty states for dashboards
- Composing MetricTile, DataStrip, or ProgressOrb components
- Reviewing dashboard layouts for Scientific Luxury compliance
- User mentions: "dashboard", "metrics display", "real-time", "monitoring UI", "command centre"
Negative Triggers
- Collecting or storing metrics data (use
metrics-collectorinstead) - Designing email templates (use
email-templateinstead) - Adding log statements (use
structured-logginginstead) - Implementing non-dashboard UI components (use
scientific-luxuryinstead)
Core Directives
The Three Laws of Dashboards
1. Timeline, never grid: No grid-cols-2/grid-cols-4 layouts. Use vertical timelines, horizontal data strips, and orbital arrangements. 2. Spectral, never static: Every status maps to a spectral colour with breathing animation. No grey placeholder states. 3. Real-time, never stale: Live data via Supabase Realtime or 30-second polling. Always show connection status.
---
Existing Project Infrastructure
Component Library
Location: apps/web/components/status-command-centre/
| Category | Components |
|---|---|
| Main | StatusCommandCentre (full/compact/minimal variants) |
| Data Display | DataStrip (horizontal metrics), MetricTile (stat tile with trends) |
| Visualisation | ProgressOrb, ProgressRing, StatusPulse, StatusBadge |
| Activity | AgentNode, AgentActivityCard, ActivityTimeline, AgentThinkingIndicator |
| Utility | NotificationStream, ElapsedTimer |
| Hooks | useElapsedTime, useCountdown, useStatusTransitions, useStatusColourTransition |
| Utils | formatElapsedAU, formatTimestampAU, formatDateAU, getAustralianTimezone |
Dashboard Pages
| Page | Location | Data Source |
|---|---|---|
| Agent Dashboard | apps/web/app/(dashboard)/agents/page.tsx | /api/agents/stats, /api/agents/list |
| Analytics Dashboard | apps/web/app/dashboard-analytics/page.tsx | /api/analytics/metrics/overview |
| Status Command Centre | Embedded component | Supabase Realtime (agent_runs table) |
Backend APIs
| Route | Location | Returns |
|---|---|---|
/analytics/metrics/overview | apps/backend/src/api/routes/analytics.py | Runs, success rate, cost, tokens |
/analytics/metrics/agents | Same file | Per-agent performance breakdown |
/analytics/metrics/costs | Same file | Cost by model, token breakdown |
/api/agents/stats | apps/backend/src/api/routes/agent_dashboard.py | Aggregate agent statistics |
/api/agents/{id}/health | Same file | AgentHealthReport model |
/api/agents/performance/trends | Same file | Time-series trend data |
---
Layout Patterns
BANNED: Card Grid
// REJECTED — violates Scientific Luxury and Timeline law
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card>...</Card>
<Card>...</Card>
</div>REQUIRED: Timeline Layout
The StatusCommandCentre implements a vertical timeline spine with agent nodes:
<div className="relative pl-4">
{/* Vertical Timeline Spine */}
<motion.div
initial={{ scaleY: 0 }}
animate={{ scaleY: 1 }}
transition={{ delay: 0.3, duration: 0.8, ease: [0.19, 1, 0.22, 1] }}
className="absolute top-0 bottom-0 left-8 w-px origin-top
bg-gradient-to-b from-white/10 via-white/5 to-transparent"
/>
{/* Agent Nodes along the spine */}
<div className="space-y-8">
{runs.map((run, index) => (
<AgentNode key={run.id} run={run} index={index} />
))}
</div>
</div>REQUIRED: Horizontal Data Strip
Replace metric grids with DataStrip — inline horizontal layout with spectral-coloured values:
<DataStrip
metrics={[
{ label: 'Total', value: runs.length },
{ label: 'Active', value: activeRuns.length, variant: 'info' },
{ label: 'Completed', value: completedRuns.length, variant: 'success' },
{ label: 'Failed', value: failedRuns.length, variant: 'error' },
]}
/>DataStrip uses JetBrains Mono for values, text-[10px] tracking-widest uppercase for labels, pipe separators between metrics, and spectral glow on non-zero highlighted values.
---
Spectral Colour Mapping
Every AgentRunStatus maps to a spectral colour defined in constants.ts:
| Status | Colour | HSL | Animation |
|---|---|---|---|
pending | Slate | hsl(220 14% 46%) | Pulse (idle) |
in_progress | Blue | hsl(217 91% 60%) | Spin (active) |
awaiting_verification | Amber | hsl(38 92% 50%) | Pulse (active) |
verification_in_progress | Amber | hsl(38 92% 50%) | Spin (active) |
verification_passed | Green | hsl(142 76% 36%) | None (idle) |
verification_failed | Red | hsl(0 84% 60%) | Pulse (urgent) |
completed | Emerald | #00FF88 | None (idle) |
failed | Red | #FF4444 | Pulse (urgent) |
blocked | Slate | Muted | None (idle) |
escalated_to_human | Magenta | #FF00FF | Pulse (urgent) |
Access via getStatusConfig(status) from constants.ts. Each config includes colour (primary, glow, background), icon, animation, and intensity.
DataStrip variants use simplified spectral colours: info=#00F5FF, success=#00FF88, warning=#FFB800, error=#FF4444.
---
Animation Patterns
All animations use Framer Motion. CSS transitions are BANNED.
Staggered Entry
Components animate in sequence with delay offsets:
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 * index, ease: [0.19, 1, 0.22, 1] }}
>Breathing Orbs
Status indicators use breathing animation for active states:
<motion.span
className="h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: config.colour.primary }}
animate={{ opacity: [1, 0.4, 1], scale: [1, 1.2, 1] }}
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
/>Ambient Glow
Active agents trigger a radial gradient background glow:
{activeRuns.length > 0 && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.1 }}
className="pointer-events-none absolute inset-0"
style={{ background: 'radial-gradient(ellipse at 20% 10%, hsl(217 91% 60% / 0.15) 0%, transparent 50%)' }}
/>
)}Status Transitions
Use useStatusTransitions hook to detect state changes and apply transition animations (success ripple, error shake).
---
Data Fetching Patterns
Server Components (Initial Load)
Use Next.js Server Components for initial data fetch with cache: 'no-store':
async function fetchAgentStats() {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
const res = await fetch(`${backendUrl}/api/agents/stats`, { cache: 'no-store' });
if (!res.ok) return FALLBACK_STATS;
return res.json();
}Always provide fallback data so the page renders even when the backend is unavailable.
Polling (Analytics Dashboard)
For non-realtime pages, poll at 30-second intervals:
useEffect(() => {
fetchMetrics();
const interval = setInterval(fetchMetrics, 30_000);
return () => clearInterval(interval);
}, []);Supabase Realtime (Command Centre)
For the Status Command Centre, subscribe to agent_runs table changes:
const channel = supabase
.channel('agent-runs')
.on('postgres_changes', { event: '*', schema: 'public', table: 'agent_runs' },
(payload: RealtimePayload) => {
if (payload.eventType === 'INSERT') addRun(payload.new);
if (payload.eventType === 'UPDATE') updateRun(payload.new);
})
.subscribe();Types RealtimeEvent, RealtimePayload, and ConnectionStatus are defined in types.ts.
Connection Status
Always display connection state using the ConnectionIndicator component:
- Connected (Emerald breathing dot + "Live" label)
- Reconnecting (Amber dot + "Reconnecting" label)
- Disconnected (Red dot + "Offline" label)
---
Loading & Empty States
Loading Skeleton
Every dashboard page must show a skeleton that matches the final layout structure:
function LoadingSkeleton() {
return (
<div className="space-y-8">
<motion.div
className="h-10 w-64 rounded-sm bg-white/5"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
{/* More skeleton elements matching the real layout */}
</div>
);
}Rules: Use bg-white/5 for skeleton blocks, rounded-sm only, breathing opacity animation, staggered delays per element.
Empty State
When no data is available, show a centred empty state with a breathing orb:
<div className="flex flex-col items-center justify-center py-20">
<motion.div
className="h-3 w-3 rounded-full bg-white/20"
animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
/>
<h3 className="text-xl font-light text-white">No Active Agents</h3>
<p className="font-mono text-xs text-white/40">Description text.</p>
</div>---
Component Composition
New Dashboard Page Template
Every dashboard page follows this structure:
// 1. Server Component wrapper (data fetch)
export default async function DashboardPage() {
const data = await fetchData();
return (
<div className="relative min-h-screen bg-[#050505]">
{/* Header */}
<header className="border-b border-white/[0.06] px-8 py-6">
<p className="text-[10px] tracking-[0.3em] text-white/30 uppercase">Category Label</p>
<h1 className="text-4xl font-extralight tracking-tight text-white">Page Title</h1>
<DataStrip metrics={[...]} />
</header>
{/* Content — timeline or orbital, never grid */}
<Suspense fallback={<LoadingSkeleton />}>
<DashboardContent data={data} />
</Suspense>
{/* Footer */}
<footer className="px-8 py-4">
<p className="font-mono text-[10px] text-white/20">
{new Date().toLocaleDateString('en-AU')}
</p>
</footer>
</div>
);
}When to Use Each Component
| Need | Component | Import From |
|---|---|---|
| Inline metrics row | DataStrip | status-command-centre |
| Single stat with trend | MetricTile | status-command-centre |
| Agent execution status | AgentNode | status-command-centre |
| Circular progress | ProgressOrb or ProgressRing | status-command-centre |
| Status dot | StatusPulse | status-command-centre |
| Status label | StatusBadge | status-command-centre |
| Step timeline | ActivityTimeline | status-command-centre |
| Notification sidebar | NotificationStream | status-command-centre |
| Elapsed time counter | ElapsedTimer | status-command-centre |
---
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
grid-cols-2 / grid-cols-4 metric cards | Violates Scientific Luxury layout rules | DataStrip or timeline layout |
Standard <Card> with rounded-lg | Wrong corners, wrong aesthetic | rounded-sm border-[0.5px] border-white/[0.06] |
CSS transition: all 0.3s linear | Banned by Bezier (Council of Logic) | Framer Motion with physics-based easing |
| White/light background dashboards | Violates OLED black requirement | bg-[#050505] always |
| No loading skeleton | Layout shift on data load | Skeleton matching final layout structure |
| No connection indicator | Users cannot tell if data is live | ConnectionIndicator in every realtime page |
| Hardcoded status colours | Drift from spectral palette | getStatusConfig(status).colour |
setInterval without cleanup | Memory leak on unmount | useEffect with clearInterval in cleanup |
---
Checklist for New Dashboard Pages
Layout
- [ ] OLED black background (
bg-[#050505]) - [ ] Timeline or orbital layout (no card grids)
- [ ]
DataStripfor summary metrics (not metric grid) - [ ] Single pixel borders (
border-[0.5px] border-white/[0.06]) - [ ]
rounded-smonly (norounded-lg,rounded-xl) - [ ] JetBrains Mono for data values
Data
- [ ] Server Component for initial fetch with fallback data
- [ ] Polling interval (30s) or Supabase Realtime subscription
- [ ] Connection status indicator shown
- [ ] Loading skeleton matching final layout
- [ ] Empty state with breathing orb
Animation
- [ ] Framer Motion for all transitions (no CSS transitions)
- [ ] Staggered entry for list items
- [ ] Breathing animation for active status indicators
- [ ] Ambient glow when agents are active
Integration
- [ ] Uses
metrics-collectorMetricsRegistryfor data source - [ ] Spectral colours from
STATUS_CONFIGconstants - [ ] Australian locale formatting (
formatDateAU,formatTimestampAU) - [ ] Types imported from
status-command-centre/types.ts
---
Response Format
[AGENT_ACTIVATED]: Dashboard Patterns
[PHASE]: {Design | Implementation | Review}
[STATUS]: {in_progress | complete}
{dashboard analysis or implementation guidance}
[NEXT_ACTION]: {what to do next}Integration Points
Scientific Luxury
- OLED black background, spectral colours, single-pixel borders, physics-based animations
- All dashboard components enforce the design system automatically via
constants.ts
Metrics Collector
MetricsRegistryprovides the data layer (counters, gauges, histograms)- Time-series aggregation powers trend charts
- Summary endpoint feeds
DataStripandMetricTilecomponents
State Machine
AgentRunStatus(10 states) maps directly toSTATUS_CONFIGcolour/animation entriesuseStatusTransitionshook handles state change animations
Structured Logging
- Dashboard pages log
dashboard_loaded,dashboard_errorevents - Connection status changes logged for debugging
Cron Scheduler
- Cron job metrics displayed via
DataStriporMetricTile - Daily report data powers the analytics dashboard trends
Australian Localisation (en-AU)
- Date Format: DD/MM/YYYY via
formatDateAU()utility - Time: H:MM am/pm AEST/AEDT via
formatTimeAU()andgetAustralianTimezone() - Currency: AUD ($) —
total_cost_usdconverted to AUD for display - Spelling: colour, behaviour, analyse, optimise, centre
- Footer: Australian date format in dashboard footers
Dashboard Patterns — Before/After Examples
Demonstrates transformation from generic grid dashboard to Scientific Luxury timeline dashboard.
---
Example: Metrics Dashboard
BEFORE — Generic Grid Dashboard
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
export default function Dashboard() {
return (
<div className="min-h-screen bg-gray-50 p-8">
<h1 className="text-2xl font-bold text-gray-900 mb-6">
Analytics Dashboard
</h1>
{/* Metric cards — symmetrical grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<Card className="rounded-lg shadow-md">
<CardHeader>
<CardTitle className="text-sm text-gray-500">Total Runs</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-gray-900">1,000</p>
<span className="text-green-500 text-sm">+10%</span>
</CardContent>
</Card>
<Card className="rounded-lg shadow-md">
<CardHeader>
<CardTitle className="text-sm text-gray-500">Success Rate</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-gray-900">99.99%</p>
</CardContent>
</Card>
<Card className="rounded-lg shadow-md">
<CardHeader>
<CardTitle className="text-sm text-gray-500">Active</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-blue-500">50</p>
</CardContent>
</Card>
<Card className="rounded-lg shadow-md">
<CardHeader>
<CardTitle className="text-sm text-gray-500">Errors</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold text-red-500">0</p>
</CardContent>
</Card>
</div>
{/* Table */}
<Card className="rounded-lg shadow-md">
<table className="w-full">
<thead className="bg-gray-100">
<tr>
<th className="p-3 text-left text-gray-600">Agent</th>
<th className="p-3 text-left text-gray-600">Status</th>
<th className="p-3 text-left text-gray-600">Time</th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="p-3">Agent 1</td>
<td className="p-3"><span className="text-green-500">●</span> Running</td>
<td className="p-3">2 min ago</td>
</tr>
</tbody>
</table>
</Card>
</div>
);
}Violations:
bg-gray-50(banned — must be#050505)grid-cols-4(banned — card grid layout)rounded-lg(banned — must berounded-sm)shadow-md(banned — no generic shadows)font-bold(wrong hierarchy — usefont-extralightfor titles)text-gray-500,text-gray-900(banned — use white opacity)text-green-500,text-blue-500(banned — use spectral colours)- Round numbers:
1,000,99.99%,50,0(AI tells) bg-gray-100table header (banned)- No Framer Motion animations
- No loading skeleton or empty state
- No connection indicator
AFTER — Scientific Luxury Timeline Dashboard
import { Suspense } from 'react';
import { motion } from 'framer-motion';
const FALLBACK_RUNS: AgentRun[] = [];
async function fetchRuns() {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
try {
const res = await fetch(`${backendUrl}/api/agents/stats`, { cache: 'no-store' });
if (!res.ok) return FALLBACK_RUNS;
return res.json();
} catch {
return FALLBACK_RUNS;
}
}
function LoadingSkeleton() {
return (
<div className="space-y-8 px-8 py-8">
<motion.div
className="h-12 w-full rounded-sm bg-white/5"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="ml-8 h-20 rounded-sm bg-white/5"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
/>
))}
</div>
);
}
export default async function AnalyticsDashboard() {
const runs = await fetchRuns();
return (
<div className="relative min-h-screen bg-[#050505]">
{/* Header */}
<header className="border-b border-white/[0.06] px-8 py-6">
<p className="text-[10px] uppercase tracking-[0.3em] text-white/30">
Real-Time Analytics
</p>
<h1 className="text-4xl font-extralight tracking-tight text-white">
Agent Analytics
</h1>
{/* DataStrip — replaces card grid */}
<div className="mt-4 flex items-center gap-8 border-[0.5px] border-white/[0.06] bg-white/[0.01] px-6 py-3">
<div className="flex items-baseline gap-2">
<span className="text-[10px] tracking-widest text-white/30 uppercase">Runs</span>
<span className="font-mono text-lg font-medium tabular-nums text-[#00F5FF]">1,247</span>
</div>
<div className="h-4 w-px bg-white/10" />
<div className="flex items-baseline gap-2">
<span className="text-[10px] tracking-widest text-white/30 uppercase">Success</span>
<span className="font-mono text-lg font-medium tabular-nums text-[#00FF88]">94.7%</span>
</div>
<div className="h-4 w-px bg-white/10" />
<div className="flex items-baseline gap-2">
<span className="text-[10px] tracking-widest text-white/30 uppercase">Active</span>
<span className="font-mono text-lg font-medium tabular-nums text-[#00F5FF]">47</span>
</div>
<div className="h-4 w-px bg-white/10" />
<div className="flex items-baseline gap-2">
<span className="text-[10px] tracking-widest text-white/30 uppercase">Errors</span>
<span className="font-mono text-lg font-medium tabular-nums text-[#FF4444]">3</span>
</div>
</div>
</header>
{/* Timeline Content */}
<main className="px-8 py-8">
<Suspense fallback={<LoadingSkeleton />}>
<div className="relative pl-4">
{/* Vertical Timeline Spine */}
<motion.div
initial={{ scaleY: 0 }}
animate={{ scaleY: 1 }}
transition={{ delay: 0.3, duration: 0.8, ease: [0.19, 1, 0.22, 1] }}
className="absolute top-0 bottom-0 left-8 w-px origin-top
bg-gradient-to-b from-white/10 via-white/5 to-transparent"
/>
<div className="space-y-6">
{runs.map((run, index) => (
<motion.div
key={run.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
delay: index * 0.1,
duration: 0.5,
ease: [0.19, 1, 0.22, 1],
}}
className="relative ml-8 rounded-sm border-[0.5px] border-white/[0.06] bg-white/[0.01] px-6 py-4"
>
<div className="flex items-center gap-3">
<motion.div
className="h-2 w-2 rounded-full"
style={{ backgroundColor: getStatusColour(run.status) }}
animate={
run.status === 'in_progress'
? { scale: [1, 1.3, 1], opacity: [1, 0.6, 1] }
: {}
}
transition={{ duration: 2, repeat: Infinity }}
/>
<h3 className="text-sm font-light text-white/90">{run.agent}</h3>
<span className="font-mono text-[10px] text-white/30">
{formatTimestampAU(run.timestamp)}
</span>
</div>
</motion.div>
))}
</div>
</div>
</Suspense>
</main>
{/* Footer */}
<footer className="border-t border-white/[0.06] px-8 py-4">
<p className="font-mono text-[10px] text-white/20">
{new Date().toLocaleDateString('en-AU')}
</p>
</footer>
</div>
);
}---
Summary of Transformations
| Before | After |
|---|---|
bg-gray-50 | bg-[#050505] |
grid-cols-4 card grid | DataStrip horizontal metrics |
rounded-lg shadow-md cards | rounded-sm border-[0.5px] border-white/[0.06] |
font-bold text-gray-900 | font-extralight tracking-tight text-white |
text-green-500 | text-[#00FF88] spectral Emerald |
Static ● icon | Breathing orb with Framer Motion |
| No animations | Staggered entry + timeline spine animation |
| No loading state | Skeleton matching final layout |
| No empty state | Centred breathing orb + guidance |
Round numbers (1,000) | Organic numbers (1,247) |
bg-gray-100 table header | Timeline layout (no table needed) |
| No connection indicator | Status dot with "Live" label |
Generic Dashboard Page — Template
Portable dashboard page template. No project-specific design system. Replace tokens with your own.
import { Suspense } from 'react';
import { motion } from 'framer-motion';
// -- Types --
interface Metric {
label: string;
value: string | number;
colour?: string;
}
interface ListItem {
id: string;
title: string;
status: string;
timestamp: string;
value?: string | number;
}
// -- Data Fetching --
async function fetchData(): Promise<ListItem[]> {
const baseUrl = process.env.API_URL || 'http://localhost:8000';
try {
const res = await fetch(`${baseUrl}/api/data`, { cache: 'no-store' });
if (!res.ok) return [];
return res.json();
} catch {
return [];
}
}
// -- Loading Skeleton --
function LoadingSkeleton() {
return (
<div className="space-y-6 p-8">
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="h-20 rounded-sm bg-white/5"
animate={{ opacity: [0.4, 0.8, 0.4] }}
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.15 }}
/>
))}
</div>
);
}
// -- Empty State --
function EmptyState() {
return (
<div className="flex flex-col items-center justify-center py-20 text-center">
<motion.div
className="h-3 w-3 rounded-full bg-white/20"
animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
transition={{ duration: 2, repeat: Infinity }}
/>
<h3 className="mt-4 text-lg font-light text-white/80">
No data available
</h3>
<p className="mt-1 text-sm text-white/40">
Data will appear here when available.
</p>
</div>
);
}
// -- Metrics Bar --
function MetricsBar({ metrics }: { metrics: Metric[] }) {
return (
<div className="flex items-center gap-6 border border-white/[0.08] bg-white/[0.02] px-6 py-3 rounded-sm">
{metrics.map((metric, index) => (
<div key={metric.label} className="flex items-baseline gap-2">
{index > 0 && <div className="mr-4 h-4 w-px bg-white/10" />}
<span className="text-xs uppercase tracking-wider text-white/40">
{metric.label}
</span>
<span
className="font-mono text-lg font-medium tabular-nums"
style={{ color: metric.colour || 'rgba(255,255,255,0.8)' }}
>
{metric.value}
</span>
</div>
))}
</div>
);
}
// -- List Item --
function DataRow({ item, index }: { item: ListItem; index: number }) {
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{
delay: index * 0.08,
duration: 0.4,
ease: [0.4, 0, 0.2, 1],
}}
className="rounded-sm border border-white/[0.08] bg-white/[0.02] px-6 py-4"
>
<div className="flex items-center justify-between">
<h3 className="text-sm font-light text-white/90">{item.title}</h3>
<span className="text-xs text-white/40">{item.timestamp}</span>
</div>
{item.value && (
<p className="mt-1 font-mono text-lg font-medium text-white/80">
{item.value}
</p>
)}
</motion.div>
);
}
// -- Dashboard Content --
function DashboardContent({ data }: { data: ListItem[] }) {
if (data.length === 0) return <EmptyState />;
return (
<div className="space-y-4">
{data.map((item, index) => (
<DataRow key={item.id} item={item} index={index} />
))}
</div>
);
}
// -- Page --
export default async function DashboardPage() {
const data = await fetchData();
const metrics: Metric[] = [
{ label: 'Total', value: data.length },
{ label: 'Active', value: data.filter((d) => d.status === 'active').length },
{ label: 'Done', value: data.filter((d) => d.status === 'done').length },
];
return (
<div className="min-h-screen bg-[#0A0A0A]">
<header className="border-b border-white/[0.08] p-8">
<h1 className="text-3xl font-light text-white">Dashboard</h1>
<div className="mt-4">
<MetricsBar metrics={metrics} />
</div>
</header>
<main className="p-8">
<Suspense fallback={<LoadingSkeleton />}>
<DashboardContent data={data} />
</Suspense>
</main>
<footer className="border-t border-white/[0.08] p-4">
<p className="text-xs text-white/30">
{new Date().toLocaleDateString()}
</p>
</footer>
</div>
);
}Customisation Points
- Replace
bg-[#0A0A0A]with your background token - Replace
border-white/[0.08]with your border token - Replace
rounded-smwith your chosen border radius - Add status colours via your theme config
- Replace Framer Motion with CSS transitions if not using Framer
- Adjust MetricsBar to match your metrics component
- Add sidebar navigation per your layout requirements
SL Dashboard Page — Template
Scientific Luxury dashboard page template. Uses timeline layout, DataStrip metrics, OLED Black, and Framer Motion.
import { Suspense } from 'react';
import { motion } from 'framer-motion';
// -- Types --
interface DashboardMetric {
label: string;
value: string | number;
variant?: 'info' | 'success' | 'warning' | 'error';
}
interface DashboardItem {
id: string;
title: string;
status: string;
timestamp: string;
value?: string | number;
}
// -- Data Fetching (Server Component) --
const FALLBACK_DATA: DashboardItem[] = [];
async function fetchDashboardData(): Promise<DashboardItem[]> {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000';
try {
const res = await fetch(`${backendUrl}/api/dashboard/data`, {
cache: 'no-store',
});
if (!res.ok) return FALLBACK_DATA;
return res.json();
} catch {
return FALLBACK_DATA;
}
}
// -- Loading Skeleton --
function LoadingSkeleton() {
return (
<div className="space-y-8 px-8 py-8">
{/* DataStrip skeleton */}
<motion.div
className="h-12 w-full rounded-sm bg-white/5"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
{/* Timeline skeleton */}
{[0, 1, 2].map((i) => (
<motion.div
key={i}
className="ml-8 h-24 rounded-sm bg-white/5"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity, delay: i * 0.2 }}
/>
))}
</div>
);
}
// -- Empty State --
function EmptyState() {
return (
<div className="flex flex-col items-center justify-center py-20">
<motion.div
className="h-3 w-3 rounded-full bg-white/20"
animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
transition={{ duration: 2, repeat: Infinity, ease: 'easeInOut' }}
/>
<h3 className="mt-4 text-xl font-light text-white">
No data available
</h3>
<p className="mt-2 font-mono text-xs text-white/40">
Data will appear here when available.
</p>
</div>
);
}
// -- Timeline Node --
const VARIANT_COLOURS = {
info: '#00F5FF',
success: '#00FF88',
warning: '#FFB800',
error: '#FF4444',
default: '#6B7280',
};
function TimelineNode({
item,
index,
}: {
item: DashboardItem;
index: number;
}) {
const colour =
VARIANT_COLOURS[item.status as keyof typeof VARIANT_COLOURS] ||
VARIANT_COLOURS.default;
const isActive = item.status === 'in_progress';
return (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
delay: index * 0.1,
duration: 0.5,
ease: [0.19, 1, 0.22, 1],
}}
className="relative ml-8 rounded-sm border-[0.5px] border-white/[0.06] bg-white/[0.01] px-6 py-4"
>
{/* Status orb */}
<div className="flex items-center gap-3">
<motion.div
className="h-2 w-2 rounded-full"
style={{ backgroundColor: colour }}
animate={
isActive
? { scale: [1, 1.3, 1], opacity: [1, 0.6, 1] }
: {}
}
transition={
isActive
? { duration: 2, repeat: Infinity, ease: 'easeInOut' }
: {}
}
/>
<h3 className="text-sm font-light text-white/90">{item.title}</h3>
</div>
<div className="mt-2 flex items-baseline gap-4">
{item.value && (
<span
className="font-mono text-lg font-medium tabular-nums"
style={{ color: colour }}
>
{item.value}
</span>
)}
<span className="font-mono text-[10px] text-white/30">
{item.timestamp}
</span>
</div>
</motion.div>
);
}
// -- DataStrip --
function DataStrip({ metrics }: { metrics: DashboardMetric[] }) {
return (
<div className="flex items-center gap-8 border-[0.5px] border-white/[0.06] bg-white/[0.01] px-6 py-3">
{metrics.map((metric, index) => (
<div key={metric.label} className="flex items-center gap-2">
{index > 0 && <div className="mr-6 h-4 w-px bg-white/10" />}
<span className="text-[10px] uppercase tracking-widest text-white/30">
{metric.label}
</span>
<span
className="font-mono text-lg font-medium tabular-nums"
style={{
color:
VARIANT_COLOURS[
metric.variant as keyof typeof VARIANT_COLOURS
] || '#00F5FF',
}}
>
{metric.value}
</span>
</div>
))}
</div>
);
}
// -- Dashboard Content --
function DashboardContent({ data }: { data: DashboardItem[] }) {
if (data.length === 0) return <EmptyState />;
return (
<div className="relative pl-4">
{/* Vertical Timeline Spine */}
<motion.div
initial={{ scaleY: 0 }}
animate={{ scaleY: 1 }}
transition={{ delay: 0.3, duration: 0.8, ease: [0.19, 1, 0.22, 1] }}
className="absolute top-0 bottom-0 left-8 w-px origin-top
bg-gradient-to-b from-white/10 via-white/5 to-transparent"
/>
{/* Timeline Nodes */}
<div className="space-y-6">
{data.map((item, index) => (
<TimelineNode key={item.id} item={item} index={index} />
))}
</div>
</div>
);
}
// -- Page Component --
export default async function DashboardPage() {
const data = await fetchDashboardData();
const metrics: DashboardMetric[] = [
{ label: 'Total', value: data.length },
{
label: 'Active',
value: data.filter((d) => d.status === 'in_progress').length,
variant: 'info',
},
{
label: 'Completed',
value: data.filter((d) => d.status === 'completed').length,
variant: 'success',
},
{
label: 'Failed',
value: data.filter((d) => d.status === 'failed').length,
variant: 'error',
},
];
return (
<div className="relative min-h-screen bg-[#050505]">
{/* Header */}
<header className="border-b border-white/[0.06] px-8 py-6">
<p className="text-[10px] uppercase tracking-[0.3em] text-white/30">
Real-Time Monitoring
</p>
<h1 className="text-4xl font-extralight tracking-tight text-white">
Dashboard Title
</h1>
<div className="mt-4">
<DataStrip metrics={metrics} />
</div>
</header>
{/* Content */}
<main className="px-8 py-8">
<Suspense fallback={<LoadingSkeleton />}>
<DashboardContent data={data} />
</Suspense>
</main>
{/* Footer */}
<footer className="border-t border-white/[0.06] px-8 py-4">
<p className="font-mono text-[10px] text-white/20">
{new Date().toLocaleDateString('en-AU')}
</p>
</footer>
</div>
);
}Checklist
- [x] OLED Black
bg-[#050505]background - [x] Timeline layout (not card grid)
- [x] DataStrip for summary metrics
- [x]
border-[0.5px] border-white/[0.06]borders - [x]
rounded-smonly - [x]
font-mono tabular-numsfor data values - [x]
text-[10px] tracking-[0.3em] uppercasefor labels - [x] Framer Motion for all animations
- [x] Staggered entry for timeline nodes
- [x] Breathing orb for active states
- [x] Loading skeleton matching layout
- [x] Empty state with breathing orb
- [x] Fallback data for server fetch
- [x] Australian date format in footer
Dashboard Patterns — Anti-Patterns Reference
Extracted from SKILL.md §Anti-Patterns, §BANNED: Card Grid, and §The Three Laws of Dashboards.---
Anti-Pattern 1: Card Grid Layout (grid-cols-2 / grid-cols-4)
Why it fails: Violates the first law of dashboards ("Timeline, never grid") and the Scientific Luxury layout rules.
// REJECTED
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<h3>Total Users</h3>
<p className="text-3xl font-bold">1,234</p>
</Card>
<Card>
<h3>Revenue</h3>
<p className="text-3xl font-bold">$50,000</p>
</Card>
</div>
// CORRECT — DataStrip for metrics
<DataStrip
metrics={[
{ label: 'Total', value: runs.length },
{ label: 'Active', value: activeRuns.length, variant: 'info' },
{ label: 'Completed', value: completedRuns.length, variant: 'success' },
{ label: 'Failed', value: failedRuns.length, variant: 'error' },
]}
/>
// CORRECT — Timeline for content
<div className="relative pl-4">
<motion.div
initial={{ scaleY: 0 }}
animate={{ scaleY: 1 }}
transition={{ delay: 0.3, duration: 0.8, ease: [0.19, 1, 0.22, 1] }}
className="absolute top-0 bottom-0 left-8 w-px origin-top
bg-gradient-to-b from-white/10 via-white/5 to-transparent"
/>
<div className="space-y-8">
{runs.map((run, index) => (
<AgentNode key={run.id} run={run} index={index} />
))}
</div>
</div>---
Anti-Pattern 2: Standard <Card> with rounded-lg
Why it fails: Wrong corners, wrong aesthetic. rounded-lg is banned by the Scientific Luxury design system.
// REJECTED
<Card className="rounded-lg bg-white shadow-md p-6">
<CardTitle>Agent Status</CardTitle>
<CardContent>Running</CardContent>
</Card>
// CORRECT
<div className="rounded-sm border-[0.5px] border-white/[0.06] bg-white/[0.01] p-6">
<p className="text-[10px] uppercase tracking-[0.3em] text-white/30">Agent Status</p>
<span className="font-mono text-lg text-[#00F5FF]">Running</span>
</div>---
Anti-Pattern 3: CSS Transitions (transition: all 0.3s linear)
Why it fails: Banned by Bezier (Council of Logic). Linear motion is mechanical and lifeless.
// REJECTED
<div className="transition-all duration-300 ease-linear hover:bg-gray-100">
Dashboard card
</div>
// CORRECT — Framer Motion with physics easing
<motion.div
whileHover={{ backgroundColor: 'rgba(255,255,255,0.02)' }}
transition={{ duration: 0.4, ease: [0.19, 1, 0.22, 1] }}
>
Dashboard element
</motion.div>---
Anti-Pattern 4: White/Light Background Dashboards
Why it fails: Violates the OLED Black requirement. Every dashboard surface must use #050505.
// REJECTED
<div className="min-h-screen bg-white">
<div className="bg-gray-50 p-8">Dashboard content</div>
</div>
// CORRECT
<div className="min-h-screen bg-[#050505]">
<div className="px-8 py-8">Dashboard content</div>
</div>---
Anti-Pattern 5: No Loading Skeleton
Why it fails: Causes layout shift when data loads. Skeletons must match the final layout structure.
// REJECTED — no loading state or generic spinner
{isLoading ? <Spinner /> : <DashboardContent />}
// CORRECT — skeleton matching final layout
function LoadingSkeleton() {
return (
<div className="space-y-8">
<motion.div
className="h-10 w-64 rounded-sm bg-white/5"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
<motion.div
className="h-48 w-full rounded-sm bg-white/5"
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity, delay: 0.2 }}
/>
</div>
);
}---
Anti-Pattern 6: No Connection Indicator
Why it fails: Users cannot tell if displayed data is live, stale, or disconnected.
// REJECTED — no connection status visible
<DashboardContent data={data} />
// CORRECT — connection indicator always present
<div className="flex items-center gap-2">
<motion.span
className="h-1.5 w-1.5 rounded-full"
style={{ backgroundColor: isConnected ? '#00FF88' : '#FF4444' }}
animate={isConnected ? { opacity: [1, 0.4, 1] } : {}}
transition={{ duration: 2, repeat: Infinity }}
/>
<span className="font-mono text-[10px] text-white/40">
{isConnected ? 'Live' : 'Offline'}
</span>
</div>---
Anti-Pattern 7: Hardcoded Status Colours
Why it fails: Drift from the spectral palette. All status colours must come from the central config.
// REJECTED — hardcoded colours
<span style={{ color: status === 'completed' ? 'green' : 'red' }}>
{status}
</span>
// CORRECT — centralised config
import { getStatusConfig } from './constants';
const config = getStatusConfig(status);
<span style={{ color: config.colour.primary }}>{status}</span>---
Anti-Pattern 8: setInterval Without Cleanup
Why it fails: Memory leak when component unmounts. Intervals persist and pile up.
// REJECTED — no cleanup
useEffect(() => {
setInterval(fetchMetrics, 30_000);
}, []);
// CORRECT — cleanup on unmount
useEffect(() => {
fetchMetrics();
const interval = setInterval(fetchMetrics, 30_000);
return () => clearInterval(interval);
}, []);---
The Three Laws (Summary)
| Law | Rule | Violation |
|---|---|---|
| 1. Timeline, never grid | No grid-cols-2/grid-cols-4 | Use timelines, DataStrips, orbital arrangements |
| 2. Spectral, never static | Every status has spectral colour + animation | No grey placeholders, no hardcoded colours |
| 3. Real-time, never stale | Live data via Realtime or 30s polling | Always show connection status |
Generic Dashboard Patterns
Portable dashboard design and engineering patterns. No project-specific component or design system references.
---
Dashboard Layout Principles
1. Metrics Summary Bar
Place key metrics in a horizontal bar at the top of the page. Each metric shows a label + value. Use separators between metrics rather than card containers.
2. Primary Content Area
The main content area should use one of these layouts:
| Layout | Best For |
|---|---|
| Timeline (vertical) | Activity feeds, event logs, sequential data |
| Split view (asymmetric) | Chart + detail panel, list + preview |
| Tabbed | Multiple data views of the same entity |
| Scrollable table | Dense tabular data |
Avoid equal-column card grids for primary content — they waste space and lack hierarchy.
3. Sidebar Navigation
- Fixed on desktop (collapsible)
- Overlay on tablet
- Bottom bar on mobile
- Active state clearly indicated
---
Data Fetching Patterns
Initial Load
Use server-side rendering or SSR for initial data. Always provide fallback data so the page renders even when the backend is unavailable.
async function fetchData() {
try {
const res = await fetch(url, { cache: 'no-store' });
if (!res.ok) return FALLBACK_DATA;
return res.json();
} catch {
return FALLBACK_DATA;
}
}Polling
For non-realtime data, poll at a reasonable interval (15-60 seconds). Always clean up intervals on component unmount.
useEffect(() => {
fetchData();
const interval = setInterval(fetchData, 30_000);
return () => clearInterval(interval);
}, []);Realtime
For live data, use WebSocket or server-sent events. Subscribe on mount, unsubscribe on unmount.
Connection Status
Always display connection state to the user:
- Connected: Green indicator + "Live" label
- Reconnecting: Amber indicator + "Reconnecting"
- Disconnected: Red indicator + "Offline"
---
Loading States
Skeleton Loaders
Every dashboard page must show a loading skeleton that matches the final layout structure:
- Use low-opacity background blocks matching the shape of final elements
- Animate with a breathing opacity effect
- Stagger delays for visual interest
- Never use spinners for page-level loading
Empty States
When no data is available:
- Show a centred message with guidance
- Include a visual indicator (animated dot or icon)
- Provide a clear next action ("Run your first agent" or "Import data")
---
Status Indicators
Animated Dots
Use small animated dots for live status:
- Breathing animation (scale + opacity) for active states
- Static dot for idle/completed states
- Colour mapped to semantic meaning
Colour Mapping
Define a central status-to-colour mapping. Never hardcode colours in components.
const STATUS_COLOURS = {
active: 'var(--colour-primary)',
success: 'var(--colour-success)',
warning: 'var(--colour-warning)',
error: 'var(--colour-danger)',
idle: 'var(--colour-neutral)',
};---
Animation Patterns
Staggered Entry
List items should animate in with incremental delays:
Item 1: delay 0.0s
Item 2: delay 0.1s
Item 3: delay 0.2sState Transitions
When data changes state (e.g., "pending" to "completed"), animate the colour transition smoothly.
Ambient Effects
For dashboards with active processes, show subtle ambient effects (background glow, breathing indicators) to communicate liveness.
---
Responsive Strategy
| Breakpoint | Sidebar | Metrics | Content |
|---|---|---|---|
| Desktop (>=1024px) | Fixed (240px) | Horizontal bar | Full layout |
| Tablet (768-1023px) | Overlay | Scrollable bar | Full width |
| Mobile (<768px) | Bottom nav | 2-col or scrollable | Stacked |
---
Checklist for New Dashboards
Layout
- [ ] Metrics summary at top (horizontal bar, not card grid)
- [ ] Primary content uses timeline, split, or table layout
- [ ] Sidebar navigation with active state indicator
- [ ] Responsive across desktop, tablet, mobile
Data
- [ ] Server-side initial fetch with fallback data
- [ ] Polling or realtime subscription for live data
- [ ] Connection status indicator visible
- [ ] Loading skeleton matching final layout
- [ ] Empty state with guidance
Animation
- [ ] Staggered entry for list items
- [ ] Breathing animation for active indicators
- [ ] Smooth state transitions
- [ ] Physics-based easing (not linear)
Accessibility
- [ ] Keyboard navigation for all interactive elements
- [ ] Screen reader labels on status indicators
- [ ] Sufficient colour contrast
- [ ] Focus management on data updates
Dashboard Patterns — Component Library Reference
Extracted fromSKILL.md. Component library atapps/web/components/status-command-centre/.
---
Component Catalogue
Main Components
| Component | Variants | Purpose |
|---|---|---|
StatusCommandCentre | full, compact, minimal | Complete dashboard view with timeline and metrics |
Data Display
| Component | Usage | Key Props |
|---|---|---|
DataStrip | Horizontal inline metrics bar | metrics: Array<{ label, value, variant? }> |
MetricTile | Single stat with trend indicator | label, value, trend, variant |
DataStrip uses:
- JetBrains Mono (
font-mono) for values text-[10px] tracking-widest uppercasefor labels- Pipe separators (
w-px bg-white/10) between metrics - Spectral glow on non-zero highlighted values
- Variants:
info=#00F5FF,success=#00FF88,warning=#FFB800,error=#FF4444
Visualisation
| Component | Usage | Key Props |
|---|---|---|
ProgressOrb | Circular progress indicator with glow | progress, colour, size |
ProgressRing | Ring-style progress indicator | progress, colour |
StatusPulse | Breathing status dot | status, size |
StatusBadge | Text label with status colour | status, label |
Activity
| Component | Usage | Key Props |
|---|---|---|
AgentNode | Timeline node for agent execution | run, index |
AgentActivityCard | Detailed agent activity view | agent, runs |
ActivityTimeline | Step-by-step timeline view | steps |
AgentThinkingIndicator | Animated thinking state | isThinking |
Utility
| Component | Usage | Key Props |
|---|---|---|
NotificationStream | Sidebar notification feed | notifications |
ElapsedTimer | Live elapsed time counter | startTime |
---
Hooks
| Hook | Purpose | Returns |
|---|---|---|
useElapsedTime | Tracks elapsed time from a start timestamp | elapsed: string |
useCountdown | Counts down to a target time | remaining: string |
useStatusTransitions | Detects state changes, triggers animations | transition: { from, to } |
useStatusColourTransition | Smooth colour interpolation on status change | colour: string |
---
Utility Functions
| Function | Purpose | Format |
|---|---|---|
formatElapsedAU | Formats elapsed time in Australian style | "2m 34s" |
formatTimestampAU | Formats timestamp in DD/MM/YYYY H:MM am/pm | "26/03/2026 2:30 pm" |
formatDateAU | Formats date only | "26/03/2026" |
getAustralianTimezone | Returns current AU timezone | "AEDT" or "AEST" |
---
Spectral Colour Mapping
| Status | Colour | Hex | Animation |
|---|---|---|---|
pending | Slate | hsl(220 14% 46%) | Pulse (idle) |
in_progress | Blue | hsl(217 91% 60%) | Spin (active) |
awaiting_verification | Amber | hsl(38 92% 50%) | Pulse (active) |
verification_in_progress | Amber | hsl(38 92% 50%) | Spin (active) |
verification_passed | Green | hsl(142 76% 36%) | None (idle) |
verification_failed | Red | hsl(0 84% 60%) | Pulse (urgent) |
completed | Emerald | #00FF88 | None (idle) |
failed | Red | #FF4444 | Pulse (urgent) |
blocked | Slate | Muted | None (idle) |
escalated_to_human | Magenta | #FF00FF | Pulse (urgent) |
Access: getStatusConfig(status) from constants.ts.
---
Dashboard Page Structure
Every dashboard page follows this pattern:
1. Server Component wrapper (data fetch with fallback)
2. Header
- Category label (text-[10px] tracking-[0.3em] uppercase text-white/30)
- Page title (text-4xl font-extralight tracking-tight text-white)
- DataStrip (inline metrics)
3. Content (wrapped in Suspense with LoadingSkeleton)
- Timeline or orbital layout (never card grid)
4. Footer
- Australian date format (font-mono text-[10px] text-white/20)---
Data Fetching Patterns
| Pattern | When | Implementation |
|---|---|---|
| Server Component | Initial page load | fetch() with cache: 'no-store', fallback data |
| Polling | Non-realtime pages | setInterval(fetchMetrics, 30_000) with cleanup |
| Supabase Realtime | Live dashboards | Channel subscription to agent_runs table |
Connection status states:
- Connected: Emerald breathing dot + "Live" label
- Reconnecting: Amber dot + "Reconnecting" label
- Disconnected: Red dot + "Offline" label
---
Component Decision Matrix
| Need | Component |
|---|---|
| Inline metrics row | DataStrip |
| Single stat with trend | MetricTile |
| Agent execution status | AgentNode |
| Circular progress | ProgressOrb or ProgressRing |
| Status dot | StatusPulse |
| Status label | StatusBadge |
| Step timeline | ActivityTimeline |
| Notification sidebar | NotificationStream |
| Elapsed time counter | ElapsedTimer |