Home/ Cookbook/ Dashboards/ Operator overview

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.

Dashboards 3 primitives · 3 blocks · 0 patterns ~40 min React · @corelithzw/react

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.

Sales$12.4k
Tickets48
Cash$3.1k
Flags2
Tasks
Flags
09:14 · Tendai opened day · 09:22 · Refund $12 · 09:31 · Stock recount
01 Manager view — everything
Sales$12.4k
Tickets48
My tasks
09:22 · Refund $12 · 09:31 · Stock recount
02 Clerk view — role-filtered
Sales$12k
Tk48
Cash$3k
Flg2
Tasks
Flags
More fits.
03 Dense view — more rows visible

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

01

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.

Code-only step

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.

Step 1 · Layout@corelithzw/react
02

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.

Sales today
$12,420
+6%
Tickets
48
−2%
Cash in drawer
$3,140
Open flags
2
Step 2 · KPI strip
03

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.

Today's tasks
Approve refund — $422h
Close stock countEoD
Flagged anomalies
Drawer over by $18
Refund > 5% of sales
Step 3 · Panels
04

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.

Code-only step

+30sload() 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.

Step 4 · Polling

Final composition

The whole OperatorOverview, assembled. Layout, role filter, 30-second poll, skeleton on first load, inline error.

Sales
$12,420
Tickets
48
Cash
$3,140
Flags
2
Today's tasks
Approve refund · 2h
Close stock count · EoD
Flagged anomalies
Drawer over by $18
Refund > 5%
09:14 · day opened · 09:22 · refund $12 · 09:31 · recount Updated 12s ago
OperatorOverview.tsx@corelithzw/react

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. useInterval with pauseWhenHidden: true stops 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.