Commit graph

99 commits

Author SHA1 Message Date
Mitchell R
70bdc3bb8b
fix(cursor): correct Xcursor binary format (was missing version field)
Previous generator packed 5 fields in the image chunk header but Xcursor
format needs 9 (header_size, type, nominal, version, w, h, xhot, yhot,
delay). Missing version field → malformed → wlroots ignored it → fell
back to default visible cursor. Now writes correct 68-byte Xcursor with
all 9 header fields. Added more cursor names (x_cursor, pirate, sides).

Also: terminal UI shows bash-style cwd$ prompt, separates command from
output visually, auto-detects pwd after each command for prompt update.
2026-05-23 00:22:28 +02:00
Mitchell R
9ebdc894a1
fix(terminal): get channel from server heartbeat response, not env/build 2026-05-22 20:51:18 +02:00
Mitchell R
76f725c149
fix(coordinator): use config.cookieName directly, not envStr 2026-05-22 20:42:48 +02:00
Mitchell R
5198a681eb
debug(ws): log admin debug WS auth failure details 2026-05-22 20:39:19 +02:00
Mitchell R
31ba05b703
fix(debug-ws): route via /admin/ws/debug/ so Angie forwards correctly 2026-05-22 20:28:26 +02:00
Mitchell R
1f0bcd1084
fix(remote-debug): successful auth resets lockout + drop empty WS token param 2026-05-22 20:23:20 +02:00
Mitchell R
c5068615ee
feat(remote-debug): journal streaming + secure terminal via WebSocket
Kiosk side (remote_debug.rs + ws_client.rs refactor):
  - Journal streaming: server sends journal-start → kiosk spawns
    journalctl -f, pipes lines back as journal-line messages via WS.
    journal-stop kills the process. On-demand, not always-on.
  - Terminal: server sends terminal-request → kiosk checks lockout +
    firmware_channel == "dev" → generates 8-char code displayed on
    screen as fullscreen overlay (NOT logged) → server relays admin's
    code via terminal-auth → kiosk validates with constant-time compare
    → on success spawns bash, relays I/O as base64 terminal-data.
  - Lockout: 3 failed codes per boot → lockout_count++. 3 lockouts
    (9 total failures) → permanent (reflash only). Reboot resets
    attempt counter, not lockout counter. Successful pairing resets all.
  - ws_client.rs rewritten with split reader/writer + tokio::select!
    for multiplexing incoming WS messages with outbound journal/terminal
    data from sync threads.

Server side (coordinator-ws + routes-admin):
  - New admin debug WS endpoint: /ws/admin/debug/:kioskId. Authenticated
    via admin API key (query param) or session cookie. Relays messages
    bidirectionally between admin browser ↔ kiosk.
  - Admin pages: /admin/kiosks/:id/logs (journal viewer with start/
    stop/clear) and /admin/kiosks/:id/terminal (code entry + terminal
    area). Both open in new tabs from the kiosk detail page.
  - Angie proxy config updated with /ws/admin/debug/ location block.

Security:
  - Terminal only on dev channel
  - Code displayed physically on screen, never logged or stored server-side
  - Lockout: 3/boot, 3 lockouts = permanent, pairing resets
  - Kiosk responds "locked" without specifying which lockout triggered
2026-05-22 20:13:39 +02:00
Mitchell R
e0b9955522
fix(admin): only show Live Events panel for ONVIF cameras 2026-05-22 19:48:41 +02:00
Mitchell R
05ca368f29
fix(onvif): import discovered cameras as type=onvif with credentials
importDiscoveredCamera was hardcoded to type="rtsp", losing ONVIF
identity. Camera edit showed RTSP fields, ONVIF event subscription
skipped (checks cam_type=="onvif"), re-discovery impossible.

Now creates type="onvif" with onvif_host/port/username/password stored
on the camera row. Streams still go into camera_streams (unchanged).
Bundle ships onvif fields → kiosk subscribes to PullPoint events.

Also passes host + port as hidden form fields from discover results
page so the add handler has them available. Basic manual camera
creation via UI stays rtsp-only (simpler); discovery flow produces
onvif type.
2026-05-22 18:30:41 +02:00
Mitchell R
2e40e78413
fix(admin): mask passwords in stream RTSP URIs on camera detail page 2026-05-21 16:29:24 +02:00
Mitchell R
7d81891b0e
fix(version): derive server version from git at Docker build time
Coolify pulls from GitHub and runs docker compose build — no guaranteed
env vars like SOURCE_COMMIT. Previous approach relied on ARG/ENV
passthrough that silently defaulted to "dev".

Fix: install git in the builder stage, COPY .git into context, run
git describe --tags --always to derive the version, write it to
/app/server/.bf-version. version.ts reads this file as a fallback
between env vars and the "dev" literal.

Chain: BF_SERVER_VERSION env → BF_BUILD_VERSION env → .bf-version file
→ COOLIFY_GIT_COMMIT env → SOURCE_COMMIT env → "dev".

Also: fix .gitignore for rauc-signing/ (was under wrong path).
2026-05-21 16:02:21 +02:00
Mitchell R
6e10913380
fix(admin): cell edit no longer corrupts grid when spans change
Use hx-retarget/hx-reswap response headers to replace full grid
when cell dimensions change (overlap resolution may move other cells).
Single-cell swap when only content fields change.
2026-05-21 15:12:55 +02:00
Mitchell R
9f382775a7
feat(cameras): live ONVIF event feed on camera detail page
Camera edit page gains a "Live Events" panel that auto-refreshes every
5s via htmx. Shows last 20 events for this camera from event_log:
topic, source type, timestamp, and raw payload JSON. Surfaces ALL
ONVIF topics including unknown ones — if a camera produces an event
type we haven't seen before, it shows up here immediately.

queryEvents gains camera_id + source_type filters. Route
GET /admin/cameras/:id/events returns an HTML fragment with the event
table rows.
2026-05-21 12:09:09 +02:00
Mitchell R
74a871cd9b
fix(store): use display_layouts join table in listKiosksWithCameraInBundle
Old query referenced removed layouts.display_id column.
2026-05-21 12:08:56 +02:00
Mitchell R
e770e48f76
fix(layout): resolve cell overlaps on resize/expand
Replace naive per-cell shift with iterative overlap resolver. When any
cell grows (direction expand, dim/delta resize, or cell edit with new
spans), all overlapping cells get pushed along the expansion axis, with
cascading to prevent pushed cells from overlapping each other.

Fixes: expanding left block goes under right block instead of pushing it.
2026-05-21 12:07:32 +02:00
Mitchell R
8e75ed379d
feat(nodered): install Dashboard 2.0 + auto-sync on entities page
- Bake @flowfuse/node-red-dashboard into Node-RED Docker image
- Fire-and-forget syncDashboardsFromNodered() on GET /admin/entities
  so dashboard tabs appear without manual sync button click
2026-05-21 12:05:12 +02:00
Mitchell R
0ae161173a
feat(admin): clone layout with cells, labels, and display attachments
Adds Clone Layout button to layout edit page. Duplicates the layout
with all cells, label bindings, and display attachments. Name gets
"(copy)" suffix with dedup.
2026-05-21 12:04:11 +02:00
Mitchell R
991c2f0cd5
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.

Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.

Subscription lifecycle:
  - CreatePullPointSubscription with 60s InitialTerminationTime
  - Renew every 55s before timeout
  - Unsubscribe on bundle change / shutdown
  - Auto-resubscribe on pull/renew failure (30s backoff)
  - Generation tracking via Weak<()> so old workers self-terminate
    when start() is called with a new bundle

WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.

BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).

Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.

TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 12:03:30 +02:00
Mitchell R
9129613920
feat(cameras): sync entity name on rename + ONVIF device name from GetDeviceInformation
Two fixes:

1. When admin renames a camera, the linked entity's name now syncs
   automatically so the entity list doesn't drift from the camera list.

2. ONVIF discovery now calls GetDeviceInformation before GetProfiles
   (best-effort, catches auth-gated devices). Pulls Manufacturer + Model
   and uses the combined string as the camera's proposed name instead of
   the raw IP. E.g. "Hikvision DS-2CD2146G2" instead of "192.168.74.8".
   Falls back to host IP when the device omits the info.
2026-05-21 11:57:38 +02:00
Mitchell R
5edf9d4b0b
feat(cameras): show kiosk subscriptions on camera detail page
Camera edit page now shows a "Kiosk Subscriptions" table: every kiosk
whose layouts reference this camera, which specific layout names, and
whether the camera is in the kiosk's active layout (green "active"
badge) or just bundled (gray "bundled" badge).

Snapshot route switched from listKiosksRenderingCamera (active-only)
to listKiosksWithCameraInBundle (any layout). The kiosk's LAN endpoint
opens a one-shot RTSP connection from its own network position even when
the camera isn't on screen — no warm pipeline needed. Server falls back
to direct pull only when NO kiosk has the camera in any layout at all.
2026-05-21 11:54:25 +02:00
Mitchell R
4c1edbd3b2
fix(migrations): catch-all backfill for all missing tables/columns
Every column + table added inside an already-passed PRAGMA user_version
entry is re-created here with IF NOT EXISTS / addColumnIfNotExists so
existing deploys finally pick them up:

  - kiosks: reported_hostname, network_interfaces_json
  - kiosks: os_update_channel, os_update_target_version, os_update_last_*
  - kiosks: managed_image, managed_config_*
  - displays: active_layout_id
  - os_update_releases table + indices
  - os_update_rollouts table + indices

Rule going forward: NEVER add columns/tables inside existing migration
entries. Always append a NEW entry at the end of the MIGRATIONS array.
2026-05-21 11:46:20 +02:00
Mitchell R
436d2d730c
feat(harden): hardware-bound at-rest encryption of kiosk state files
New module kiosk/src/at_rest.rs. Derives an AES-256-GCM key via HKDF
from a Pi-bound value:
  1. /proc/device-tree/serial-number  (Pi 5 firmware exposes it)
  2. /proc/cpuinfo Serial line          (older kernels)
  3. /etc/machine-id                    (non-Pi dev fallback)

File format: "BFE1" magic || 12-byte random nonce || ciphertext+tag.
Atomic write via tempfile + rename so a crash mid-write can't leave a
half-encrypted file.

Wired into kiosk/src/server.rs at every file I/O touching sensitive
state:
  - kiosk.key      (bearer token to BF server)
  - local.key      (LAN-side API auth key)
  - bundle.json    (cached bundle with RTSP credentials in URL form)

Migration: read paths tolerate legacy plaintext (kiosks upgraded from a
pre-at_rest build) AND re-store as ciphertext on the first read. One-
shot upgrade — subsequent boots skip the migration write.

Threat model defended: SD card extraction. Attacker who pulls the card
can't decrypt without also having the same physical Pi (CPU serial is
hardware-bound). Doesn't defeat an attacker who has both — at that
point they ARE the kiosk. Bar is raised from "trivially extract every
camera password" to "must steal the device intact."

Not defended: TPM-style attestation, remote attestation, sealed boot.
Pi 5 has no TPM and we don't ship a secure-boot config.

Tests in-module: round-trip short bytes, round-trip JSON, legacy
plaintext passthrough.
2026-05-21 11:34:29 +02:00
Mitchell R
90346f4efd
feat(os-ota-ui): admin pages for OS releases + rollouts + per-kiosk panel
Mirrors the kiosk-firmware admin shape one-for-one against OS RAUC
bundles:

  /admin/os-updates                   release list, yank
  /admin/os-updates/rollouts          rollout list + create
  /admin/os-updates/rollouts/:id/state pause/resume/complete
  /admin/kiosks/:id/os-update         per-kiosk channel + pin

Templates: OsUpdatePage, OsUpdateRolloutsPage, KioskOsUpdatePanel.
KioskOsUpdatePanel is rendered next to the existing KioskFirmwarePanel
on the kiosk detail page so OS + app state sit side-by-side. The
"how bundles get here" sidebar on the list page documents the four
GitHub secrets needed (signing cert/key + autoimport URL/key) so a
new operator doesn't have to dig through scripts/ to find them.

Nav gains an OS Updates entry between Firmware and Labels. Activates
on activeNav="os-updates".

Repo + import endpoint already existed (audit confirmed earlier). All
admin routes use them as-is.
2026-05-21 11:30:33 +02:00
Mitchell R
334ee8fb93
feat(preview): pull entity snapshot from active kiosk first
When admin opens an entity preview, find a kiosk whose active layout
references the camera (new repo.listKiosksRenderingCamera). Probe each
candidate's LAN snapshot endpoint with a 4s timeout. On success, stream
the bytes back with x-bf-snapshot-source: kiosk:<id>. Falls through to
the existing server-direct ffmpeg/gst pull only when no kiosk is reachable
or has the camera in its active layout.

Kiosk side adds /local/snapshot/:camera_id?key=<local_key>. Spawns a
one-shot gst-launch (rtspsrc → decodebin → jpegenc ! filesink
num-buffers=1) on a blocking worker so axum's reactor stays free.
Prefers sub stream for snapshots to keep bandwidth low. Single-frame
pipeline tears down after the first JPEG.

LAN IP picking extracted to shared/kiosk-lan.ts so route handler and
KioskLocalPanel agree on which interface to talk to (the previously-
duplicated logic in admin-pages stays for now since it also renders the
interface list).

Why a parallel pipeline instead of teeing the warm one: cross-thread
gtk4paintablesink → appsink sample extraction is non-trivial. A 1-frame
parallel pull is cheap when the kiosk's RTSP session to that camera is
already known to work (precondition: it's in the active layout).
2026-05-21 10:35:27 +02:00
Mitchell R
7df048c195
feat(display): persist + surface active layout
Kiosk's layout.changed events now bump displays.active_layout_id on the
server side. Display edit page and kiosk edit page render the currently-
active layout, and the "Switch Layout" dropdowns pre-select it (with
"(active)" suffix) instead of defaulting to first-in-list. Stops the
operator from accidentally re-switching to the layout already showing.

Migration is idempotent + tail-positioned so existing DBs pick up the
column without breaking PRAGMA user_version semantics.
2026-05-21 10:19:39 +02:00
Mitchell R
d51e01ff0e
feat(pairing): validate replace-target matches existing kiosk
Replacing a kiosk now sanity-checks the incoming device:
- hardware_model must match (Pi 5 swapping in for Pi 5, not Pi 3)
- managed_image flag must match (don't silently switch BYO-OS ↔ image)
- capabilities can narrow legitimately but a "lost capabilities" diff is
  surfaced anyway so the operator notices.

Mismatch raises an error listing what changed; "Force replace" checkbox
on the pair form bypasses for legitimate hardware upgrades. Pending codes
panel also now renders proposed_name / hw_model / capabilities /
managed-image badge so the operator can eyeball the inbound device
before picking a replace target.
2026-05-21 10:16:55 +02:00
Mitchell R
6b959755e7
fix(migrations): backfill missing hwmon columns on existing DBs
cpu_load_percent + memory/disk columns were silently added inline to the
hwmon migration entry, but PRAGMA user_version had already passed that
index for existing deploys → ALTER never ran → replaceKioskKey and
heartbeat hit "no such column: cpu_load_percent" on upgrade. Append a
tail migration that addColumnIfNotExists for each. Lesson: never mutate
an existing migration entry; always append a new one.
2026-05-21 10:14:52 +02:00
Mitchell R
28ff450d35
revert(bundle): restore type==='rtsp' gate on stream fallback
ONVIF cams legitimately have multiple streams (main+sub) stored in
camera_streams. Dropping the type gate synthesized a single "main"
row for ONVIF cams that lacked rows, hiding the multi-stream design
the kiosk's pick_stream relies on (area >= 0.2 → main, else sub).
The "(no stream)" symptom is a partial-import bug to chase separately;
the bundle fallback is for single-URL RTSP cams only. Also drop the
backfill migration that did the same insert at the DB layer.
2026-05-21 09:39:54 +02:00
Mitchell R
281c0adf44
fix(bundle): synthesize stream for any camera with rtsp_url
ONVIF-imported cameras with rtsp_url but no camera_streams rows showed
"(no stream)" in the kiosk because the bundle fallback was gated to
type=rtsp only. Drop the type check + backfill existing rows so old
imports get a main stream row created.

feat(kiosk-mgmt): report hostname + all network interfaces

Behind Docker/Angie the server only saw the proxy bridge IP (172.31.0.2).
Kiosk now shells `ip -j addr show`, reports every non-loopback IPv4/v6
with CIDR, MAC, and operstate. Plus `hostname` for verifying that
managed-config applies landed. Admin UI renders interface list with
LAN IPs preferred for the copy-paste local-LAN endpoint.

feat(managed-config): auto-sync hostname from kiosk name

When admin renames a managed-image kiosk, slugify the name → DNS-safe
hostname and bump managed_config_version so the kiosk applies it on
next heartbeat. Empty form hostname now falls back to slug too, so
DHCP shows the friendly name.

feat(events): forward firmware + OS update outcomes as kiosk.log

Kiosk POSTs `/api/kiosk/event` with topic=kiosk.log on firmware-apply
attempts. Server-side firmware/os-update endpoints also insert into
event_log so admins can audit upgrades without correlating per-source.
Wire schema heartbeat gains reported_hostname + network_interfaces for
Rust import parity.
2026-05-21 09:23:50 +02:00
Mitchell R
49e420dea5
feat(display): report and control power state 2026-05-21 09:10:30 +02:00
Mitchell R
6cfb37aa64
fix(admin): restore display layout switching 2026-05-21 08:57:54 +02:00
Mitchell R
3d5e27bdfb
fix(release): surface build versions 2026-05-21 08:51:41 +02:00
Mitchell R
3ffaf780e3
feat(kiosk): improve display controls and health 2026-05-21 02:03:12 +02:00
Mitchell R
96f5e6a330
feat(ota): add OS update release endpoints 2026-05-20 06:19:46 +02:00
Mitchell R
9942957bcf
feat(onvif): run discovery from selected kiosk 2026-05-20 06:16:27 +02:00
Mitchell R
444bb4c116
feat(firmware): allow env import token 2026-05-20 05:02:12 +02:00
Mitchell R
dae5d0ce88
feat(managed-config): server-side scaffold for Pi-image device config
Kiosks running our pre-built image (managed_image=true at pairing) can
have their hostname, timezone, network (DHCP/static + VLAN), and Wi-Fi
configured from the admin UI. Pull-model: server stores desired-state
JSON, kiosk heartbeat returns pending_config when version exceeds
applied_version, kiosk echoes applied_version back. Wi-Fi PSK encrypted
with the cluster key so ciphertext at rest is shipped to the kiosk
without per-kiosk re-encryption.

Server side only — kiosk Rust applier (betterframe-apply-config helper
+ rollback timer) and pair-initiate marker file are next.

ci(pi-gen): use action's image-path output for asset upload

pi-gen writes the .img.xz into pi-gen-action's own working dir, not our
repo deploy/. Glob never matched. Use steps.pigen.outputs.image-path
directly — no glob needed.
2026-05-20 03:18:11 +02:00
Mitchell R
6473f0fc95
fix(firmware): diagnostic dump + smart-quote / BOM / multi-quote handling
Adds aggressive normalisation to tryParsePrivateKey:
- Strip UTF-8 BOM
- Replace smart quotes (" " ' ') with ASCII
- Strip multiple layers of wrapping quotes
- Combine escape-unfold with quote-strip (env vars that quote AND escape)
- Strip whitespace inside base64 candidate before decode

On parse failure, dumps length + head/tail samples + first-byte hex so
the operator can spot exactly what shape the env var arrived in.
2026-05-18 22:52:35 +02:00
Mitchell R
936e6170a6
feat(store): Postgres adapter foundation + BF_DB selector (phase 1)
Lays groundwork for sqlite|postgres backend selection without yet
converting Repository. Adds:

- db-adapter.ts: async DbAdapter interface (run/get/all/exec/transaction)
- sqlite-adapter.ts: wraps node:sqlite sync API in Promise-returning shape,
  caches prepared statements
- pg-adapter.ts: pg Pool + ? → $N placeholder rewrite + RETURNING-id
  capture + savepoint-nested transactions
- service-store config: driver (sqlite|postgres), pgUrl
- BF_DB env override, plumbed via envStr

Selecting BF_DB=postgres throws at init() until the Repository is
converted off DatabaseSync. This commit ships the foundation only.

Next phases (separate commits):
  2. Convert Repository methods sync → async via DbAdapter
  3. Update every caller to await
  4. Split MIGRATIONS into sqlite + portable / pg-specific sets
  5. UUIDv7 IDs for new tables on PG path

Adds deps: pg ^8.13.1, uuidv7 ^1.0.2, @types/pg ^8.20.0
2026-05-18 22:50:48 +02:00
Mitchell R
8082571b03
fix(firmware): tolerate mangled PEM in BF_FIRMWARE_SIGNING_KEY env
Coolify / docker compose env injection routinely strips real newlines or
wraps in quotes, causing createPrivateKey to throw ERR_OSSL_UNSUPPORTED
and crashing the server before it can even start.

tryParsePrivateKey now attempts: literal, \n→LF, CRLF→LF, quote-stripped,
base64-decoded, and single-line PEM re-wrapped to 64-col. On total
failure, logs a clear warning and falls back to on-disk / generated key
instead of crashing.
2026-05-18 22:47:07 +02:00
Mitchell R
150972a272 fix(server): move rate-limit creation inside register fns (BSB schema extractor)
Schema extractor evaluates module top-level statically; createRateLimiter
calls at module scope threw ReferenceError during bsb-plugin-cli build.
Lifting into the per-route register functions keeps build clean.

Also: standardise display.standby/wake audit hooks.
2026-05-14 07:49:57 +02:00
Mitchell R
17f8c7ce02 feat(server): generic MQTT telemetry bridge (off by default) 2026-05-14 07:46:56 +02:00
Mitchell R
aa4e91491b feat(server): backup + restore (AES-256-GCM, PBKDF2, admin UI) 2026-05-14 07:44:01 +02:00
Mitchell R
a6c1fb4d8d feat(server): rate limit (login + pair) + CSP/security headers 2026-05-14 07:40:22 +02:00
Mitchell R
3ec2f3bf85 feat(server): audit log — schema, helper, admin UI, hooks for login/pair/firmware 2026-05-14 07:38:18 +02:00
Mitchell R
d1fd128ea0 feat(server): env-var overrides for sec-config keys + docker healthchecks 2026-05-14 07:33:10 +02:00
Mitchell R
69cd0391b5 feat(ota): phase 3 — rollouts + automated rollback
Rollouts (server side):
- /admin/firmware/rollouts page lists + creates campaigns. Pick release,
  target kiosk_ids (empty = whole channel), percentage (1-100).
- Active rollouts override channel-latest in /api/kiosk/firmware/check.
- Deterministic bucket via sha256(rollout_id:kiosk_id) % 100 — same kiosk
  consistently lands in the same bucket across re-checks.
- Pause / resume / complete state controls.

Rollback (kiosk side):
- Before swap, kiosk writes firmware-applying.json marker.
- After clean boot + first successful heartbeat, marker deleted.
- New ExecStartPre hook (/usr/local/sbin/betterframe-firmware-rollback.sh)
  runs every service start; stale marker (>120s) + .prev present →
  restore .prev. Pairs with systemd's StartLimit to catch crash loops.
2026-05-14 07:28:20 +02:00
Mitchell R
6a8f6d76af feat(kiosk): LAN-side local HTTP server (GET layout API + admin proxy)
Kiosk now exposes :18090 with two surfaces:

- GET /local/layout/:id?key=<kiosk_local_key>
  Bookmark-friendly layout switch on this kiosk. Auth = kiosk-generated
  local key (32 random bytes, hex, stored at <state_dir>/local.key).

- ANY /proxy/* — forwards to BF server with the request's Authorization
  header preserved. Lets LAN clients reach a cloud-hosted BF server via
  the kiosk's local socket; kiosk adds no auth of its own.

Heartbeat reports {local_key, local_port}; kiosks table grows
local_key/local_port/local_last_ip columns. Admin kiosk edit page now
shows the local URLs as a copy-paste block.

Override port: BF_KIOSK_LOCAL_PORT. Disable: BF_KIOSK_LOCAL_DISABLE=1.
2026-05-14 07:24:21 +02:00
Mitchell R
e5009fdd14 feat(ota): replacement pairing + firmware OTA (admin UI, kiosk client, CI) 2026-05-13 20:56:42 +02:00
Mitchell R
b10958def7 fix(nodered): kiosk-side layout.changed events + provisioning retries
Three related fixes:

1. Idle reverts (and any other kiosk-initiated layout switch) now POST
   layout.changed to /api/kiosk/event. Previously the server only emitted
   on admin-initiated switches, so Node-RED never saw the idle revert.

2. Server's /api/kiosk/event splays the payload to the top level when
   the topic has a dedicated trigger node (layout.changed, kiosk.changed,
   kiosk.status, display.power.changed, camera.changed). The trigger
   nodes expect flat shapes matching the admin emit; the old wrapped
   shape left every field undefined.

3. Auto-provisioning of bf-server-config in Node-RED: extend retry
   window to ~5 min, log per attempt, force v2 API + full-deploy header
   so credentials inline get accepted, surface response body on failure.
2026-05-13 13:03:51 +02:00