Dev notes
A fleet management dashboard for smart micro-retail stores. Monitor store status, inventory health, alerts, and sensor data across an entire network in real time — built with tiered polling, optimistic updates, and a data freshness system.
The operator dashboard is a demo of what a real-time fleet management tool looks like for smart vending machines, lobby fridges, and micro-retail kiosks. The kind of thing where an operator manages 20-50 physical locations and needs to know at a glance which ones need attention — low stock, sensor offline, temperature alert, door left open.
It's not connected to real hardware. The data layer uses in-memory mock stores seeded from factory functions, with realistic product catalogs, sensor readings, and alert histories. The interesting part isn't the data — it's how the UI handles real-time updates, stale data, and operator actions without feeling sluggish.
Not all data changes at the same rate, so not all data should poll at the same interval. The dashboard uses three tiers:
All three use staleTime: 0 and refetchOnWindowFocus: true — an operator who tabs back to the dashboard after five minutes should see fresh data immediately, not stale numbers from the last poll cycle.
When an operator clicks "Mark Restocked" on a low-stock item, the stock bar fills immediately. When they dismiss an alert, it vanishes from the list. The UI doesn't wait for the server round-trip.
This uses TanStack Query's onMutate / onError / onSettled lifecycle — the same pattern as the calendar events. onMutate cancels in-flight queries for the affected store, snapshots the cache, and applies the change immediately. onError restores the snapshot. The user sees the change before the request completes, and if it fails, the UI rolls back cleanly.
The bulk actions ("Mark All Restocked" and "Acknowledge All Alerts") show a confirmation modal before executing, since they affect multiple records. A single misclick shouldn't dismiss twenty alerts.
The fleet overview sorts stores worst-first: offline stores at the top, then degraded stores with active alerts, then degraded without alerts, then healthy stores at the bottom. Within each tier, stores sort by name for stability.
This is a deliberate UX choice. An alphabetically sorted grid means the store that needs the most attention might be halfway down the page. Severity-first sorting puts the fires at the top of the screen — the operator opens the dashboard and immediately sees what needs action without scanning.
Store cards also use visual signals: a red left-border accent for critical items in the inventory, amber border for stale sensor data, and color-coded status badges (green for online, amber for degraded, red for offline).
In a real deployment, sensor data can go stale. A fridge might lose WiFi, a temperature probe might die, a payment terminal might stop reporting. The operator needs to know not just "what is the temperature?" but "how old is this reading?"
The freshness system uses three tiers with deterministic thresholds:
The ConnectionQuality indicator shows signal bars (strong, weak, poor, offline) based on the same thresholds. When sensors haven't reported in 30+ minutes, a SensorOfflineCallout banner appears on the inventory tab with the offline duration and last known reading.
All threshold functions accept a now parameter instead of calling Date.now() internally — deterministic inputs for deterministic tests.
A collapsible analytics section sits between the stats bar and the store grid. Three Recharts visualizations: a donut chart showing fleet health distribution (online/degraded/offline), an area chart bucketing alerts into 24 one-hour slots to show whether frequency is rising or falling, and a horizontal bar chart comparing per-store inventory health percentages.
The section defaults to collapsed and persists collapse state in localStorage. Operators who prefer the compact view don't re-collapse every visit. The data transforms are pure functions in their own module — status counting, hourly alert bucketing with a 24h cutoff, and per-store health averaging with zero-capacity safety.
Each store has four tabs: Inventory, Alerts, Activity, and Planogram. The active tab is synced to a ?tab= URL search param so it survives refresh and back/forward navigation.
Quick actions (bulk restock, bulk dismiss, force refresh) show toast notifications on completion. The toast system is framework-agnostic — a createToastStore function returns a plain object with add, remove, and subscribe methods. React binds to it via useSyncExternalStore. Toasts auto-dismiss after 3 seconds.
This pattern keeps the toast state fully testable without rendering any React components — the store is a plain function call that can be tested with timers and subscriber assertions.
The data layer follows the same BFF pattern as the rest of the app: Next.js API routes serve as the proxy layer, and the operator routes use an in-memory data store seeded from factory functions instead of a real backend. This means the dashboard works without any external dependencies — no database, no backend service, just the Next.js dev server.
Pure utility functions live in dedicated modules: operator-utils.ts for sorting and filtering, operator-freshness.ts for threshold calculations, operator-detail.ts for tab helpers and stock categorization, operator-chart-transforms.ts for chart data shaping. Every function is pure, takes explicit inputs, and returns new values — no side effects, no internal state.
One thing that surprised us: Next.js bundles each route handler independently, so a plain module-level variable in operator-data.ts ended up as a separate instance per route. The dismiss route updated its copy of the alerts map, but the alerts GET route read from a different copy where nothing had changed. The fix was to attach the data store to globalThis behind a singleton accessor — the same pattern the Next.js docs recommend for Prisma clients in development mode. Every route handler now shares the same maps regardless of bundling.
After the feature was fully built and working, I went back through it the way I'd review someone else's PR. Not looking for "does it work" — the tests answer that. Looking for "what will bite us in six months." I audited in order of severity: correctness bugs first, then performance, then UX gaps, then code quality, then test coverage.
The in-memory data layer was mutating objects directly — alert.acknowledged = true instead of returning a new object. Not a visible bug in demo mode, but in production React's diffing relies on reference identity. If the object reference doesn't change, React doesn't re-render, and the UI gets out of sync with the data. Fixed by returning new objects from every mutation.
The dismiss button had shared loading state across all alert rows. Dismissing one alert disabled the button on every alert in the list. Fixed by tracking in-flight alert IDs in a Set so each row manages its own state independently.
Two time-dependent functions — getConnectionQuality and toAlertTrendData — called Date.now() internally instead of accepting a now parameter. Every other freshness function in the codebase already took an injectable time value for deterministic testing. These two were the inconsistent ones. Fixed to match the pattern.
A subtler one: the factory generated lastPing timestamps 0-2 hours in the past at module load time, but the connection quality thresholds mark anything over 10 minutes as offline. So every store drifted into "Offline" signal and triggered sensor offline callouts as the dev server ran. Fixed by recomputing lastPing relative to Date.now() on every read from the store accessors, so demo data never goes stale regardless of how long the server has been running.
The trickiest one: dismissing an alert would vanish it momentarily (the optimistic update worked) then it would pop right back on the next poll. The dismiss PATCH route and the alerts GET route each got their own instance of operator-data.ts because Next.js bundles route handlers independently. So the dismiss mutated one copy of the in-memory map while the poll read from a separate copy where the alert was never dismissed. Fixed by attaching the data store to globalThis behind a singleton accessor — the same pattern Next.js docs recommend for Prisma clients in dev mode.
The fleet overview was making 2N+1 parallel requests per poll cycle — one alert query and one inventory query per store, plus the store list. At 6 stores that's 13 requests. At 30 stores it's 61. The useMemo that aggregated query results had unstable dependencies — the query result arrays got new references on every render — so the memo ran every render anyway.
Replaced the entire fan-out with a single /api/operator/fleet-summary endpoint that returns aggregated alert counts, inventory health, and fleet stats per store in one request. The dashboard went from N parallel queries to 1. Chart transforms that were recomputing on every render got wrapped in useMemo with stable dependencies.
When the stores fetch failed, the error state was a dead end — no retry button, no way to recover without reloading the page. Individual store sub-query failures were completely silent; the store card just showed zero alerts. Empty search results didn't suggest clearing filters. The restock button had no per-item feedback — all rows showed "Restocking..." at once and there was no success indicator after completion.
Each of these is the kind of thing that works fine in a demo but would frustrate a real operator. Added retry buttons on error states, per-store error indicators on cards, "clear filters" in empty states, and per-item restock feedback with a brief success checkmark after completion. Also added the analytics expand/collapse animation that was missing.
The Bone skeleton component was copy-pasted into four files. STATUS_CONFIG was defined twice with different shapes. Inline SVG icons were scattered across components. FleetAnalytics was flattening an alert map that the parent already had in flat form. None of these were bugs, but each one makes the next developer slower. Extracted shared components, unified configs, pushed transforms to where the data naturally lives.
The original test suite covered utility functions well but had gaps at the integration level. No test for the fleet overview rendering with real data and verifying sort order. No tests for error or empty states in tab components. No test for the RefreshBar reading from the query cache. The restock rollback test only asserted the final state — a mutant that removed the optimistic update entirely would still pass because the stock level never changed from its original value.
Backfilled all four gaps. The rollback test was the interesting one — it now verifies the optimistic update fires first (stock jumps to capacity) and then verifies it reverts after the 500 response. That's the difference between "the final state is correct" and "the rollback mechanism actually works."
The dashboard works well as a demo, but there are real things that would matter if this were serving actual operators managing actual stores.
Polling at 15-second intervals means a critical alert could sit for up to 14 seconds before the operator sees it. For a real deployment, a WebSocket connection or Server-Sent Events stream would push alerts the moment they fire. The current polling architecture is a pragmatic starting point — it works with any HTTP backend and doesn't require connection management — but the latency ceiling matters when a fridge temperature is climbing fast.
An operator managing 40 stores is not sitting on the dashboard all day. Critical alerts need to reach them on their phone. A notification layer (push notifications, Slack/Teams integration, SMS for urgent failures) would close the loop between "something went wrong" and "someone knows about it." Right now the dashboard only works if the operator is looking at it.
The inventory sparklines show 7 days of simulated history, but real historical data could power anomaly detection — flagging a fridge that's selling 3x faster than usual (likely needs an early restock) or a store whose sensor readings are drifting (might need calibration). The alert trend chart is a start, but with real data you could build baselines and surface deviations automatically.
Currently there's no auth on the operator routes. A production version would need operator accounts, role-based permissions (fleet manager vs. field technician vs. read-only viewer), and multi-tenant isolation so each operator only sees their own stores. The Auth0 integration from the rest of the app could extend here with custom claims for operator roles.
The person restocking a fridge is on their phone, not a laptop. A dedicated mobile view optimized for the field workflow — scan barcode, confirm restock, acknowledge alert, move to next store — would be a different UI from the desktop fleet overview. The current responsive layout adapts to mobile but it's still a desktop-first design. A truly mobile-first version for field techs would prioritize single-store actions over fleet comparisons.
When stores have physical locations, a map overlay with color-coded pins (green for healthy, red for critical) would give operators spatial context. A cluster of degraded stores in one building might indicate a shared infrastructure issue (power outage, network switch down) rather than individual sensor failures.
The in-memory data store means every server restart seeds fresh data. This is fine for a demo but means you can't test long-running scenarios or cross-session state. The tradeoff was intentional — wiring up a real database for demo data would have added deployment complexity without adding much to the frontend story. One gotcha that came up: static lastPing timestamps generated at module load time drifted past the freshness thresholds as the server ran, making every store show "Offline." The fix was to recompute timestamps relative to now on every read, so the demo data stays realistic regardless of server uptime.
Two tradeoffs from the initial build have since been resolved. The per-store fan-out pattern (N parallel queries for alerts and inventory) was replaced by a single /api/operator/fleet-summary endpoint that returns aggregated data in one request. The chart transforms that recomputed on every render are now memoized with stable dependencies. Both were acceptable at demo scale but would have been real problems at fleet size, so fixing them early was the right call.