Skip to content

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

  1. Search beer library (real-time filter, fallback to API search)
  2. Select keg type → auto-fills full/empty weight refs from beer profile
  3. Summary panel shows SG-adjusted gallons (not just nominal)
  4. Confirm → optimistic UI update → API calls in parallel:
  5. POST /kegs (create keg record)
  6. PATCH /taps/{id} (assign beer + keg)
  7. WebSocket event tap_updated propagates to all connected sessions
  8. Redirect to maintenance mode tare flow immediately after confirm
  9. "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
  • 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)