Scoped session browsing + installation-location surfacing — design
Date: 2026-07-16
Status: Approved (design); pending implementation plan.
Scope: Both frontends — admin dashboard (frontend/, admin.api.xylolabs.com) and operator app (frontend-app/, app.xylolabs.com) — plus one small backend DTO/query change.
Problem
Three related gaps in how operators and admins browse audio/metadata sessions and locate devices:
- No scoped session browsing. There is no way to select one device and step through its sessions, nor to select one installation (facility) and step through the mixed sessions of its various devices. In the admin app you can filter a list, but "opening" a session is a full-page jump with no next/prev and no per-device stepping. In the operator app the session list rows are not even clickable — there is no session detail page at all.
- Awkward width/layout when a session is open.
frontend/src/pages/MetadataSessionDetailPage.tsxhas nomax-w/centering at its root, so on wide monitors the charts bleed edge-to-edge; and its charts grid is declaredgrid-cols-1 md:grid-cols-2 xl:grid-cols-3while almost every card overrides itself tocol-span-full, leaving lone 1/3-width "compact" cards sitting in dead space. - Inconsistency across similar UIs. Admin uses a table + rich filters + a full-page detail; the operator app uses a flat, filter-less, non-clickable list with no detail. The device lists diverge too. Nothing shares a master-detail pattern.
- Installation location is buried. In the platform repo's canonical
firmware/board1-v1/docs/FLEET.md, the headline per-device column isLocation(e.g. "office bench (사무실, mac3)", "harbor site (HJNC)", "UNKNOWN"). In the web UIs, the admin device table shows a free-textlocationcolumn but no facility, and the operator device cards show neither location nor facility (only a page-level facility subtitle). "Where a device is installed" is the single most important field and it is not prominent.
Domain grounding
- "설치" (installation) = facility/site. In the server model this is
facilities(e.g.자일로랩스 본사/ xylolabs-hq, HJNC). A device belongs to exactly one facility viadevice.facility_id. - "설치 위치" (installation location) = where the unit physically sits. In the server this is the free-text
device.location: Option<String>("Free-text installation location, e.g. 'Building A / Line 3 / Pump 2'; operator-set via the admin device PATCH"). Combined with the facility name, this reproduces the FLEET.mdLocationcolumn. - The server also has a separate
fleetconcept (fleet_stats.rs,FleetBenchmarkPage) that groups devices byhardware_versionfor z-score benchmarking. That is unrelated to this work and is left untouched. - The facility-map subsystem stores per-device x/y placements, but admin writes elements keyed by
dongle_idwhile the operator map readsdevice_id— a real mismatch. Out of scope by decision; noted here only so a future map task can pick it up.
Decisions (locked)
- Interaction model: master-detail split. A scoped session list on the left; clicking a row opens that session's charts in a width-bounded column on the right; keep clicking down the list. Scope is switched by a top bar.
- Coverage: both apps. The operator session-detail pane is net-new.
- Location display: facility name (primary/bold) + free-text
device.location(secondary). No map placement (map subsystem stays out).
Approach
One unified "Sessions" page per app — master-detail, mirrored structure, no shared code package. The two SPAs already relate this way: they independently mirror DevicesPage, AlertsPage, SettingsPage against a shared backend, each with its own API client, Zustand stores, i18n, and auth model. Consistency comes from this shared spec + a mirrored layout, not from a shared TypeScript library (a monorepo lib was rejected as far more costly than a UX refactor warrants). A minimal admin-only variant was also rejected because the locked decisions are master-detail + both apps.
Architecture
Unified Sessions page (both apps)
- Route:
/sessions. - Admin: replaces the
/metadatalist and/metadata/:iddetail. Old routes redirect to/sessions(and/sessions?session=<id>for a deep-linked detail) so existing links/bookmarks survive. - Operator: replaces the flat
SessionsPageat/sessions. - URL state (deep-linkable, back-button-safe):
?facility=<id>&device=<id|all>&status=<all|active|closed>&mode=<all|continuous|sampling>&session=<id>&page=<n>. - Layout shell:
max-w-[1600px] mx-auto, split into a fixed-width master column (w-80 lg:w-96) and aflex-1 min-w-0detail column.
Scope bar (top)
- Facility selector: shown to multi-facility / super-admin users only (admin). The operator app is single-facility, so the facility is the implicit page context and no selector is shown. Backed by the existing facility-filter store in admin; by
useFacilityId()in operator. - Device selector: an "All devices" option plus one entry per device. Option label =
alias/dongle_id, with the device's location as a subtitle. "All devices" ⇒ facility-mixed stream; a specific device ⇒ device-scoped stream. - Status (all/active/closed) and Mode (all/continuous/sampling) filters; sort
started_at desc. - All of the above map onto the existing endpoint
GET /v1/metadata/sessions?facility_id=&device_id=&status=&mode=&page=&per_page=. No new list endpoint. Operator gets the full filter set for parity (it has none today).
Master (left) — scoped session list
- Paginated (or incrementally loaded) list scoped by the current facility/device/status/mode.
- Row: start time (primary) +
device · streams · sample-count(secondary). In facility-mixed mode ("All devices"), each row carries a per-device color chip so mixed devices read at a glance. - Selecting a row sets
?session=<id>and loads the detail pane; the selected row is highlighted.
Detail (right) — session charts
flex-1 min-w-0, width-bounded by the shell — this is the width fix.- Content: session header + device-info + session-info panels + the stream charts.
- Charts layout replacement (fixes the awkward grid): because the detail pane is already a bounded column, charts render as a clean single-column vertical stack (
flex flex-col gap-4). Compact single-sample cards may be grouped into agrid grid-cols-2 sm:grid-cols-3sub-row. The oldgrid-cols-1 md:grid-cols-2 xl:grid-cols-3+ per-cardcol-span-fullpattern (and its lonely 1/3-width dead space) is removed. - Admin reuses its existing session-detail content (stream panels, audio, search-by-sound, time-range presets/custom) as the pane body.
- Operator detail pane is net-new — it renders a historical session's waveform + sensor charts via the existing
GET /v1/metadata/sessions/{id}/streams/{streamId}/data(and.../audio) endpoints, porting/reusing the operator app's chart components. This is the largest single piece of work.
Responsive collapse
- ≥
lg: list + detail side by side. < lg: list-only; tapping a row routes to?session=<id>rendered as a full-screen detail with a "Back to list" control. Preserves the operator app's existingmin-w-0horizontal-pan fix inAppLayout.
Entry points ("select a device / an installation and open its sessions")
- Device list row/card → "Open sessions" →
/sessions?device=<id>(device-scoped). Both apps. - Facility (admin) / the app's facility → "Open sessions" →
/sessions?facility=<id>withdevice=all(facility-mixed). Admin facility surfaces + the operator app's facility context. Sessionsnav item →/sessionsunscoped (facility-mixed default for the active facility).
Installation location in the device list
- Admin
DevicesPagetable: theLocationcell becomes two lines — facility name (bold/primary) +device.location(muted secondary), placed early in the column order for prominence. Facility name is already available client-side:DevicesPage.tsx:621builds afacilityNameByIdmap (D-IA-1) from an unconditionally-loaded facilities list, presently used only in a "quiet per-row info cluster" — this change promotes that name into the prominent Location cell → no backend change on admin. Unknown location renders an explicit "— unknown location —" placeholder. - Operator
DeviceTilecards +DeviceDetailPage: add the free-textlocationprominently. Facility name is the page context and is not repeated per card. - Backend (the only backend change): add
location: Option<String>to the operatorFacilityOverviewDeviceDTO and the/v1/facility/overviewquery, and to the operatorDeviceDetailshape. Session endpoints already carry every filter needed.
Consistency deliverable
Both apps ship the identical master-detail shell, scope-bar semantics, responsive-collapse behavior, and detail width treatment. The pattern is documented briefly (in the app READMEs or a short docs/ note) so future pages conform.
Data flow
- Scope bar reads/writes URL params → React Query keys derived from
{facility, device, status, mode, page}. - Master list:
GET /v1/metadata/sessions?...(existing). - Selecting a row sets
?session=<id>. - Detail pane:
GET /v1/metadata/sessions/{id}+GET /v1/devices/{id}+ per-streamGET /v1/metadata/sessions/{id}/streams/{streamId}/data(and.../audiofor audio streams) — all existing. - Device list location: admin joins
facility_id → namefrom its already-loaded facilities list; operator reads the newlocationfield from/v1/facility/overview.
Error handling
- No
?session→ detail pane shows a "Select a session" empty state. - 404 session (deleted / not in scope) → inline error in the detail pane; the list persists.
- Facility/device with no sessions → empty state in the master column.
- RBAC facility scoping is unchanged (super-admin sees all; facility-admin is scoped by
resolve_facility_filter). - Selecting a device then switching facility clears an out-of-facility
?device/?session.
Testing
- Frontend (per CLAUDE.md): Playwright viewport tests at mobile 375 / tablet 768 / desktop 1280 for both apps — verify master-detail renders side-by-side ≥lg, collapses to list+back below lg, the body never scrolls horizontally, the detail column is width-bounded, and there are zero
pageerrorevents on all three viewports. Runnpx tsc -b --noEmitandnpx vite buildin bothfrontend/andfrontend-app/. - Backend:
cargo check+cargo clippy. Extend the metadata-sessions API tests to coverdevice_id×facility_idfilter combinations. Add an assertion thatFacilityOverviewDevice(and the operator device detail) includelocation. - Post-deploy (per CLAUDE.md): verify new bundle hashes on admin + app, confirm the three subdomains respond and the container is
Up (healthy).
Build sequence
- Backend: add
locationto the operatorFacilityOverviewDeviceDTO +/v1/facility/overviewquery + operatorDeviceDetail. - Admin: unified Sessions master-detail page + scope bar; width/chart-grid fix; device + facility "Open sessions" entry points; two-line
Locationcell in the device table;/metadata→/sessionsredirects. - Operator: mirror the Sessions master-detail — largest piece (net-new detail pane rendering historical session charts) + scope bar + location on device cards/detail.
- Consistency doc + the tests above.
Optional split: steps 1–2 + all location changes can ship as plan A (fast value), with the operator detail pane (step 3) as plan B, if landing incrementally is preferred over one feature.
Explicitly out of scope
- The facility-map
dongle_id-vs-device_idelement-keying mismatch (map subsystem untouched). - The
hardware_versionfleet-benchmark feature. - Any change to session ingest, retention, or the metadata query endpoints beyond adding
locationto the two operator DTOs.