First-run UX: fix six defects and surface motion history - #282
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
RedirectToStandalonefired unconditionally, so an operator visitinghttp://their-host/was bounced to sentinel-command.com. The root of their own deployment was unusable. Now skipped whenIS_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.
systemOkwastotal > 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.999999isplans.py's sentinel for unlimited, exactly as999is 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/settingshas always returnedcamera_transition_notificationsandnode_transition_notifications, andPOST /settings/notificationshas 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 — loggedattribute d: Expected numberon every render and dropped the rest of the path, drawing clipped. Only found because the new panel's browser pass surfaced the console error. Sweptsrc/for the same damage — none else.Motion history was recorded but unreadable
GET /api/motion/eventsand/events/statshave 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
.gitignorecovered 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 publishablepk_test_key), but it was onegit add -Aaway.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