Commit graph

216 commits

Author SHA1 Message Date
Mitchell R
9679ae7eb1 ui: default kiosk background to black 2026-05-11 11:50:45 +02:00
Mitchell R
51c58e7abf feat: Pi fan control + temp monitoring + stream swap on layout change
Kiosk:
- hwmon.rs reads /sys/class/thermal + /sys/class/hwmon for CPU temp,
  fan RPM, fan PWM
- Heartbeat reports cpu_temp_c, fan_rpm, fan_pwm
- WS message "fan" with {pwm: N} or {mode: "auto"} sets pwm1_enable+pwm1
- Picture content_fit Cover → Contain (no more cropping/overlay cuts)
- ensure_warm tears down + rebuilds pipeline when desired stream
  changes (M↔S swap on layout change)

Server:
- Migration v0.8: add cpu_temp_c, fan_rpm, fan_pwm to kiosks
- Heartbeat persists hwmon fields
- KioskEditPage shows CPU/fan/PWM + Auto/Off/50%/Full buttons
- POST /admin/kiosks/:id/fan dispatches via coordinator WS
2026-05-11 11:47:07 +02:00
Mitchell R
5a67c80caa fix: fire CEC AND DPMS unconditionally for power commands
Pi5 cec-ctl returns ok even when monitor ignores CEC. Doing both
covers TVs (CEC) and monitors (DPMS) without detection logic.
Both idempotent.
2026-05-11 11:18:31 +02:00
Mitchell R
0cd508a2ec chore: remove unused legacy stream_uri method 2026-05-11 11:06:24 +02:00
Mitchell R
29b7e30844 feat: auto stream selection + M/S badge overlay
Kiosk pick_stream() implements CLAUDE.md heuristic:
- cell area >= 20% of grid → main stream
- cell area < 20% → sub stream
- explicit "main"/"sub" selector still honored

Badge overlay shows which stream is rendering:
- 'M' when camera has multi-stream and we picked main
- 'S' when we picked sub
- nothing when single-stream

Small label, top-left corner, semi-transparent black background.

Reduces buffer drops on multi-camera grids — small cells now use
low-res sub streams instead of all decoding 4K main.
2026-05-11 11:05:38 +02:00
Mitchell R
820e0a5945
fix(proxy): split Node-RED route surfaces
Route backend, kiosk ingest, kiosk dashboards, and public Node-RED HTTP-in separately. Keep Node-RED editor under admin auth and attach kiosk auth when kiosk loads protected dashboard URLs.
2026-05-11 10:44:45 +02:00
Mitchell R
346ddfa3a4
fix(proxy): strip Node-RED ingress bases
Keep the two external ingress flows at /in/public and /in/kiosk while allowing Node-RED routes to stay path-local, such as /test1.
2026-05-11 10:38:32 +02:00
Mitchell R
02412169a0
fix(deploy): make Docker the service runtime
Remove host daemon deployment for server, proxy, and Node-RED so Node-RED is only reachable through the Compose proxy boundary.
2026-05-11 10:08:33 +02:00
Mitchell R
96d7cc45ba
fix(deploy): require proxied local services
Bind native backend services and Node-RED to loopback so Angie remains the public auth boundary. Keep Docker on an internal compose network and stop kiosk fallback to a layout when display default is none.
2026-05-11 09:51:00 +02:00
Mitchell R
026325ccd0
feat(layout): add branded none cells
Migrate empty layout cells to an explicit none state so kiosk renders the BetterFrame placeholder instead of blank HTML.
2026-05-11 09:38:50 +02:00
Mitchell R
b3c17a9d53
fix(deploy): gate proxied runtime routes 2026-05-11 08:57:55 +02:00
Mitchell R
e38c92f753
fix(power): add monitor fallback checks 2026-05-11 08:55:42 +02:00
Mitchell R
0d9451ae95
feat(onvif): batch import discovered cameras 2026-05-11 08:40:25 +02:00
Mitchell R
bd20580f06
fix(layouts): anchor side action menu 2026-05-11 00:31:44 +02:00
Mitchell R
ab8eeb1d09
fix(layouts): push cells on expand 2026-05-11 00:29:33 +02:00
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
00b304c39f feat: stream warmth — keep cameras warm across layout swaps
Previously every reload-bundle killed and restarted all pipelines.
Now:
- WARM_CAMERAS map: camera_id → (pipeline, paintable)
- On reload: stop only pipelines for cameras no longer needed
- Needed = cells with content_type=camera + layout.preload_camera_ids
- Reuse existing pipeline+paintable, attach to new Picture widget
- Preloaded cameras keep decoding even when not visible

Achieves the "zero perceived latency" layout swap goal from CLAUDE.md
when cameras overlap between layouts.
2026-05-10 22:51:28 +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
c0704be343 feat: DPMS fallback via wlr-randr for non-CEC desktop monitors 2026-05-10 22:46:30 +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
3a8fd70528 fix: stop old pipelines before re-render to prevent buffer overload 2026-05-10 22:21:02 +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
72d8ad717f fix: import PluginFeatureExtManual for set_rank 2026-05-10 20:59:23 +02:00
Mitchell R
61ab099f87 fix: demote Pi5 hw H265 decoder — sw fallback for non-standard resolutions 2026-05-10 20:52:25 +02:00
Mitchell R
246febcc80 fix: filter rtspsrc pads — only link video to decodebin, skip audio 2026-05-10 20:51:29 +02:00
Mitchell R
877596fc15 fix: add comprehensive pipeline logging for debugging 2026-05-10 20:49:00 +02:00
Mitchell R
30eb85e6b1 fix: use property_from_str for queue leaky enum 2026-05-10 20:14:58 +02:00
Mitchell R
527a62d2e5 fix: drop HANDLES_COMMAND_LINE flag 2026-05-10 20:13:01 +02:00
Mitchell R
f6dec4bf39 fix: import ApplicationExt for set_flags 2026-05-10 20:12:14 +02:00
Mitchell R
e7237d077f fix: suppress GTK file-open warning, read server URL from env 2026-05-10 20:11:31 +02:00
Mitchell R
76af07de61 fix: use std::sync::mpsc + timeout_add_local for thread→UI messaging 2026-05-10 20:10:23 +02:00
Mitchell R
df231344a8 fix: use glib channel for thread→UI communication (Send safety) 2026-05-10 20:09:06 +02:00
Mitchell R
c4315917d8 fix: resolve all Rust compile errors in kiosk app 2026-05-10 20:04:43 +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
371c023c81
feat: Rust kiosk app — GTK4 + GStreamer multi-camera display
- Server discovery (localhost → betterframe.local → cloud)
- Pairing flow with fullscreen code display
- Bundle fetch and layout rendering
- GTK4 Grid layout matching template regions
- GStreamer pipelines per camera cell via gtk4paintablesink
- Heartbeat loop in background thread
- Placeholder widgets for web/html cells
2026-05-10 04:18:40 +02:00
Mitchell R
f5a2645ffc
fix: force sw decode for H265 — Pi5 hw decoder rejects 960x1080 2026-05-10 04:11:43 +02:00
Mitchell R
8422fdce90
feat: auto-detect codec — try H264, H265, then uridecodebin fallback 2026-05-10 04:11:18 +02:00
Mitchell R
888dc4a4e5
fix: use uridecodebin with video caps filter for codec-agnostic RTSP 2026-05-10 04:10:33 +02:00
Mitchell R
2588611501
fix: depay h264 before decodebin to avoid audio pad linking failure 2026-05-10 04:09:01 +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
a88e3f5c72
fix: use waylandsink with fullscreen for RTSP display 2026-05-10 03:58:40 +02:00
Mitchell R
bcaca8fdc7
fix: default stream_selector to 'auto' for non-camera cells 2026-05-10 03:48:23 +02:00