Commit graph

249 commits

Author SHA1 Message Date
Mitchell R
01a1aad2fd
fix(kiosk): rename reserved keyword gen, clean warnings
gen is reserved in Rust 2024 edition. Also remove unused
serde_json::Value import and prefix unused end_idx variable.
2026-05-21 12:14:24 +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
32382beb3b
fix(image): install gstreamer1.0-tools for gst-launch-1.0 snapshot pipeline 2026-05-21 11:58:41 +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
45c7ef26b2
fix(kiosk): add missing os_update import + guard Option<UCM>
1. ui.rs lacked `use crate::os_update` — module declared in main.rs
   but ui.rs couldn't resolve os_update::check/apply calls.
2. webkit6::WebView::user_content_manager() returns Option<UCM> not
   UCM directly — guard with if-let instead of calling method on Option.
2026-05-21 11:40:39 +02:00
Mitchell R
a7d661ca42
fix(ci): use job-level env vars for secret-gated step conditions
`secrets` context isn't available in step-level `if:` expressions inside
a reusable workflow_call. Move the secret-presence check to job-level
env (HAS_RAUC_SECRETS, HAS_AUTOIMPORT) and reference those in step if:.
2026-05-21 11:36:50 +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
d149ed68e5
feat(harden): nftables default-drop firewall + first-boot password rotation
Two image-side hardening pieces both small enough to ship together.

deploy/nftables/nftables.conf — single ruleset installed at /etc/nftables.conf.
Default-drop input. Allowed: loopback, established/related, ratelimited
ICMP, kiosk local API :18090 from RFC1918 / RFC4193 / link-local sources
only. SSH stays gated by sshd-disabled (image build sets enable-ssh: 0
and 01-run-chroot masks it); the firewall rule for :22 is left commented
in for triage scenarios. Forward dropped. Output left wide open — kiosk
needs to dial out to arbitrary RTSP cameras + the BF server (which may
live on the public internet) without explicit allowlisting.

deploy/systemd/betterframe-firstboot.{service,sh} — runs once per device
before betterframe-kiosk starts. Generates a 24-char unambiguous-glyph
password, applies via chpasswd, stores at /etc/betterframe/admin-password
(0400 root), and prints a banner to tty1 so an HDMI-attached operator
can transcribe it during the boot window before cage takes over the
screen. Marker at /var/lib/betterframe/.firstboot-complete prevents
re-run on subsequent boots. Without this, every kiosk built from the
same image shipped with bfadmin:betterframe — a single password leak
compromises the entire fleet.

Future follow-up: post the rotated password (encrypted with cluster_key)
to the BF server via heartbeat so admin UI can surface it. Not in this
commit; the local file + tty banner are the only retrieval paths today.
2026-05-21 11:18:28 +02:00
Mitchell R
0fa797adfa
feat(os-ota): install RAUC + system.conf + boot backend in pi-gen image
Phase 2b. Bake the runtime side of RAUC into the curated image so a
freshly-flashed kiosk can accept .raucb bundles immediately:

- Add `rauc` + `dosfstools` to the apt package list.
- Drop deploy/rauc/system.conf to /etc/rauc/system.conf (already declares
  the A/B slot layout that repartition-image.sh produces).
- Drop deploy/rauc/betterframe-rauc-boot.sh to
  /usr/local/sbin/betterframe-rauc-boot.sh — the custom bootloader
  backend that flips the BF_BOOTSEL autoboot.txt to point at the
  freshly-installed slot via Pi 5 tryboot.
- Drop deploy/rauc/ca-cert.pem (operator-supplied, committed) to
  /etc/rauc/keyring.pem so rauc can verify CMS signatures. If the cert
  isn't committed yet, image build emits a workflow warning and the
  kiosk image installs but refuses every bundle — image still flashes,
  just no OS OTA until the cert is committed.
- Enable BF_ENABLE_OS_OTA=1 in /etc/default/betterframe-kiosk so the
  kiosk Rust consumer actually polls for bundles. Set to 0 to pin OS
  version for a specific kiosk.

mark-good was already wired (deploy/systemd/betterframe-rauc-mark-good.{service,sh}).
The kiosk's heartbeat loop also calls `rauc status mark-good` as a
belt+suspenders backup; both are idempotent.
2026-05-21 11:09:10 +02:00
Mitchell R
3575f1169b
feat(os-ota): A/B image repartition + bigger Blacksmith binary runners
Phase 2a of OS OTA: post-process pi-gen output into a RAUC-compatible
A/B layout. New deploy/rauc/repartition-image.sh:

- Decompresses the stock pi-gen 2-partition image
- Extracts bootfs (vfat) + rootfs (ext4) blobs
- Compacts rootfs with resize2fs -M and grows back with 25% headroom
- Patches /etc/fstab inside rootfs to use LABEL=BF_BOOT_A /
  LABEL=BF_ROOT_A / LABEL=BF_DATA (slot-agnostic; RAUC re-labels per
  slot on install)
- Stamps /etc/betterframe/{os-version,os-compatibility} for the kiosk's
  os_update.rs to read at runtime
- Builds two bootfs copies, each with cmdline.txt root= rewritten to
  the matching ROOT slot
- Lays out 6 GPT partitions: BF_BOOTSEL (autoboot.txt with tryboot
  pointing at boot_partition=2 / [tryboot] boot_partition=3), BF_BOOT_A,
  BF_BOOT_B, BF_ROOT_A (populated), BF_ROOT_B (empty, RAUC fills on
  first install), BF_DATA
- Recompresses with xz -T0

build-bundle.sh now takes the already-extracted slot images so the
.raucb bundle re-uses the exact same blobs that ship inside the A/B
initial-flash image — no duplication, no drift.

CI wires the repartition step between pi-gen output and the GitHub
Release upload. Ships the A/B image (not the stock pi-gen one).

Also: bump Blacksmith binary builders from 2/4 vCPU to 8 vCPU each.
Image job stays on GitHub's ubuntu-24.04-arm (Blacksmith arm kernel
6.5 doesn't ship binfmt_misc as a loadable module, which pi-gen-action's
defensive modprobe step still requires).

What's still pending:
  - In-image RAUC install (rauc package + drop system.conf + CA cert
    at /etc/rauc/keyring.pem). Without this, the image boots A/B-laid-
    out but rauc install commands have no daemon to talk to.
  - Admin UI for OS releases + rollouts (task #4).
2026-05-21 10:57:00 +02:00
Mitchell R
659670b494
feat(os-ota): kiosk-side RAUC bundle consumer
Phase 3 of the OS OTA pipeline. New module kiosk/src/os_update.rs polls
/api/kiosk/os/check with the kiosk's compatibility string and current OS
version (read from /etc/betterframe/os-compatibility +
/etc/betterframe/os-version, both written by the image build), downloads
the bundle, sha256-verifies the transport, and hands off to
`rauc install`. RAUC takes it from there: CMS signature verify against
/etc/rauc/keyring.pem, copy into inactive A/B slot, arm tryboot via the
custom bootloader backend, return. We then post /api/kiosk/os/applied
and `systemctl reboot` into the new slot.

Wired into the existing 60s heartbeat loop in ui.rs, gated by
BF_ENABLE_OS_OTA=1 (default OFF so dev kiosks on non-A/B images don't
keep trying + failing). Runs BEFORE the kiosk-binary check on each tick
so an OS bundle that ships an updated kiosk binary doesn't race the
firmware path.

On clean-boot heartbeat success we now also call `rauc status
mark-good` so the boot-attempts counter resets — three bad boots in a
row will auto-roll back without us needing a separate rollback path.

What's NOT in this commit:
  - A/B partition layout in the pi-gen image (task #6, blocks actual
    deployment — bundles can be served + accepted but `rauc install`
    will refuse without two valid slots).
  - Admin UI for managing releases + rollouts (task #4).
2026-05-21 10:47:45 +02:00
Mitchell R
084c119c44
feat(os-ota): build + sign + auto-import .raucb bundles in CI
Phase 1 of the OS OTA pipeline. Three pieces:

scripts/gen-rauc-signing-keys.sh — one-shot helper that issues an
Ed25519 X.509 CA + signing cert pair. Operator runs locally, commits
the CA cert (for embedding in kiosk image at /etc/rauc/keyring.pem),
stores the signing pair as GitHub Actions secrets
(BF_RAUC_SIGNING_CERT + BF_RAUC_SIGNING_KEY), keeps the CA private
key offline. RAUC verifies bundles against the keyring in the image.

deploy/rauc/build-bundle.sh — takes the pi-gen .img.xz, parses its
partition table with sfdisk, dd-extracts bootfs (vfat) + rootfs
(ext4) into a staging dir, renders manifest.raucm.in with version
+ git sha, runs `rauc bundle --cert= --key=` to produce a signed
.raucb. Verifies the bundle round-trips with `rauc info`.

build.yml gains two gated steps:
  - "Build RAUC bundle": runs only when both signing secrets are set,
    uploads .raucb as a release asset alongside the .img.xz.
  - "Auto-import OS bundle into BF server": POSTs the GH release asset
    URL to ${BF_AUTOIMPORT_URL}/api/admin/os/import so the server
    pulls + stores the bundle. Mirrors the kiosk-binary auto-import
    flow that already worked.

Compatibility string is `betterframe-rpi5-aarch64` (matches the value
already declared in deploy/rauc/system.conf). Channel passed through
from inputs (dev for master pushes, stable/beta for tags).

What's NOT in this commit:
  - Pi image A/B partition layout (custom genimage / pi-gen patch)
  - rauc package install + keyring drop in pi-gen stage
  - Kiosk-side os_update.rs Rust consumer that polls /api/kiosk/os/check
  - Admin UI for releases + rollouts

A bundle built today reaches /api/admin/os/import on the server but
isn't installable yet — kiosks have no consumer and no A/B layout.
That's the next 3 phases. Bundle production needs to be solid first
so the kiosk side can be tested against real artifacts.
2026-05-21 10:44:24 +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
88bbe040e5
feat(image): enable kiosk-app OTA by default in pi-gen build
BF_ENABLE_APP_OTA gates the periodic firmware check in ui.rs:449.
Our curated image is the place where it should be on — operator can
flip to 0 in /etc/default/betterframe-kiosk to pin a specific build.
Without this, a fresh image never picked up new dev releases even when
the auto-import secrets were configured.
2026-05-21 10:23:02 +02:00
Mitchell R
e26af6c0cc
fix(kiosk): hide cursor inside WebViews + at compositor level
WebView showed a grey/black half-square in the top-left — that's GDK's
"none" cursor rendered by WebKit's own surface, which ignores the
GTK-window CSS we set elsewhere. Inject a WebKit UserStyleSheet that
applies cursor:none !important to every page + frame at User priority,
overriding page-author CSS.

For the boot gap (cage start → first kiosk frame), set XCURSOR_SIZE=1
and WLR_NO_HARDWARE_CURSORS=1 in the systemd unit. SW fallback honors
the 1-pixel size; HW cursors don't, which is why a default arrow leaks
through on some Pi GPUs.
2026-05-21 10:21:17 +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
5d225f7b49
fix(kiosk): install gstreamer1.0-gtk4 for paintablesink
Kiosk's pipeline.rs needs the gtk4paintablesink element to bridge
GStreamer → GTK4 Picture. Plugin ships in Debian package
gstreamer1.0-gtk4 (separate from plugins-base/good/bad). Without it
ElementFactory::make returns Err → create_camera_pipeline returns None
→ ensure_warm returns None → every camera cell renders
"(no stream)". Add to both the pi-gen image stage and the
setup-pi-kiosk.sh installer.
2026-05-21 09:43:19 +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
251b076b99
fix(ci): bump patch for all release channels 2026-05-21 02:02:20 +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
6995990aca
fix(ci): avoid auto-import argv limit 2026-05-20 05:21:16 +02:00
Mitchell R
08568e52fa
feat(kiosk): harden field image defaults 2026-05-20 05:18:18 +02:00
Mitchell R
87cde93316
feat(ota): add RAUC OS update foundation 2026-05-20 05:15:29 +02:00
Mitchell R
444bb4c116
feat(firmware): allow env import token 2026-05-20 05:02:12 +02:00
Mitchell R
d4abc86999
fix(kiosk): allow server discovery 2026-05-20 04:54:15 +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
4e652c6fd1
fix(pi-gen): resolve files/ from sub-stage dir, not BASE_DIR
\${BASE_DIR} is pi-gen's own checkout (/pi-gen), not the path of the
custom stage. Resolve files/ relative to the script's own location.
2026-05-20 02:57:41 +02:00
Mitchell R
04d40adb93
ci(pi-gen): stage files into chroot via host-side 00-run.sh
Pi-gen doesn't auto-copy a sub-stage's files/ dir into the chroot. The
chroot script's install commands were reaching for /tmp/bf-files/... which
never existed. Add a host-side 00-run.sh that bulk-copies files/* into
ROOTFS_DIR/tmp/bf-files, then rename the chroot script to 01-run-chroot.sh
so it sorts AFTER the host copy ('-' < '.' bites you otherwise).
2026-05-20 02:31:35 +02:00
Mitchell R
771b94d387
ci(pi-gen): swap to GitHub ubuntu-24.04-arm runner + pi-gen-action v1.11.0
Blacksmith arm64 runner kernel 6.5 ships binfmt_misc built-in (not modular),
so pi-gen-action@v1's modprobe step errors. GitHub's ubuntu-24.04-arm has
modular binfmt_misc and is the configuration #179 confirms works.
2026-05-20 01:59:40 +02:00
Mitchell R
e19dedfe17
ci(pi-gen): native arm64 runner skips QEMU entirely
usimd/pi-gen-action#179: Trixie + QEMU breaks on x86 runners (arch-test
"arm64: not supported"). Native arm64 runner means no qemu, no binfmt
registration dance — pi-gen runs the chroot directly. Faster too.
2026-05-20 01:42:30 +02:00
Mitchell R
5e1f8f80bc
ci(pi-gen): apt qemu-user-static instead of tonistiigi (chroot needs static binary)
tonistiigi/binfmt registers /usr/bin/qemu-aarch64 (dynamic). Even with F-flag
preload, qemu still dlopen's its libs at exec time — fails inside pi-gen's
chroot. Debian's qemu-user-static ships /usr/bin/qemu-aarch64-static and
post-install sets F flag automatically. Pi-gen's dependencies_check needs
the static path.
2026-05-20 01:32:05 +02:00
Mitchell R
39fac39bf2
ci(pi-gen): verbose-output: true to surface pi-gen stdout/stderr
Pi-gen container exits in 0.288s after image build with no logs printed.
Default action input verbose-output=false suppresses pi-gen output;
flipping to true should show what build.sh trips on inside the container.
2026-05-20 01:13:19 +02:00
Mitchell R
d608609ec4
ci(pi-gen): drop extra-host-dependencies (caused sudo apt exit 100)
Log diagnosis on run 26130391965:
  ##[error]The process '/usr/bin/sudo' failed with exit code 100

Failure was inside the action's 'Installing build dependencies on host'
step. extra-host-dependencies: qemu-user-static binfmt-support broke
apt — possibly conflicting locks or the action's input handling.

tonistiigi/binfmt --install arm64 already registered qemu-aarch64 with
'flags: POCF' (F = kernel-resident static binary). That's enough; no
need for the inside-container qemu packages.
2026-05-20 01:05:49 +02:00
Mitchell R
d342a3779f
ci(pi-gen): bring back extra-host-dependencies + binfmt diagnostic 2026-05-20 00:58:02 +02:00