Scale Sense — Dashboard UX¶
Tech stack¶
- Framework: React (Vite)
- State: Zustand (server state + WebSocket state)
- Data fetching: React Query (REST API calls, caching)
- Charts: Recharts or Chart.js
- Routing: React Router v6
- Styling: Tailwind CSS
- Types: shared from
packages/api-client
Screen hierarchy¶
/ → redirect to last viewed location
/locations → location picker (multi-location managers)
/locations/:loc_id → tap grid (main dashboard)
/locations/:loc_id/taps/:tap_id → tap detail + history chart
/locations/:loc_id/taps/:tap_id/assign → beer/keg assignment
/locations/:loc_id/alerts → alert history + ack
/locations/:loc_id/devices → device health table
/locations/:loc_id/reports → usage reports (4 tabs)
/locations/:loc_id/settings → all settings panels
State management¶
// DashboardStore — Zustand
type DashboardStore = {
location: Location | null
taps: Record<string, Tap> // keyed by tap_id
alerts: Record<string, Alert> // keyed by alert_id
ws: WebSocket | null
ws_status: "connecting" | "connected" | "reconnecting" | "error"
applyWSEvent: (event: WSEvent) => void
acknowledgeAlert: (alert_id: string) => void
}
React Query caches REST responses with stale-while-revalidate. Zustand handles WebSocket events (mutates tap/alert state in place). Never merge BLE state into dashboard state — phone app only.
WebSocket lifecycle¶
Connect on dashboard mount:
1. new WebSocket(wss://ws.scalesense.com)
2. On open: send { action: "auth", token: cognitoJwt }
3. On message: route to store.applyWSEvent(event)
4. On ping: send { action: "pong" }
5. On close: refreshAllTapStates() then exponential backoff reconnect
Reconnect backoff: start 1s, double each failure, max 30s. On reconnect: fetch fresh state from REST before re-listening — fills gaps during disconnect.
Tap card states¶
| State | Border | Progress bar | Actions |
|---|---|---|---|
| Normal | Default | Green filled | Maintain, History |
| Warning (< 20%) | 1.5px amber | Amber filled | Maintain, History |
| Critical (< 10%) | 1.5px red | Red filled | Swap keg |
| Maintenance | 1.5px blue | Gray (paused) | Exit, countdown |
| Offline | Default, dimmed | Gray (last known) | Diagnose |
| Tare required | 0.5px dashed | Empty striped | Tare now |
Tap grid (main screen) layout¶
- 3 columns on desktop, 2 on tablet, 1 on mobile
- Alert banner at top if any critical alerts (dismissible per session, not ack)
- Summary bar: total taps / online taps / active alerts (avg level removed — not actionable)
- Filter tabs: All / Alerts only / Low kegs
- WebSocket live dot in header (green = connected, gray = reconnecting)
- Export bar: "Inventory snapshot: [PDF] [CSV]" — both download in-place (window.location.href, Content-Disposition: attachment on S3 object)
- PDF: generated by pdf-lib (StandardFonts, no external files) — landscape letter, navy header, alternating rows
- CSV: flat table, same data
Tap detail screen¶
- Large % display (48px)
- Progress bar
- Info rows: beer, keg, pour rate, tapped date, battery, signal, last read, scale ID, firmware
- History chart with range buttons: 1h / 24h / 7d / 30d
- Bin size auto-selected: 5m / 15m / 1h / 6h
- Two datasets: Keg Level (blue solid, filled) + Battery (amber dashed, no fill)
- Both share 0–100% Y axis; inline legend in chart header
- Chart scoped to current keg_id — clean break on beer swap
- Commands panel: Report now / Tare / Calibrate / Update firmware
- Change beer button → assignment flow
Beer/keg assignment flow¶
- Search beer library (real-time filter, fallback to API search)
- Select keg type → auto-fills full/empty weight refs from beer profile
- Summary panel shows SG-adjusted gallons (not just nominal)
- Confirm → optimistic UI update → API calls in parallel:
- POST /kegs (create keg record)
- PATCH /taps/{id} (assign beer + keg)
- WebSocket event
tap_updatedpropagates to all connected sessions - Redirect to maintenance mode tare flow immediately after confirm
- "Do this later" allowed — tap shows dashed border + "Tare now" CTA
Reports screen (4 tabs)¶
Pour history¶
- Stacked bar chart: gallons per tap per day/hour
- Summary metrics: total poured, busiest tap, peak hour, avg pour rate
- Range: 24h / 7d / 30d
Keg lifespan¶
- Horizontal bar chart: avg lifespan per beer
- Color: red < 3d, blue 3-6d, green > 6d
- Filter by keg type (half/quarter/sixth/slim)
- History log table: beer, tap, tapped date, volume poured, lifespan, yield %
- Insights panel: auto-generated from Lambda nightly (stored in DynamoDB)
- Yield = total poured ÷ nominal volume (SG-corrected)
Usage patterns¶
- Day-of-week volume bar chart
- Style breakdown doughnut chart
- Top beers table with vs-prev-30d trend
Device trends¶
- Battery trend line chart (multi-series, one line per scale)
- Offline events table: scale, went offline, came back, duration, likely cause
Beer library screen¶
- Table: Name / Brewery / Style / ABV / SG / Actions
- Click any row to expand inline keg weight panel
- Shows per keg-type: Full lbs / Empty lbs + source badge
- Badge states:
default(gray, static reference),set(blue, real tare measured),calculated(green, running average from history — future #20) - Keg weight records seeded with half_barrel defaults for all beers on first load
- Edit/Delete actions on custom beers only
Device health screen¶
- Summary: total / online / offline / low battery / outdated firmware
- Filterable table: All / Issues / Offline / Low battery
- Battery shown as inline progress bar (green / amber / red)
- Signal shown as bar indicator (1-4 bars) + dBm value
- Firmware badge: green "latest" or amber "outdated"
- Actions: Update (per device) / Diagnose (offline) / Detail
- "Update all outdated" header button
- Calibration table: scale factor, last calibrated, method, drift % (color coded)
- Green < 1%, amber 1-3%, red > 3%
Settings screen panels¶
Left sidebar nav: - Location info (name, address, timezone, operating hours, quiet hours) - Alert thresholds (per-level % and hours, grace period, device thresholds) - Notifications (push toggles, email recipients, auto-export schedule) - Users & roles (team list, invite, permission matrix) - Firmware (channel: stable/beta/manual, auto-update window, staged rollout, auto-rollback) - Reporting (memory/magnetic store retention, publish interval, pour rate window) - Billing (plan, usage metrics) - Danger zone (reset taps, clear history, delete location)
Key UX decisions¶
Optimistic updates¶
Beer assignment updates tap card immediately in Zustand before API confirms. Rollback on API error.
Stale reading indicator¶
If updated_at > 5 min ago: show timestamp in amber, suppress stale during maintenance.
Maintenance countdown¶
Dashboard shows countdown in tap card ("in maintenance · 2:34 left").
Synced to WebSocket maintenance_entered event timestamp — dashboard calculates countdown locally.
Alert banner vs notification¶
Banner: shows most critical active alert at top of dashboard, dismissible per session. Dismissing banner does NOT acknowledge the alert in the system. Acknowledge = explicit action in alerts screen or tap card button.
Export¶
PDF uses ReportLab on Lambda (SG-corrected gallons, alert indicators, generated timestamp). CSV is flat table, same data. Both returned as presigned S3 URLs — browser opens URL, download is handled by S3. Auto-export: EventBridge → Lambda → SES (PDF attached to email).
Dashboard-only vs phone-only features¶
Dashboard only: reports, billing, detailed settings, firmware management Phone only: BLE maintenance, live weight stream, identify flash Both: tap grid, tap detail, alerts, basic commands (report now, swap keg)