The radiantwave 1.8 tag was force-moved from 34c72b5 to a5a6b5b while flake.lock still pinned the old commit, so the ISO baked a baseline that was not the closure RadiantWave CI published to Attic. With version 1.8 overridden this could still compile RadiantWave from source instead of substituting it. - Update flake.lock so radiantwave?ref=refs/tags/1.8 resolves to a5a6b5b. - Expose channelVersion/channelAtticUrl/channelManifestDir/baselineOutPaths from flake.nix for CI verification. - Prod workflows: fail fast unless channel.appVersion equals the channel manifest version, baselineOutPaths.<system> equals the manifest storePath, and that store path exists in the channel's Attic cache.
122 lines
9.8 KiB
Markdown
122 lines
9.8 KiB
Markdown
# AGENTS.md
|
|
|
|
## What this repo is
|
|
|
|
NixOS flake that defines kiosk systems running the RadiantWave app. Not application code — this is the OS image and deployment configuration. The actual app lives in the `radiantwave` flake inputs (from gitea.radiantwavetech.com).
|
|
|
|
Hosted on Gitea: `https://gitea.radiantwavetech.com/RadiantWaveTech/KioskConfig`
|
|
|
|
## Prod/dev split
|
|
|
|
Two branches — `main` (prod) and `dev`. There are **no `-dev` configs**: each hardware target has exactly one config, and the branch selects its channel via the single `channel` block at the top of `flake.nix`:
|
|
|
|
- `main`: `channel.appInput = radiantwave` (pinned to tag `1.8`), `atticRepo = "radiantwave"`, `gitRef = ""` (default branch), `appVersion = "1.8"`.
|
|
- `dev`: `channel.appInput = radiantwave-dev`, `atticRepo = "radiantwave-dev"`, `gitRef = "?ref=dev"`, `appVersion = <dev fingerprint>`.
|
|
|
|
Everything downstream (the configs, ISOs, updater module, autoUpgrade refs) reads from `channel`, so a `dev`→`main` merge carries your changes automatically — no copying between sibling configs. To flip a channel, change only those four `channel` values. When merging, reconcile just this one block.
|
|
|
|
`channel.appVersion` is applied to the baked app via `appPackage = system: channel.appInput.packages.${system}.default.override { version = channel.appVersion; }`. RadiantWave's own flake only injects its version from the ambient `APP_VERSION` env var on impure evals, so pure evals (local `nix build`, `build-rpi.sh`) fall back to `0.1.0`. Declaring the version here keeps local and CI builds correct without `--impure`. Bump `appVersion` whenever the pinned input changes (release tag on `main`, short commit hash on `dev`).
|
|
|
|
The baked baseline must be **byte-identical to the closure RadiantWave CI publishes to Attic** — that is what lets the ISO/`sdImage` substitute RadiantWave instead of compiling it from source, and makes the first live update a no-op. The prod workflows assert this in "Verify baked baseline matches the published channel": it compares `channelVersion` to the manifest's `version`, `baselineOutPaths.<system>` to the manifest's `storePath`, and confirms that store path exists in the channel's Attic cache. If it fails, the usual cause is a **moved release tag**: `radiantwave` is pinned by tag ref in `flake.lock`, so re-tagging `1.8` (as happened when it moved from `34c72b5` to `a5a6b5b`) leaves the lock on the old commit until you run `nix flake update radiantwave`. The RadiantWave flake also includes `.gitea/` in the app source, so its CI workflow changes alter the derivation hash.
|
|
|
|
Both gitea repos are public: all git access is plain HTTPS (`git+https://...`), no SSH keys or access tokens on the kiosks. Don't reintroduce `git+ssh` URLs in flake/upgrade refs.
|
|
|
|
## Key commands
|
|
|
|
```bash
|
|
# Build a system closure (any config name from flake.nix)
|
|
nix build .#nixosConfigurations.<name>.config.system.build.toplevel
|
|
|
|
# Build a bootable ISO
|
|
nix build .#nixosConfigurations.<name>.config.system.build.isoImage
|
|
|
|
# Deploy to a remote kiosk over SSH
|
|
nixos-rebuild switch --flake .#<name> --target-host root@<host>
|
|
|
|
# Push build result to Attic cache (not currently wired into any config)
|
|
attic push kiosk ./result
|
|
|
|
# Evaluate available config names
|
|
nix eval .#nixosConfigurations --apply builtins.attrNames
|
|
```
|
|
|
|
## Config variants
|
|
|
|
One config per hardware target. Dev/prod is the branch, not the config — see `channel` in `flake.nix`:
|
|
|
|
| Name | Target hardware | Disk device | App input |
|
|
|---|---|---|---|
|
|
| `radiantwavekiosk` | Intel NUC 14 (NVMe) | `/dev/nvme0n1` | `radiantwave` (tag 1.8) |
|
|
| `radiantwavekiosk-commercial` | Commercial HW (SATA) | `/dev/sda` | `radiantwave` (tag 1.8) |
|
|
| `radiantwavekiosk-android` | Intel NUC 14, rotated display | `/dev/nvme0n1` | `radiantwave` (tag 1.8) |
|
|
| `radiantwavekiosk-rpi` | Raspberry Pi 5 (SD image) | — | `radiantwave` (tag 1.8) |
|
|
|
|
On the `dev` branch, the same four configs use `radiantwave-dev` and track `?ref=dev`.
|
|
|
|
Every variant sets its own `networking.hostName` (same as the config name) and `system.configurationRevision`, so `tools/find_systems.zsh` can identify fleet members.
|
|
|
|
### USB images (zero-touch installers)
|
|
|
|
Each ISO installs exactly one closure using that closure's disk layout (`mkIso` in `flake.nix` derives both from the target config name):
|
|
|
|
| ISO | Installs | Disk layout |
|
|
|---|---|---|
|
|
| `kiosk-iso` | `radiantwavekiosk` | NVMe (`disk-config.nix`) |
|
|
| `commercial-iso` | `radiantwavekiosk-commercial` | SATA (`commercial-disk-config.nix`) |
|
|
| `android-iso` | `radiantwavekiosk-android` | NVMe |
|
|
|
|
These same names build **dev-flavored** images when the flake is checked out on the `dev` branch (the channel block selects the dev app, attic repo, and upgrade ref), so there are no separate `-dev` ISOs.
|
|
|
|
The installer lives once in `zero-touch-installer.nix` and is parameterized by `targetConfig`. Adding a new variant = add the `nixosConfigurations.*` entry, a matching `packages.x86_64-linux.*` line, and an ISO if needed.
|
|
|
|
Raspberry Pi systems are built as SD images, not ISOs:
|
|
`nix build .#nixosConfigurations.radiantwavekiosk-rpi.config.system.build.sdImage` (see `build-rpi.sh`).
|
|
|
|
## Hardware facter reports
|
|
|
|
`facter.json` is a hardware report captured on a **NUC 14**. It is currently applied to **every x86 variant**, including the commercial SATA hardware — acceptable until per-model reports exist.
|
|
|
|
To generate a report for a hardware model:
|
|
|
|
1. Boot that hardware from any NixOS live image (the kiosk installers work).
|
|
2. With real peripherals attached (display(s), audio), run:
|
|
```bash
|
|
sudo nix shell nixpkgs#nixos-facter -c nixos-facter -o facter-<model>.json
|
|
```
|
|
3. Commit `facter-<model>.json` to the repo root.
|
|
4. Wire it up at the marked spot in `flake.nix` (`facter.reportPath` inside `mkKioskSystem`) by selecting the right file per variant.
|
|
|
|
Regenerate when major hardware changes, not on a schedule — reports are snapshots. RPi configs don't use the facter module, so no report is needed there.
|
|
|
|
## CI (Gitea)
|
|
|
|
`.gitea/workflows/build-*.yml` — all triggered by `workflow_dispatch` on the `build-server` runner. They only **build and publish** what is committed. `flake.lock` is updated manually: commit an updated lock (e.g. after `nix flake update radiantwave-dev`) and dispatching the workflow is the release trigger. The prod workflows run a "Verify baked baseline matches the published channel" preflight that fails the build if the baked closure diverges from the channel manifest or is missing from Attic (see the `channel` block and `baselineOutPaths` in `flake.nix`).
|
|
|
|
## Important gotchas
|
|
|
|
- **Disk device differs by variant**: `disk-config.nix` uses `/dev/nvme0n1`, `commercial-disk-config.nix` uses `/dev/sda`. `mkIso` pairs each ISO with the correct layout automatically — keep new ISOs paired correctly.
|
|
- **Changing a config name** requires updating that config's `upgradeFlakeRef` in `flake.nix` (it embeds the name).
|
|
- **The `channel` block is the dev/prod toggle** in `flake.nix` — `appInput`, `atticRepo`, `gitRef`, `appVersion`. When merging `dev`→`main`, keep this block's values for the target branch; only this block differs between branches.
|
|
- **`rotated_display.nix` uses `mkForce`** on `loginShellInit` — it replaces the entire kiosk startup script from `configuration.nix`; don't edit both.
|
|
- **Auto-update**: `system.autoUpgrade` runs every 10 min (`*:00/10`) with `--refresh`, operation `switch`, no auto-reboot. A `radiantwave-backup` rsync runs before each attempt. The health-check/rollback service is written but commented out in `configuration.nix`.
|
|
- **Live app updater**: each config's `radiantwaveUpdaterModule` points at a per-arch manifest — x86 uses `<repo>/manifest.json`, RPi uses `<repo>/aarch64-manifest.json` (via the `manifestFile` arg). A 2-min timer polls it and fetches the announced closure from the channel's Attic repo. RPi live updates require RadiantWave CI to publish both an aarch64 closure and `aarch64-manifest.json`; until then the RPi stays on the baked baseline.
|
|
- **No binary cache**: kiosks fetch from cache.nixos.org only and compile RadiantWave itself from source during upgrades. Attic integration was planned (see `PLAN-update-system.md`) but never wired; the old netrc/JWT setup has been removed.
|
|
- **Secrets**: none in-tree anymore (gitea repos are public). If either repo goes private, re-add auth via `nix.settings.access-tokens` in `configuration.nix`.
|
|
- **Flake inputs**: `radiantwave` is pinned to tag `1.8` (`?ref=refs/tags/1.8`); `radiantwave-dev` tracks `?ref=dev`. The `channel` block picks which one the configs consume. Bump the pinned tag and `channel.appVersion` together. **Tag refs are live**: if RadiantWave force-moves a tag, the lock keeps the old commit until `nix flake update radiantwave`, so the baked baseline silently diverges from the published channel (the CI preflight catches this).
|
|
- **SSH keys on machines are inbound-only** (`authorizedKeys` for admin access); outbound git is HTTPS. The root key in `zero-touch-installer.nix` must stay in sync with `myKey` in `configuration.nix`.
|
|
|
|
## File map
|
|
|
|
- `flake.nix` — entry point: inputs, the `channel` dev/prod selector, builders (`mkKioskSystem`, `mkRpiKioskSystem`, `mkIso`), all system definitions, facter report wiring
|
|
- `configuration.nix` — shared NixOS config (packages, services, kiosk user, auto-upgrade, health check, VM variant)
|
|
- `zero-touch-installer.nix` — parameterized USB installer module used by all three ISOs
|
|
- `disk-config.nix` — NVMe disk layout (NUC)
|
|
- `commercial-disk-config.nix` — SATA disk layout (commercial HW)
|
|
- `rotated_display.nix` — portrait display override for Android variant
|
|
- `facter.json` — hardware facter report (NUC 14)
|
|
- `assets/` — logo and splash image
|
|
- `plymouth-theme/` — boot splash theme
|
|
- `tools/find_systems.zsh` — subnet sweep for fleet inventory over SSH
|
|
- `build-rpi.sh` — containerized RPi SD-image build helper
|
|
- `PLAN-*.md`, `report.md`, `timeline.csv`, `kiosk-url-changes.csv` — planning/history notes, not configuration
|