The firmware publishes status/heartbeat, system/alerts, system/info and
status/playback with retain=true. On every backend (re)connect - every
restart and every uvicorn --reload - the broker replays the last message on
each of those topics for every device that ever connected. We handled those
replays as if they had just happened:
- heartbeats: a row with received_at=now() for every device, so devices
that have been dead for months showed ONLINE for 90s after each restart
and got pinged. ~770k such rows exist locally.
- boot_report: the last boot logged again as a new reboot (the phantom
PANIC entries on the Health tab).
- alerts / other info events: logged again as new occurrences.
MQTT delivers retain=1 only for replays caused by a new subscription; live
publishes always arrive with retain=0. The flag is now passed through to the
handlers and the WS broadcast:
- heartbeat: replays are not stored. A live heartbeat is.
- {"state":"offline"} heartbeat (LWT / graceful disconnect) is no longer
stored as a sign of life. It marks the device offline immediately in
a small in-memory set (mqtt/presence.py) used by /mqtt/status and the ping
loop; a later live heartbeat clears it. Replayed offline markers also mark
offline, since a retained message is the device's last word.
- boot_report: live -> always a new boot. Replay -> stored only if it
differs from the device's latest boot row (i.e. we missed it while down).
- alerts: replay still syncs the current-alert row; history gets a row on a
live alert (even an identical repeat - faults recur) or on a replay that
changes state. Replaces the 98dd16b rule that dropped identical live alerts.
- other info events: replays are not logged.
- Frontend (DeviceList, DeviceDetail, LogsTab) ignores retained WS messages
for live updates, and flips a device offline on the offline marker instead
of marking it online.
Verified locally after a backend restart: only the 7 actually-live devices
got new heartbeat rows (none from the replays), and no boot/alert/info rows
were created.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both create paths (Users > Add User page and the CreateUserModal used during
device onboarding) now have a Confirm Password field. A mismatch shows an
inline error and blocks the create call, so a typo can't silently become the
user's Firebase Auth password. The confirm value is client-side only and is
never sent to the backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The amber diagonal slash on offline devices was too loud on the Fleet list.
Offline now renders all arcs unlit in the neutral dim colour. The amber "?"
for online-but-no-rssi (legacy v1 firmware) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replaces the generic "Playback command failed" with the firmware's specific
messages, documents "Already stopped" as SUCCESS, url no longer sticky,
and the stricter field validation.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pid, speed, duration, started_at, source, ts on status/playback (and the
same fields on the WebSocket playback INFO event), plus how to handle a
stale retained message and when the epoch fields are 0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Follow-up on 529e866 after review:
- SignalIndicator: offline now renders a diagonal slash (standard
"no signal" convention) instead of an X. Added a distinct amber "?"
state for devices that are online but whose firmware never reports rssi
at all (legacy v1 heartbeats predate the rssi/state/ok fields — see API
Reference's v2 migration notes) — previously this looked identical to
"not loaded yet". DeviceListCardView's online block now opts into this
by passing isOnline explicitly.
- Copy-to-clipboard for the serial number was invisible: the icon button
had opacity:0 as an inline style, which beats a CSS hover rule at equal
specificity, so the reveal-on-hover code paths never fired. Replaced
with a shared CopyableSerial component (components/shared/) where the
whole serial text is clickable, not just a trailing icon, and the icon
sits at partial opacity at rest instead of fully hidden. Wired into both
DeviceDetail's header and OverviewTab's hero Serial Number field.
- GeneralTab firmware hero: all stat columns are now equal-width via a
grid instead of ad-hoc flex gaps, and backup_version's "Unknown" sentinel
(the device's own placeholder when no second OTA slot has ever been
flashed) is no longer rendered as the literal string "vUnknown".
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
SignalIndicator now takes an isOnline prop — when a device is offline it
renders a dimmed amber glyph with an X instead of showing the last-known
(possibly full-bars) RSSI reading, which was misleading on the Device List
table view. Card view already special-cased offline devices and is
unaffected.
Also, while working the device details surface:
- DeviceDetail: hover-to-reveal copy button next to the serial number
subtitle in the page header.
- OverviewTab hero: swapped the redundant "Location" field (already shown
in detail on the General tab's map) for live Firmware Version.
- GeneralTab: new full-width, compact Firmware hero row (version, channel,
validation state, boot count, backup slot) sourced from firmware.status.
- ControlTab: max width brought in line with every other tab (2000, was
1400), and added a 4th "Firmware" section with channel selection,
Update Now, custom-URL flash, and commit/rollback controls, wired to the
firmware.status / ota.* / firmware.commit / firmware.rollback commands
documented in the API Reference.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
req_id (added 2026-09-21 in the v2 topic rebuild, F-062):
- New Extras tab documents it fully: envelope-level (sibling of cmd,
not inside contents), always optional, exact echo/reply behavior,
parse-failure edge case, and an MQTT-only gap — WebSocket and HTTP
transports don't actually read or echo it despite CommandBus
supporting it generically at the bus level.
- Every command's Contents section now carries a persistent note
pointing to Extras, instead of duplicating the explanation 40+ times
or only mentioning it in one Transports card.
- Removed the old "req_id Correlation" card from the Transports tab —
superseded by the Extras tab.
Transports tab: dropped the V2/Legacy sub-tab switcher. It only ever
showed a "not documented yet" placeholder for Legacy, and the legacy
topic set belongs with the rest of the migration reference, not
alongside the current transport list.
Legacy Migration tab (renamed from "v1 → v2 Migration"): added a
"Legacy Topic Migration" table ahead of the command migration table,
mapping every MQTT topic from the old pre-rewrite firmware
("Controller - Production FW") to its v2 equivalent — including three
v2 topics (system/alerts, system/info, system/metrics) that have no
legacy predecessor at all. Reconstructed from that firmware's source;
it has been fully replaced, so this is historical reference only.
playback.play: filled in the full contents field set per the current
Player.cpp implementation — segment_duration, pause_duration,
total_duration, and continuous_loop are the legacy (no "mode") path,
while duration is the v2 path read only when mode is present. Added a
warning callout since sending mode alongside the legacy duration
fields doesn't merge behavior — only one path is read, based solely on
whether mode is present. This is intentional: mode is how a v2 caller
opts in, and its absence is how a v1 caller's request still works.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
relay.set_config is a partial update on the firmware side — any bell
index not present in the durations/outputs maps is left completely
untouched. EditBellOutputsModal and OnboardDevice's bell config step
both only sent entries for the currently-active bell count, so
deselecting a bell (lowering the active count) never actually
deactivated its output on the device — it stayed wired to whatever it
was last set to. The only way to disable a bell was to reselect it and
explicitly set it to "Disabled", which isn't obvious.
Both now send all maxOutputs slots on every save, forcing output to 0
for anything beyond the active count, so lowering the active bell
count reaches the firmware the way it visually appears to in the UI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
WebSerial port ownership was previously held inside StepFlash, which
made it awkward for other steps (StepVerify) to read from the same
port without fighting over exclusive access. useSerialConnection
centralizes port open/close/read/write so the wizard's steps share one
connection lifecycle instead of each managing their own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New POST /devices/{id}/reset-stats + ResetStatsModal lets staff clear
QA/bench test data accumulated before a device ships to a customer:
Firestore bell/playback counters and Postgres history tables (logs,
heartbeats, commands, boot events, alert events, ping samples,
diagnostics reports, control/reports, and opt-in live alerts). The two
firmware-side resets (telemetry.reset_boot_data, logs.clear) go over
the existing MQTT command/ack flow instead, since they need the device
online and duplicating that round-trip server-side would just be a
second, inferior implementation.
Also includes two incidental cleanups in devices/router.py: audit-log
entries for create/update now use console_name in their label (missed
by the earlier console_name commit), and add/remove-device-user rename
their local Firestore client from `db` to `fs` to stop shadowing the
`db: AsyncSession` dependency param in the same function scope.
AddDeviceUserModal's results list also gets a max-height + scroll so a
long match list doesn't grow the modal off-screen.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New guided flow (Bell Cloud > Devices > Onboard) for claiming a
manufactured/flashed device into a customer's fleet in one pass:
looks up the device by serial, finds or creates the customer
(CreateCustomerModal for a quick inline create), finds or creates the
app user account (reuses CreateUserModal from the user-creation work),
and assigns the device — replacing what used to require jumping
between the inventory, CRM, and user-management pages separately.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The inventory list and detail page previously only showed raw
customer_id / user_list references. list_devices now batch-resolves
customer names and user display names/emails (via Firestore get_all(),
avoiding an N+1 round trip per device) and exposes them as
customer_name / users on DeviceInventoryItem. Search now matches
against device name, console name, customer name, and assigned users
in addition to serial/owner/batch.
Also adds hw_types (multi-select board type) and has_users filters to
GET /manufacturing/devices, and carries console_name through for
display. DeviceInventoryDetail is updated to show and use all of this
(customer name, assigned users, new UI components for signal/charts).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- devices: breadcrumb now falls back through console_name -> device_name
-> device_id, matching every other device display label in the console
- melodies/archetypes: breadcrumb was reading a bare d.name field that
doesn't exist on the melody schema (name lives at
d.information.name.<locale>), so these breadcrumbs always showed blank.
Now uses getLocalizedValue like the rest of the melodies UI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sysadmin/admin-only settings page for configuring how long device log
history is kept before pruning. GET/PUT /api/settings/log-retention,
new LogRetentionSettings page, nav entry, and route.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Surfaces the new device-health telemetry (boot events, ping RTT,
diagnostics reports, alert events) across the console:
- HealthTab: boot/diagnostics timeline, CPU temp and RSSI charts
(LineChart), current alert status, and a Settings sub-tab for
DeviceHealthSettings thresholds
- LogsTab: dedicated log explorer embedded in the Health tab, with
level/source filtering and fmtLogTimestamp for dense timestamp rows
- OverviewTab: a "Latest Device Issue" card showing the most recent
alert event, colour-coded by severity and fading with age, plus a
modal to inspect the surrounding log lines
- DeviceList/DeviceListCardView/DeviceListMapView: fleet list gains a
SignalIndicator-based RSSI display in place of the plain online dot
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New users can now be created with a password, which creates a real
Firebase Auth account (so they can log into the mobile app immediately)
alongside the Firestore profile document. UserCreate is now
UserProfile + password (request-only, never persisted or echoed back);
deleting a user also removes their Auth account.
- backend/users: split UserCreate into UserProfile (persisted shape)
and UserCreate (adds password), wire firebase_auth create/delete
- CreateUserModal: new lightweight modal for creating a user from
other flows (e.g. device onboarding) without leaving the page
- UserForm: adds the password field for new users; also fixes
useToast() being used undestructured (toast.success(...) was being
called on the hook's return value instead of its .toast method)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets staff set a friendly name for a device independent of whatever
name the end user gave it in the app. console_name is never shown to
app users and never synced from/to device_name; every display label
across search, equipment/helpdesk name resolution, device search, and
the Manage tab's issue linker now falls back through console_name ->
device_name -> serial rather than device_name alone.
Also includes an incidental one-line fix in devices/service.py: the
nested-struct deep-merge in update_device() was missing the newly
added device_health_settings key.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adopts useDeviceCommand across DeviceDetail and its tabs so config
changes (log levels, clock settings, bell outputs, alert thresholds,
backlight, attributes) are sent to the device immediately via
control commands and only persisted to Firestore once the device
acks success, instead of writing Firestore first and hoping the
device eventually picks it up.
- GeneralTab: log-level sliders now call log.set_serial/sd/mqtt
directly and revert on failure; a background log.get_config +
network.info pull reconciles Firestore against the device's actual
state once per mount
- ClockTab, BellsTab, ControlTab, and the Edit* modals: same
live-command-then-persist pattern
- EditLoggingModal is removed — its job (log level editing) moved
inline into GeneralTab's sliders, so a modal round-trip is no
longer needed
- DeviceDetail wires the shared useDeviceCommand connection through
to each tab and adds a Health tab entry
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pulls the send-command-and-await-ack machinery out of DeviceDetail.jsx
into a reusable hook, so other pages (the onboarding wizard, etc.) can
send a device a command and await its control/ack reply the same way,
with a live-updating toast for non-silent commands.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scrollbar theming was scoped to .app, so anything rendered outside it via
createPortal(..., document.body) — Modal, Select/MultiSelect's floating
menu, DataTable's column-visibility picker — fell back to the browser's
default light-on-dark scrollbar. Extends the same scrollbar-color rules
to .modal, .select-menu, and .dt-col-picker.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.header used a lighter 0.30 background tint than .sidebar's 0.40, so
scrolled content underneath showed through almost undimmed even though
backdrop-filter was applied — the two fixed chrome surfaces no longer
read as one consistent glass layer. Aligns the tint and adds a shadow
to give the header separation from content.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Six new design-system components, all documented with live examples in
the StyleGuide as required by CLAUDE.md before any page can use them:
- MultiSelect: checkbox dropdown for filters/tags, built on Select's
trigger/menu styling
- LineChart: telemetry/time-series charting for the upcoming Health tab
- SignalIndicator: signal-strength glyph for device RSSI display
- PressHoldButton: press-and-hold confirmation for destructive actions
- EditableText: inline click-to-edit text with a hover/focus-revealed
pencil affordance
- TimeRangeSelect: preset + custom time-range picker, backed by the new
lib/timeRange.js helper
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Toast gains a pending/update lifecycle so async actions can show a single
toast that transitions from in-progress to success/error, instead of
firing a new one. Select's floating-menu placement logic is extracted
into a shared helper so other dropdown-style components (MultiSelect)
can reuse it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was previously a dead stub (TODO: implement). This real implementation
is the foundation for live heartbeats, ping RTT, and the device command
ack flow that later commits build on.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Firmware rebuilt its MQTT topics to v2 (control/command+ack+reports,
status/playback, system/alerts+info+logs+metrics, LWT, req_id, per-topic
switches — see project-vesper's docs/reference/vesper_mqtt_topic_spec_v2.md
and feature-catalog.md F-062). Mirrors that in the console's API Reference:
- Transports tab now splits into V2 (fully documented: topic table with
per-topic switches, req_id correlation, heartbeat/playback/reports/
alerts/boot-report/metrics payload cards) and Legacy (placeholder,
to be filled in later)
- mqtt namespace gains mqtt.get_topics / mqtt.set_topics command docs
- mqtt.disable description updated for its new graceful-offline behavior
- Message envelope docs (top of page) note the new optional req_id field
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Firmware side added an MQTT-only sysadmin command to override a
device's factory-set identity (serial/hw_family/hw_revision) — see
project-vesper commit 64a8bc2. Per docs/README.md's console-sync
rule, mirrors the same command into the console's live API Reference
page.
Note: this repo had substantial other pending uncommitted work
(diagnostics reports, boot history, log retention, etc.) in this
same file and elsewhere at the time of this commit — left untouched
and still uncommitted, only the system.set_identity hunk is included
here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Mirrors firmware changes: new SD log retrieval commands on LoggingHandler
(logs.list, logs.download — chunked/paginated for offline-device
troubleshooting), and firmware.status now returns OTA rollback safety-net
diagnostics (retry/failure counts, backup partition info).
Adds a dedicated Heartbeat Payload card to the Transports tab showing
the full JSON schema with per-field descriptions, including the new rssi field.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
network.info: updated description and response example to reflect new fields
(subnet, mac, hostname, ssid, connection_type). Notes it as the command to use
for console panel population.
network.status: updated to show slimmed response (connected, ap_mode, rssi,
uptime_ms, state). Notes it as the command for monitoring/polling, not display.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- clock.get_config response example now includes gmt_offset_sec, dst_offset_sec, ntp_server
- Description updated to note it supersedes clock.get_timezone
- clock.set_backlight field names corrected to match wire format
(backlight, backlight_output, backlight_on, backlight_off)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Document three new batch commands with full response/errors/example fields:
- relay.set_config: combined bell durations + outputs
- clock.set_config: clock hardware settings (enabled, c1, c2, timings)
- clock.set_alerts_config: alert type, bell assignments, silence windows
Also updated output field notes throughout clock and relay commands to reflect
the enforced 1-based output numbering convention (0 = disabled, 255 = unconfigured).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Manufacturing router now uses shared/audit.log_action (Postgres) instead
of the separate manufacturing/audit.py (SQLite mfg_audit_log), so all
manufacturing events appear in the Log Viewer
- Added log_action calls to 5 previously unlogged endpoints: lifecycle
patch, lifecycle create, lifecycle delete, flash asset upload, flash
asset note
- Removed the now-redundant /manufacturing/audit-log endpoint
- Log Viewer restricted to sysadmin only: backend uses require_sysadmin
(was require_admin_or_above), frontend adds role guard on the page
- Fixed Action badge column clipping: table-layout auto + whiteSpace nowrap
so the column sizes to fit the widest badge (Status Change)
- Added device_batch entity type to Log Viewer entity labels and filters
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Orders:
- Auto-set customer status to ACTIVE when creating a new order (both "+ New Order" and "Init Negotiations")
- Update Status panel now resets datetime to current time each time it opens
- Empty note on status update saves as empty string instead of falling back to previous note
- Default note pre-filled per status type when Update Status panel opens or status changes
- Timeline items now show verbose date/time ("25 March 2026, 4:49 pm") with muted updated-by indicator
CustomerDetail:
- Reordered tabs: Overview | Communication | Quotations | Orders | Finance | Files & Media | Devices | Support
- Renamed "Financials" tab to "Finance"
CustomerList:
- Location column shows city only, falls back to country if city is empty
OverviewTab:
- Hero status container redesigned: icon + status name + verbose description + shimmer border
- Issues, Support, Orders shown as matching hero cards on the same row (status flex-grows to fill space)
- All four cards share identical height, padding, and animated shimmer border effect
- Stat card borders use muted opacity to stay visually consistent with the status card
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New public page at /serial-monitor: connects to Web Serial (115200 baud),
streams live output, saves sessions to localStorage + downloads .txt
- New protected page at /settings/serial-logs (admin/sysadmin only):
lists saved sessions, expandable with full scrollable log, search,
export and delete per session
- Registered routes in App.jsx and added Log Viewer to Console Settings sidebar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>