3 Commits
Author SHA1 Message Date
NeilClack 809882b333 Merge branch 'dev'
build / nix-build (push) Successful in 10m59s
2026-10-14 15:03:33 -05:00
NeilClack 0124e1c67b Update README 2026-10-14 15:03:26 -05:00
NeilClack 476202f8e2 Merge dev into main: Bug Fix - ScheduleManager startup self-deadlock
build / nix-build (push) Successful in 9m50s
Bug Fix: ScheduleManager::start() held mutex_ and then called loadCache(), which re-locks the same non-recursive mutex. Whenever a schedule cache file was present (the normal state for a provisioned kiosk), this self-deadlocked and blocked SDL_AppInit before the first frame was presented, so the kiosk booted to a black screen showing only the compositor cursor.

Narrow the lock scope so loadCache() acquires mutex_ itself, and add a regression test that starts with a cached payload present.
2026-10-07 08:58:46 -05:00
+143 -28
View File
@@ -10,6 +10,149 @@ and manages WiFi/provisioning on a headless Linux kiosk.
**Audio:** SDL3_mixer
**Target:** Linux kiosk (Wayland); developed on macOS
---
## Version Deployment
> How to release a new version of RadiantWave, and what happens on the kiosks when you do.
### Two channels, two update paths
| Channel | App input | Attic cache | Manifest |
|---|---|---|---|
| `dev` | `radiantwave-dev` (`?ref=dev`) | `attic.../radiantwave-dev` | `files.../radiantwave-dev/manifest.json` |
| `main` | `radiantwave` (`?ref=refs/tags/1.8`) | `attic.../radiantwave` | `files.../radiantwave/manifest.json` |
A kiosk gets RadiantWave by two independent paths:
- **Live app update** -- CI publishes a manifest; the kiosk's
`radiantwave-updater.timer` fetches the closure from Attic and swaps
`/var/lib/radiantwave/current`. Takes effect in ~2 min, no rebuild.
- **NixOS rebuild** -- `system.autoUpgrade` runs every 10 min against the
`KioskConfig` repo with `--refresh` and rebuilds the whole system closure.
`KioskConfig` enables `services.radiantwaveUpdater` on all four configs on
**both** branches, so the live path is always available.
**An app-only feature only needs the live app update path. You do not commit,
push, or rebuild `../KioskConfig`.** Its `flake.lock` pin of `radiantwave-dev`
only determines the closure baked into the ISO; the updater overwrites that
pointer as soon as it sees a newer manifest.
### Test a feature on the dev kiosk -- manual steps
**1. Build and test locally.**
```sh
nix develop --command bash -c '
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/test_assets
'
```
Run each `./build/test_<name>` binary you touched -- there is no ctest
integration and no aggregate target. If you added a test file, register it as
a new `add_executable` in `CMakeLists.txt` first.
**2. Land it on `dev`.**
```sh
git switch dev && git pull
git switch -c feature/<name>
# ... commit your work ...
git push -u origin feature/<name>
```
Open a PR against `dev` and merge. The kiosk only ever builds `dev`.
**3. Dispatch the build workflow** on the RadiantWave repo in Gitea Actions.
**4. Verify.**
The Home footer's Version row shows the new short commit hash, and:
```sh
ssh kiosk@<dev-kiosk> 'readlink /var/lib/radiantwave/current'
curl -fsSL https://files.radiantwavetech.com/radiantwave-dev/manifest.json | jq .storePath
```
Both must match.
### Test a feature on the dev kiosk -- automated by CI
> Everything between dispatching `build.yml` and the kiosk running the new
> binary. No human step is required for any of it.
>
> **Trigger.** `workflow_dispatch` force-clones the `dev` branch
> (`REF=dev`, hardcoded -- you cannot pick a branch). A tag push clones the tag.
> Either way `APP_VERSION` becomes the short commit hash on dispatch, or
> `git describe --tags --always` on a tag.
>
> **Build.** Native `x86_64-linux` package, then the offline suite via
> `checks.<system>.default`, then the live portal tests from `nix develop`
> (`RWT_LIVE_*` secrets), then the `aarch64-linux` package and checks. The
> aarch64 build is emulated via binfmt/qemu-user and is the long pole.
>
> **Publish.** `attic push radiantwave:radiantwave-dev` with both
> `result-pkg` and `result-pkg-aarch64`, using `ATTIC_TOKEN_DEV`. Then it
> writes `{"version","storePath"}` from `readlink -f` of each result into
> `manifest.json` (x86) and `aarch64-manifest.json` (arm), and `scp`s both to
> `157.245.124.103:/mnt/infrastructure_data/files/radiantwave-dev/`, which is
> served as `https://files.radiantwavetech.com/radiantwave-dev/`.
>
> **Kiosk side.** `radiantwave-updater.timer` (`OnBootSec=2min`,
> `OnUnitActiveSec=2min`, `Persistent`) runs the oneshot
> `radiantwave-updater.service`, which curls the manifest, `jq`s `.storePath`,
> and compares it against `readlink /var/lib/radiantwave/current`. On a new
> path it fetches the closure, never building:
>
> ```sh
> nix-store -r "$NEW_PATH" \
> --add-root /var/lib/radiantwave/.current-root \
> --option extra-substituters "<atticUrl> https://cache.nixos.org"
> ```
>
> Then it swaps atomically with `ln -sfn` plus `mv -T`. Both Attic caches and
> their public keys are already trusted in `configuration.nix`, so the kiosk
> needs no credentials.
>
> **Failure mode.** The Attic push and manifest publish are ordinary steps, not
> `always()`, so **any failing test blocks publication** and the kiosk keeps
> running the previous build. Check the run's log before assuming a deploy
> landed.
### Gotchas
- **Dispatch hardcodes `dev`.** `build.yml` sets `REF=dev` on
`workflow_dispatch`; you cannot select a branch.
- **Push before you dispatch.** CI clones `dev` at dispatch time, so dispatching
first publishes the previous commit and the feature appears not to deploy.
- **Reboot briefly reverts the app.** `radiantwave-updater.nix` seeds the
pointer with tmpfiles type `L`, which re-points `current` at the ISO's baked
baseline on every boot. The 2-minute timer restores the live build shortly
after, so the window is brief but real.
- **A reboot does not lose kiosk data.** `radiantwave-backup.service` rsyncs
`~/.local/radiantwave` (minus `audio/`) before every upgrade attempt.
- **Don't bump `channel.appVersion` for an app-only deploy.** It only labels
the baked ISO baseline. Bump it when rebuilding a dev ISO so it advertises
the new fingerprint.
- **No local Linux builder on macOS.** `extra-platforms = x86_64-darwin` only,
so `nix build .#packages.x86_64-linux.default` and
`nixos-rebuild switch --target-host` are not available from a Mac. The
`orion-lxc` runner is the only `x86_64-linux` builder.
### Releasing to production
1. Merge `dev` -> `main` in `radiantwave`.
2. Tag and push: `git tag 1.9 && git push origin 1.9`. The tag push triggers
`build.yml`, which publishes to the `radiantwave` Attic cache and
`radiantwave/manifest.json` using `ATTIC_TOKEN_PROD`.
3. In `KioskConfig`, on `main`: set `radiantwave.url` to
`?ref=refs/tags/1.9`, set `channel.appVersion = "1.9"`, and run
`nix flake update radiantwave`. Commit both -- the lock and the channel
block must agree.
4. Its CI fails fast unless the baked baseline exactly matches the published
manifest, so this step cannot drift silently.
---
## Project Layout
@@ -564,34 +707,6 @@ status to decide which page to show.
---
## Rebuild Progress
| Component | Status |
| ----------- | -------- |
| Entry / Event Loop | Done |
| Window + Renderer | Done |
| Page System | Done |
| PageManager (stack) | Done |
| KeybindManager | Done |
| Home Page | Done |
| Settings Page | Done |
| Provisioning FSM | — |
| Pages (11 types) | — |
| OpenGL Context | — |
| Virtual Framebuffer | — |
| Shader Manager | — |
| Font Manager | — |
| Line Patterns | — |
| Color Patterns | — |
| Mixer | — |
| Network / WiFi | — |
| Database (SQLite) | — |
| HTTP Client | — |
| Logging | — |
| Config / App State | — |
---
## Common Pitfalls
- Do not commit audio assets (`.wav` files) -- downloaded at runtime.