Build a one-screen operator overview
By the end of this recipe you'll have a desktop dashboard that fits the whole operator's morning — KPI strip, today's tasks, flagged anomalies, recent activity — into a single viewport, refreshes itself every 30 seconds, and adapts to the user's role so a clerk sees less than a manager.
Overview
A three-band layout: KPI strip up top, two operational panels in the middle, recent activity along the bottom. No scroll on a 1440px display.
An operator dashboard is not a report. The job of this screen is to answer "is anything on fire?" in ten seconds, then offer the next action. The KPI strip gives the headline; the two-column panels split into "things I need to do today" (tasks) and "things the system noticed" (flags); the activity strip at the bottom is the read-only proof of work.
We poll, not subscribe. A 30-second useInterval refresh covers 95% of dashboards and avoids the WebSocket reconnection logic that bites you every Friday at 5pm. If you genuinely need sub-second updates, swap the hook for a subscription — the model stays the same.
Role-based filtering happens at the data layer, not the JSX. A clerk and a manager render the same component; the API returns a different set of panels based on the bearer token. That keeps the front-end honest and stops the "manager view via DevTools" exploit dead.
Skip this recipe if you're on mobile-first — the KPI hero + drilldown recipe is purpose-built for that. Use this one when the operator sits at a desk and needs everything visible at once.
What you'll build
The same dashboard in three contexts. Whole shape stays; density and content change.
You can see this live in
Portal demos that wire this operator overview in context. Open one to see the morning view.
Required pieces
Everything this recipe pulls from @corelithzw/react. Each link opens the reference page.
@corelithzw/react exports used here: Stack, KpiGrid, RowCard, EmptyState, Alert, Skeleton, useInterval. useInterval is the first new hook this batch introduces — keep the name.
Step-by-step build
Lay out three bands with CSS Grid, not flex stacks
Grid lets you declare the row heights — auto, 1fr, auto — so the middle panel absorbs the leftover viewport. A flex column would let the panels overflow and ruin the no-scroll promise. Set min-height: 0 on the scrollable children so the grid actually clips.
Row 2: 1fr — task + flag panels absorb leftover height
Row 3: auto — activity strip pinned to bottom
min-height: 0 on row 2 children is what makes overflow scroll, not push the page.
Switch to the Code tab to see the snippet.
Build the KPI strip from one source array
Feed KpiGrid a flat array of items and let the role filter remove the ones a clerk shouldn't see. The grid handles spacing, deltas and alignment so you only think about the data.
Two panels: tasks and flags, both scrollable
Each panel is a tiny header plus a vertically-scrolling list of RowCards. The list — not the whole page — owns the scrollbar, so the KPI strip and the activity bar stay locked in place.
Refresh quietly with useInterval — no spinner
The first load shows a skeleton, but every subsequent refresh updates in place. Show a tiny "Updated 12s ago" caption so the operator knows the screen is alive without anything moving. Pause the interval when the tab is hidden so you don't drain laptop batteries.
+30s → load() in background → state updates → React diffs the UI
No spinner. The KPI numbers tick up; the rows shuffle. The operator's eye catches it.
Switch to the Code tab to see the snippet.
Final composition
The whole OperatorOverview, assembled. Layout, role filter, 30-second poll, skeleton on first load, inline error.
Variations
Three forks. Same shape, different volume.
Dense mode
For power users who want more rows visible. Halve the gap and the row padding via a single density flag — every spacing token follows.
// Density flag on the wrapper
<div data-density="dense" style={{ gap: 8 }}>
<KpiGrid items={kpis} density="dense" />
<Panel density="dense">
{tasks.map((t) =>
<RowCard density="dense" key={t.id} {...t} />)}
</Panel>
</div>
Single-column on tablet
Below 900px collapse the two panels into one. The bottom activity stays; the KPI strip wraps. No layout-shift on resize because grid handles it.
// Just change one grid-template
const isNarrow = useMediaQuery('(max-width: 900px)');
<div style={{
gridTemplateColumns:
isNarrow ? '1fr' : '1fr 1fr',
}}>
Real-time via subscription
Swap useInterval for an EventSource when sub-second updates matter. Same model in, same JSX out.
// Drop-in replacement for useInterval
useEffect(() => {
const es = new EventSource('/api/overview/stream');
es.onmessage = (e) => setData(JSON.parse(e.data));
return () => es.close();
}, [role]);
Accessibility
What this recipe takes care of for you.
- Each panel is a landmark. The task panel and flag panel use
<section aria-label="…">so a screen-reader user can jump panel-to-panel with the rotor. - Refresh doesn't steal focus. The 30-second poll mutates state in place. The user's caret stays where it was — typing in a task description doesn't get wiped.
- Tab hidden = polling paused.
useIntervalwithpauseWhenHidden: truestops fetching when the document is backgrounded — fewer requests, longer laptop battery, no surprises when the user returns. - The stale-data warning is non-modal. When a poll fails we show an
Alert tone="warning"at the top of the KPI strip — the operator keeps working off the last-known good data while the retry button is one tab away. - Density mode keeps the same hit target. "Dense" shrinks the visual padding but keeps a 44px tap area via a transparent before-pseudo — pointer-only operators get more rows, finger-only operators still get a thumb-sized target.
- Role filter is server-enforced. The clerk literally cannot see manager-only KPIs — they're not in the response. JSX-only role guards are theatre.