Commit graph

125 commits

Author SHA1 Message Date
Mitchell R
02e57a5d54
fix(onvif): import profiles as streams 2026-05-11 00:20:48 +02:00
Mitchell R
3be1a9a624 feat: entities (unified content pool) + ONVIF discovery flow
Entities:
- New entities table — id, name, type (camera|html|web), camera_id,
  html_content, web_url
- Auto-create entity per camera on createCamera
- Layout cells reference entity_id (replaces inline content_type/
  camera_id/html_content/web_url)
- Bundle resolves entities back to legacy cell fields for kiosk compat
  (Rust kiosk unchanged)
- Full CRUD: /admin/entities, /admin/entities/new, /admin/entities/:id
- Cell editor: single entity dropdown with type badges

ONVIF discovery:
- /admin/cameras/discover — host/port/user/pass form
- Server queries ONVIF device, lists profiles with name/resolution/
  encoding/framerate
- "Add" creates camera + main stream from chosen profile
- shared/onvif.ts: minimal SOAP+UsernameToken+PasswordDigest client
  (no external dep)
- Camera new form simplified to RTSP-only with discover link
2026-05-10 23:18:44 +02:00
Mitchell R
f61c3db0e8 feat: Node-RED outbound bridge — forward kiosk events to Node-RED
- shared/nodered-bridge.ts: fire-and-forget POST to Node-RED HTTP-in
- api-http: kiosk event endpoint now forwards to Node-RED at /in/<topic>
- Best-effort, never blocks. 3s timeout, warn on failure.
- sec-config: noderedUrl on api-http (defaults to http://127.0.0.1:1880)

Node-RED flows can attach http-in nodes at /in/<topic> to receive
camera motion, GPIO events, etc. Inbound commands (Node-RED → server)
go through the admin API with admin Bearer token (no new endpoints
needed for v0.1).
2026-05-10 22:49:59 +02:00
Mitchell R
cbb1683c5d feat: deployment artifacts + CEC relay + auth-check endpoint
Deployment (deploy/):
- systemd units for server (system) and kiosk (user session)
- Angie/nginx proxy config — routes admin, api, ws, node-red
- Dockerfile + docker-compose for containerized deployment
- deploy/README.md with install instructions

Auth:
- /api/admin/_check endpoint for proxy auth_request subrequest
- Returns 200 if admin session valid, 401/403 otherwise
- Sets X-BetterFrame-User header for upstream

CEC (Pi5 HDMI control):
- kiosk/src/cec.rs wraps cec-ctl subprocess
- Standby/wake/active-source commands
- WS message types "standby" / "wake" dispatched to CEC
- Admin UI: Wake/Standby buttons on kiosk edit page
- Server sendToKiosk via coordinator
2026-05-10 22:45:56 +02:00
Mitchell R
766bf8dee0 feat: WebKit for web/html cells + display auto-discovery via heartbeat
Rust kiosk:
- web cells now use webkit6 WebView (load_uri)
- html cells use WebView.load_html (full HTML rendering)
- query_displays() reads /sys/class/drm/ for connected HDMI/DP outputs
- Heartbeat reports display geometry every 60s

Server:
- /api/kiosk/heartbeat accepts displays array
- Syncs kiosk-reported displays to display records
- Updates dimensions when changed, creates new displays for new ports
2026-05-10 22:39:53 +02:00
Mitchell R
722ddcfb12 feat: layout builder — resize cells + in-place htmx editing
UI improvements:
- Click cell → htmx swaps in edit form inside the cell (no page reload)
- Cancel re-fetches cell in read mode
- Save returns updated cell HTML, htmx swaps it
- Edit form includes Width/Height inputs for col_span/row_span
- Inline +W/-W/+H/-H buttons on each cell for quick resize
- Add (+) and delete (×) buttons also htmx — only the grid swaps

Routes:
- GET /admin/layouts/:id/cells/:cellId — cell fragment (read mode)
- GET /admin/layouts/:id/cells/:cellId/edit — cell fragment (edit mode)
- POST /admin/layouts/:id/cells/:cellId/resize — adjust span by delta
- All cell ops return fragment if hx-request header present, else 302

All mutations trigger notifyKiosks() — kiosks live-update via WS.
2026-05-10 22:31:37 +02:00
Mitchell R
374a2e091b fix: proper migration version tracking via user_version PRAGMA
- Migrations now run exactly once per DB lifetime, tracked via
  SQLite's user_version PRAGMA
- Re-runs become no-ops after schema reaches target version
- v0.2 also made defensive — skips if template_id already dropped

Fixes "no such column: layouts.template_id" on second startup after
v0.5 rebuild dropped the legacy columns.
2026-05-10 22:18:03 +02:00
Mitchell R
16ab165b06 feat: live updates via WebSocket — server pushes, kiosk reloads
Server side:
- service-coordinator-ws: full WS implementation using ws package
- Auth via ?token=<kiosk_key> query param
- Coordinator registry for cross-plugin notification
- Admin mutations call notifyKiosks() → server pushes reload-bundle
- 30s ping/pong heartbeat

Kiosk side:
- Rust ws_client with tokio runtime + tokio-tungstenite
- Auto-reconnect with exponential backoff (1s → 60s cap)
- On reload-bundle: re-fetches bundle, re-renders layout
- Pong replies to server pings

Also fix: auto-suffix kiosk name on UNIQUE collision (re-pair with
same hostname no longer fails).
2026-05-10 22:15:58 +02:00
Mitchell R
2398be6853 fix: drop legacy layouts.template_id/display_id columns via table rebuild
- Migration v0.5: rebuild layouts table without template_id, display_id,
  regions, grid_cols, grid_rows, is_default
- Migration v0.6: rebuild layout_cells table without region_name
- Migration v0.7: drop layout_templates table entirely (concept removed)
- createLayout simplified to clean column set (no sentinel values)
- createLayoutCell simplified (no region_name placeholder)
- Removed all layout_template repo methods (dead code)

Answers user question "why template_id/display_id in template": SQLite
can't drop columns without rebuilding the table. Now done properly.
2026-05-10 22:03:32 +02:00
Mitchell R
b8f934b2be fix: createLayout passes 0 for legacy NOT NULL template_id 2026-05-10 21:59:47 +02:00
Mitchell R
533412a826 refactor: Nx-Witness layout builder + drop regions/is_default
- Cells own position directly (row/col/row_span/col_span)
- Drop regions JSON from layouts (cells ARE the regions)
- Drop is_default from layouts (display.default_layout_id owns)
- Drop grid_cols/grid_rows from layouts (computed from cells)
- Layout new form: name, description, priority, resets_idle_timer only
- Layout edit: visual grid builder, + buttons on cell edges,
  click cell to assign content
- Bundle cells now carry position directly
- Rust kiosk attaches widgets using cell position
- Migration v0.4: backfills cell positions from old region map
2026-05-10 21:55:19 +02:00
Mitchell R
7fbda3c2b3 refactor: merge templates into layouts, displays from kiosks
- Eliminated layout_templates as separate entity — regions/grid now
  live directly on layouts
- Displays created from kiosk pairing (not standalone), each display
  has kiosk_id FK
- Removed Templates from sidebar nav and all template routes/pages
- Layout creation uses preset buttons (fullscreen, 2x2, 1+3, 3x3)
  that set regions directly on the layout
- Setup no longer creates default display/layout (deferred to pairing)
- Pairing creates HDMI-0 display for new kiosk
- Bundle reads regions from layout directly, no template lookup
- Rust kiosk updated to match new bundle format
- DB migration adds regions/grid_cols/grid_rows to layouts, kiosk_id
  to displays, copies existing template data
2026-05-10 21:39:09 +02:00
Mitchell R
e3bb5ae048 feat: RTSP field split, coordinator-ws stub, CLAUDE.md update
- Camera add/edit: split RTSP URL into host/port/path/user/pass fields
- Camera edit updates stream URI when RTSP URL changes
- service-coordinator-ws: HTTP health stub (WS upgrade deferred)
- Repository: added updateCameraStream
- CLAUDE.md: full rewrite reflecting current architecture
2026-05-10 15:35:47 +02:00
Mitchell R
9827e456b3
fix: URL-encode RTSP credentials to handle special chars like @ 2026-05-10 04:03:23 +02:00
Mitchell R
bcaca8fdc7
fix: default stream_selector to 'auto' for non-camera cells 2026-05-10 03:48:23 +02:00
Mitchell R
cc306cec57
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
  (no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
  BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
  to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
  updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
  updateDisplay, and more
2026-05-10 03:45:53 +02:00
Mitchell R
7b4a11c182
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
  attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
  with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
  deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
  updateLabel, cameraLabelIds
2026-05-10 03:24:04 +02:00
Mitchell R
94e316a207
feat: implement kiosk API, pairing flow, and bundle generation
- service-api-http: h3 on :18081 with pairing, bundle, heartbeat,
  and event endpoints
- shared/pairing.ts: 8-char code state machine (initiate → claim →
  confirm)
- shared/bundle.ts: label-scoped bundle with cluster-encrypted ONVIF
  passwords
- Admin kiosks page: POST /admin/kiosks/pair wired to confirmPairing
- sec-config: api-http bound to 0.0.0.0 with auth config
2026-05-10 03:12:07 +02:00
Mitchell R
3f358e5e5e
fix: remove hx-boost causing broken AJAX navigation 2026-05-10 03:05:14 +02:00
Mitchell R
bacf1d2fcb
fix: use deterministic HKDF key for cookie signing
cookieMac was using encryptString which generates a random IV per
call, making the HMAC key non-deterministic. Cookie signed at login
could never verify on subsequent requests. Now uses deriveKey(info)
which uses HKDF — deterministic for same server key.
2026-05-10 02:59:04 +02:00
Mitchell R
fe11519925
fix: drop Secure cookie flag for non-TLS dev access 2026-05-10 02:57:31 +02:00
Mitchell R
3c5256bbb4
fix: avoid h3 setCookie, use Set-Cookie header on Response directly
h3 v2's setCookie modifies event response headers but doesn't
carry them when handler returns a raw Response object. Build
Set-Cookie header manually in redirect helpers instead.
2026-05-10 02:53:06 +02:00
Mitchell R
a64363258d
fix: use Response wrapper instead of h3 tagged template html()
h3 v2's html() is a tagged template literal, not a function that
accepts a string. JSX-rendered markup passed directly causes
"first.reduce is not a function". Created htmlPage() helper that
wraps markup in a proper Response with text/html content type.
2026-05-10 02:50:16 +02:00
Mitchell R
a8b0fbb2bc
refactor: collapse 6 non-service plugins into shared modules
BSB plugins should be actual services (own port, lifecycle, resource
ownership). Moved secrets, auth, pairing, bundle, nodered-bridge, and
cec-relay from plugin folders to shared modules under server/src/shared/.

4 BSB plugins remain: service-store, service-admin-http,
service-api-http, service-coordinator-ws.

service-admin-http now initializes secrets + auth as plain modules in
init() using the store repo from the plugin-registry singleton. No
more setSiblings() hack or inter-plugin wiring.

sec-config.yaml updated: secrets/auth config moved into
service-admin-http, pairing config into service-api-http, nodered
config into service-coordinator-ws.
2026-05-10 02:29:25 +02:00
Mitchell R
2fd2502b85
adding initial project 2026-05-10 01:09:13 +02:00