Skip to content

First-run UX: fix six defects and surface motion history - #282

Merged
Sbussiso merged 2 commits into
masterfrom
first-run-ux-fixes
Sep 10, 2026
Merged

First-run UX: fix six defects and surface motion history#282
Sbussiso merged 2 commits into
masterfrom
first-run-ux-fixes

Conversation

@Sbussiso

Copy link
Copy Markdown
Contributor

Ran Command Center in AUTH_PROVIDER=local — the exact path a first self-hosted install takes — and drove every page in a real browser. Everything here was found that way, not by reading code.

Six defects, all visible in the first minute

1. A self-hosted install's root redirected to our marketing site. RedirectToStandalone fired unconditionally, so an operator visiting http://their-host/ was bounced to sentinel-command.com. The root of their own deployment was unusable. Now skipped when IS_LOCAL_AUTH.

2. Every new org displayed the wrong timezone. Intl.supportedValuesOf("timeZone") returns 418 IANA zones and does not include a bare "UTC" — which is exactly what the backend defaults a new org to. <select value="UTC"> matched no option, so the browser silently rendered the first one: Africa/Abidjan. The stored value was correct; only the display lied, which is worse than an error.

3. A brand-new install reported "Offline" in amber. systemOk was total > 0, so zero cameras — the state every install starts in — rendered as a warning. Now reads "No cameras yet" in neutral; "Offline" means what it says.

4. Self-hosted showed 0.0 / 999999h. 999999 is plans.py's sentinel for unlimited, exactly as 999 is for cameras/nodes — and those already render as three lines away in the same component. The sidebar contradicted the Settings page, which said "Unlimited" for the same plan.

5. Two of three notification settings had no control. GET /api/settings has always returned camera_transition_notifications and node_transition_notifications, and POST /settings/notifications has always persisted them — only the motion toggle was rendered. An operator flooded with node-transition notifications had no way to stop them.

6. The camera icon was malformed on every CameraCard. Path read h16c0 1.1-.9-2-2V6, which the SVG parser rejects outright — logged attribute d: Expected number on every render and dropped the rest of the path, drawing clipped. Only found because the new panel's browser pass surfaced the console error. Swept src/ for the same damage — none else.

Motion history was recorded but unreadable

GET /api/motion/events and /events/stats have shipped since motion ingestion landed — paginated, camera-filterable, require_view — and nothing in the SPA ever called them. The only motion surface was the live SSE toast, so "what triggered overnight?" was unanswerable.

Adds a Motion tab to Admin, mirroring the existing tabs' shape. Per-camera rollup answers "which camera saw the most movement" before you read a row; scores carry colour because that's the number operators scan for.

Verified against 140 seeded events across two cameras: 3 pages, correct counts and peak scores, camera-name resolution, zero console errors.

Also

.gitignore covered only bare .env, missing .env.local — the file Vite's own docs designate for secrets, and the one AGENTS.md tells self-hosted operators to create. Nothing has leaked (the only tracked env file holds a publishable pk_test_ key), but it was one git add -A away.

Verified

99 frontend tests pass, production bundle builds, all six fixes confirmed in-browser after the change.

Noted, not fixed: CI doesn't lint the frontend (audit + test + build only), which is why 5 eslint errors have been sitting unnoticed — the backend gates on ruff. Wants its own change.

🤖 Generated with Claude Code

Sbussiso and others added 2 commits September 9, 2026 15:05
Ran Command Center in AUTH_PROVIDER=local — the exact path a first
self-hosted install takes — and drove every page in a real browser.
Everything below was visible within the first minute.

1. A self-hosted install's root redirected to our marketing site.
   RedirectToStandalone fired unconditionally, so an operator visiting
   http://their-host/ was bounced to sentinel-command.com and the root
   of their OWN deployment was unusable. Now skipped when IS_LOCAL_AUTH.

2. Every new org displayed the wrong timezone.
   Intl.supportedValuesOf("timeZone") returns 418 IANA zones and does
   NOT include a bare "UTC" — the backend defaults a new org to exactly
   "UTC", so <select value="UTC"> matched no option and the browser
   silently rendered the first one instead: "Africa/Abidjan". The stored
   value was right; only the display lied, which is worse than an error.

3. A brand-new install reported "Offline" in amber.
   systemOk was `total > 0`, so zero cameras — the state EVERY install
   starts in — rendered as a warning. It now reads "No cameras yet" in
   neutral, and "Offline" means what it says: cameras exist, none is up.

4. Self-hosted showed "0.0 / 999999h" of viewer hours.
   999999 is plans.py's sentinel for unlimited, exactly as 999 is for
   cameras and nodes — and those already render as ∞ three lines away in
   the same component. Viewer-hours didn't, so the sidebar contradicted
   the Settings page, which said "Unlimited" for the same plan.

5. Two of three notification settings had no control.
   GET /api/settings has always returned camera_transition_notifications
   and node_transition_notifications, and POST /settings/notifications
   has always persisted them, but only the motion toggle was rendered.
   An operator being flooded with camera- or node-transition bell
   notifications had no way to stop them.

Also: .gitignore only covered bare `.env`, missing `.env.local` — the
file Vite's own docs designate for secrets, and the one AGENTS.md tells
self-hosted operators to create. Nothing has leaked (the only tracked
env file holds a publishable pk_test_ key), but it was one `git add -A`
away. Now `.env.*` with an exemption for `.env*.example`.

All five verified in the browser after the fix; 99 frontend tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Motion was recorded but unreadable. GET /api/motion/events and
/events/stats have shipped since motion ingestion landed — paginated,
camera-filterable, require_view — and NOTHING in the SPA ever called
them. The only motion surface was the live SSE toast in useMotionAlerts,
so an operator could see motion happening right now but had no way to
answer "what triggered overnight?" — which for a security product is the
question the product exists to answer.

Adds a Motion tab to the Admin dashboard alongside Stream Access /
Organization Audit / MCP Activity, deliberately mirroring their shape
(filter row → summary → table → pager) so it reads as part of the same
surface. Per-camera rollup on top answers "which camera saw the most
movement" before you read a single row; scores carry colour because that
is the one number an operator actually scans for.

Verified against 140 seeded events across two cameras: 3 pages, correct
per-camera counts and peak scores, camera-name resolution, and zero
console errors.

Separately, the camera icon on every CameraCard was malformed. The path
read "h16c0 1.1-.9-2-2V6", which the SVG parser rejects outright — it
logged 'attribute d: Expected number' on every render and dropped the
remainder of the path, so the icon drew clipped. Only found because the
new panel's browser pass surfaced the console error. Restored to the
canonical arc (h16c1.1 0 2-.9 2-2), and swept the rest of src/ for the
same class of damage — none found.

Note for later: CI does not lint the frontend (audit + test + build
only), which is why 5 eslint errors have been sitting unnoticed. The
backend gates on ruff. Not fixed here — it wants its own change.

99 frontend tests pass; production bundle builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Sbussiso
Sbussiso merged commit 584bc9c into master Sep 10, 2026
12 checks passed
@Sbussiso
Sbussiso deleted the first-run-ux-fixes branch September 10, 2026 01:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant