21 KiB
RadiantWave Kiosk Update System Plan
Goal: Bulletproof automatic update system for RadiantWave kiosk
Deadline: Production in 2 days
Architecture: Manifest-based app updates + Flake lock system updates + systemd service supervision
Timezone: UTC (all kiosks run in UTC)
Variants: All variants (production + dev) auto-update
Notifications: Logs only (no email/Slack)
Executive Summary
This plan implements a three-part update system:
- Kiosk App Supervision: Convert the kiosk app from exec'd login shell to a supervised systemd user service with automatic restart
- RadiantWave App Updates: Hourly manifest checks trigger updates via Attic binary cache
- System Updates: NixOS updates on boot and nightly at 8PM UTC
All updates show Plymouth splash during transition and auto-rollback on failure.
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ BOOT SEQUENCE │
├─────────────────────────────────────────────────────────────────┤
│ 1. Plymouth splash (held indefinitely) │
│ 2. getty autologin as kiosk on tty1 │
│ 3. loginShellInit runs: │
│ a. Set environment variables │
│ b. Wait for network (up to 10s) │
│ c. Start radiantwave-audio-init user service │
│ d. Start kiosk-app.service (systemd user service) │
│ e. Quit Plymouth │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ KIOSK APP SERVICE │
├─────────────────────────────────────────────────────────────────┤
│ kiosk-app.service (systemd user service) │
│ ├─ Runs: dbus-run-session cage -- radiantwave -d -s │
│ ├─ Restart=always, RestartSec=5 │
│ ├─ Logs to /tmp/kiosk.log │
│ └─ On failure: auto-restart (3 attempts) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ UPDATE CHECKERS │
├─────────────────────────────────────────────────────────────────┤
│ radiantwave-update-checker.service (hourly) │
│ ├─ Checks: https://attic.radiantwavetech.com/manifest.json │
│ ├─ If new version: trigger radiantwave-upgrade.service │
│ └─ Runs as: root (system service) │
│ │
│ system-update-checker.service (on boot + 8PM UTC) │
│ ├─ Triggers: nixos-upgrade.service │
│ └─ Runs as: root (system service) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ UPGRADE PROCESS │
├─────────────────────────────────────────────────────────────────┤
│ radiantwave-upgrade.service (triggered by update checker) │
│ ├─ 1. Show Plymouth splash │
│ ├─ 2. Stop kiosk-app.service │
│ ├─ 3. Update flake.lock (git pull) │
│ ├─ 4. Run nixos-rebuild switch │
│ ├─ 5. Restart kiosk-app.service │
│ ├─ 6. Quit Plymouth │
│ └─ 7. Health check (if fails: auto-rollback) │
└─────────────────────────────────────────────────────────────────┘
Component Details
1. Kiosk App Systemd Service
File: kiosk-app.nix (new module)
Purpose: Supervise the cage/radiantwave process with automatic restart
Key Configuration:
{ config, lib, pkgs, ... }:
{
# System service (runs as root, starts user service)
systemd.services.kiosk-app = {
description = "RadiantWave Kiosk Application";
after = [ "graphical.target" ];
wantedBy = [ "graphical.target" ];
# Start the user service
serviceConfig = {
ExecStart = "${pkgs.systemd}/bin/systemctl --user start kiosk-app-session.service";
ExecStop = "${pkgs.systemd}/bin/systemctl --user stop kiosk-app-session.service";
Type = "oneshot";
RemainAfterExit = true;
};
};
# User service (actual cage/radiantwave process)
systemd.user.services.kiosk-app-session = {
description = "RadiantWave Kiosk Session";
after = [ "graphical-session.target" ];
serviceConfig = {
ExecStart = "${pkgs.dbus}/bin/dbus-run-session ${pkgs.cage}/bin/cage -- ${config.radiantApp}/bin/radiantwave -d -s";
Restart = "always";
RestartSec = 5;
# Logging
StandardOutput = "journal";
StandardError = "journal";
# Environment
Environment = [
"GODEBUG=cgocheck=0"
"WAYLAND_DISPLAY=wayland-0"
"XDG_RUNTIME_DIR=/run/user/1000"
];
};
# Restart policy
unitConfig = {
StartLimitIntervalSec = 60;
StartLimitBurst = 3;
};
};
}
Changes to configuration.nix:
- Remove the
execline fromenvironment.loginShellInit - Add
systemctl --user start kiosk-app-session.serviceinstead - Keep Plymouth quit after service start
Updated loginShellInit:
environment.loginShellInit = ''
# ... existing env setup and network wait ...
# Start audio init
systemctl --user start radiantwave-audio-init.service
# Start kiosk app service
systemctl --user start kiosk-app-session.service
# Wait a moment for cage to start
sleep 2
# Quit Plymouth
sudo -n plymouth quit --wait
# Keep shell alive (don't exec)
# The systemd service manages the kiosk app
'';
2. Attic Cache Configuration
File: attic-cache.nix (new module)
Purpose: Configure Nix to use Attic as a binary cache
Key Configuration:
{ config, lib, pkgs, ... }:
{
# Add Attic to trusted caches
nix.settings = {
trusted-caches = [
"https://attic.radiantwavetech.com/radiantwave"
];
# Extra substituters (for pulling)
extra-substituters = [
"https://attic.radiantwavetech.com/radiantwave"
];
# Public keys (Attic uses Ed25519)
trusted-public-keys = [
"radiantwave:PLACEHOLDER_PUBLIC_KEY"
];
};
# Ensure nix can use the cache without authentication
# (Attic is configured as public/open)
}
Note: Replace PLACEHOLDER_PUBLIC_KEY with the actual Attic public key after server setup.
3. RadiantWave Update Checker
File: radiantwave-update-checker.nix (new module)
Purpose: Check Attic manifest hourly for new RadiantWave versions
Key Configuration:
{ config, lib, pkgs, ... }:
{
systemd.services.radiantwave-update-checker = {
description = "Check for RadiantWave updates";
# Run hourly
startAt = "hourly";
serviceConfig = {
Type = "oneshot";
# Check manifest and trigger upgrade if needed
ExecStart = pkgs.writeScript "check-radiantwave-update" ''
#!/bin/bash
set -euo pipefail
MANIFEST_URL="https://attic.radiantwavetech.com/manifest.json"
CURRENT_VERSION_FILE="/etc/radiantwave-version"
# Fetch manifest
MANIFEST=$(curl -sf "$MANIFEST_URL" || exit 0)
# Extract version
NEW_VERSION=$(echo "$MANIFEST" | ${pkgs.jq}/bin/jq -r '.version')
# Get current version
if [ -f "$CURRENT_VERSION_FILE" ]; then
CURRENT_VERSION=$(cat "$CURRENT_VERSION_FILE")
else
CURRENT_VERSION="0.0.0"
fi
# Compare versions
if [ "$NEW_VERSION" != "$CURRENT_VERSION" ]; then
echo "Update available: $CURRENT_VERSION -> $NEW_VERSION"
# Trigger upgrade
systemctl start radiantwave-upgrade.service
else
echo "Already up to date: $CURRENT_VERSION"
fi
'';
};
};
# Upgrade service (triggered by checker)
systemd.services.radiantwave-upgrade = {
description = "Upgrade RadiantWave";
serviceConfig = {
Type = "oneshot";
# Show Plymouth during upgrade
ExecStartPre = "${pkgs.plymouth}/bin/plymouth show-splash || true";
# Stop kiosk
ExecStart = "${pkgs.systemd}/bin/systemctl stop kiosk-app.service";
# Update flake.lock and rebuild
ExecStart = pkgs.writeScript "upgrade-radiantwave" ''
#!/bin/bash
set -euo pipefail
# Update flake.lock
cd ${config.system.build.toplevel} # or git repo path
git pull origin main
# Update radiantwave input
nix flake update radiantwave
# Rebuild and switch
nixos-rebuild switch --flake .#${config.networking.hostName}
# Update version file
NEW_VERSION=$(curl -sf https://attic.radiantwavetech.com/manifest.json | ${pkgs.jq}/bin/jq -r '.version')
echo "$NEW_VERSION" > /etc/radiantwave-version
'';
# Restart kiosk
ExecStart = "${pkgs.systemd}/bin/systemctl start kiosk-app.service";
# Quit Plymouth
ExecStartPost = "${pkgs.plymouth}/bin/plymouth quit --wait || true";
# Rollback on failure
FailureAction = "none"; # Handled by healthcheck
# Timeout
TimeoutStartSec = 300; # 5 minutes for rebuild
};
# Run after update checker
after = [ "radiantwave-update-checker.service" ];
};
}
4. System Update Configuration
File: system-updates.nix (modify existing)
Purpose: Configure NixOS system updates on boot and nightly at 8PM UTC
Changes to configuration.nix:
# Replace existing system.autoUpgrade
system.autoUpgrade = {
enable = true;
flake = upgradeFlakeRef; # e.g., "git+https://gitea...KioskConfig.git#radiantwavekiosk"
# Run on boot and nightly at 8PM UTC (kiosks are in UTC)
dates = "20:00"; # 8PM UTC daily
randomizedDelaySec = "10min"; # Avoid thundering herd
flags = [ "--refresh" "-L" ];
operation = "switch";
};
Note: The existing health check service (radiantwave-healthcheck) already runs after nixos-upgrade.service and handles rollback. Keep this as-is.
5. Update Rollback Mechanism
File: update-rollback.nix (modify existing health check)
Purpose: Auto-rollback on failed updates
Existing mechanism (already in configuration.nix):
systemd.services.radiantwave-healthcheck = {
description = "Health check after upgrade";
after = [ "nixos-upgrade.service" ];
wantedBy = [ "nixos-upgrade.service" ];
serviceConfig = {
Type = "oneshot";
ExecStartPre = "sleep 90"; # Wait for cage to start
ExecStart = pkgs.writeScript "healthcheck" ''
#!/bin/bash
if ! pgrep -f ".radiantwave-unwrapped"; then
echo "Health check failed, rolling back..."
nixos-rebuild switch --rollback
fi
'';
};
};
Enhancement: Add version file tracking and logging:
ExecStart = pkgs.writeScript "healthcheck" ''
#!/bin/bash
set -euo pipefail
LOG_FILE="/var/log/radiantwave-healthcheck.log"
echo "$(date): Starting health check" >> "$LOG_FILE"
# Wait for cage to start
sleep 10
# Check if radiantwave is running
if pgrep -f ".radiantwave-unwrapped" > /dev/null; then
echo "$(date): Health check passed" >> "$LOG_FILE"
# Update version file on success
if [ -f "/tmp/pending-version" ]; then
cp /tmp/pending-version /etc/radiantwave-version
fi
else
echo "$(date): Health check FAILED, rolling back..." >> "$LOG_FILE"
nixos-rebuild switch --rollback 2>> "$LOG_FILE"
echo "$(date): Rollback complete" >> "$LOG_FILE"
fi
'';
6. CI Changes to Push to Attic
File: .gitea/workflows/build-prod.yml (modify)
Purpose: Push built closures to Attic cache
Add to workflow:
jobs:
build:
runs-on: ubuntu-latest
steps:
# ... existing build steps ...
# Push to Attic
- name: Push to Attic cache
run: |
# Install attic-client
nix profile install github:zedant/attic#attic-client
# Login to Attic (token from secrets)
attic login production https://attic.radiantwavetech.com production "${{ secrets.ATTIC_TOKEN }}"
# Push closures
attic push production ./result-kiosk
attic push production ./result-android
attic push production ./result-commercial
# Update manifest
- name: Update manifest
run: |
VERSION=$(date +%Y.%m.%d-%H%M)
STORE_PATH=$(readlink -f ./result-kiosk)
# Create manifest
cat > manifest.json << EOF
{
"version": "$VERSION",
"storePath": "$STORE_PATH",
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"commit": "${{ github.sha }}"
}
EOF
# Upload manifest to Attic or fileserver
# (Attic doesn't support arbitrary file uploads, so use a fileserver or git)
Note: Attic doesn't support arbitrary file uploads. Options for manifest:
- Host on a simple fileserver (nginx, S3, etc.)
- Commit to a git repo and serve via GitHub/Gitea Pages
- Use Attic's labels feature (if available)
Implementation Steps
Phase 1: Attic Server Setup (User's Responsibility)
- Set up Attic server at
attic.radiantwavetech.com - Configure public/open access (no authentication)
- Create cache named
radiantwave - Note the public key for Nix configuration
- Test pushing a closure:
attic push radiantwave /nix/store/...
Phase 2: Manifest Hosting (User's Responsibility)
- Choose manifest hosting location (fileserver, git, S3)
- Create initial manifest with current version
- Ensure manifest is accessible via HTTPS
Phase 3: Kiosk Configuration Changes
- Create
kiosk-app.nixmodule with systemd service - Create
attic-cache.nixmodule for Attic configuration - Create
radiantwave-update-checker.nixmodule - Modify
configuration.nix:- Remove
execfromloginShellInit - Add
systemctl --user start kiosk-app-session.service - Update
system.autoUpgradeschedule
- Remove
- Modify
rotated_display.nixto match changes - Test locally with
nixos-rebuild build
Phase 4: CI Changes
- Add Attic push step to
build-prod.yml - Add manifest update step
- Test CI pipeline with manual trigger
Phase 5: Testing
- Build ISO with changes
- Test on VM or physical hardware
- Verify kiosk starts automatically
- Test manual update trigger
- Test automatic update detection
- Test rollback on failure
- Test Plymouth splash during updates
Testing Checklist
Kiosk App Service
- Kiosk starts automatically on boot
- Kiosk restarts if crashed (kill -9 radiantwave)
- Logs appear in
/tmp/kiosk.logandjournalctl - Audio init completes before kiosk starts
Update Detection
- Manifest check runs hourly
- System update check runs on boot
- System update check runs at 8PM UTC
- Checker logs show current vs available version
Update Process
- Plymouth splash shows during update
- Kiosk stops before rebuild
- Rebuild completes successfully
- Kiosk restarts after rebuild
- Plymouth quits after kiosk starts
- Version file updates on success
Rollback
- Health check runs after upgrade
- Rollback triggers if radiantwave not running
- Rollback completes successfully
- Logs show rollback reason
Network Failure Handling
- Update checker continues if manifest unreachable
- Rebuild continues if Attic unreachable (uses local cache)
- Kiosk restarts even if update fails
File Map (New/Modified)
New Files
kiosk-app.nix- Systemd service for kiosk appattic-cache.nix- Attic binary cache configurationradiantwave-update-checker.nix- Hourly update checkersystem-updates.nix- System update schedule (or modify configuration.nix)
Modified Files
configuration.nix- Remove exec, add service start, update autoUpgrade schedulerotated_display.nix- Match loginShellInit changes.gitea/workflows/build-prod.yml- Add Attic push step.gitea/workflows/build-dev.yml- Add Attic push step (optional)
Rollback Strategy
If the update system itself fails:
- Manual rollback: SSH into kiosk, run
nixos-rebuild switch --rollback - Boot previous generation: Select previous generation in systemd-boot (if timeout enabled)
- Reinstall from ISO: Last resort, use zero-touch installer ISO
Monitoring and Debugging
Logs to Check
/tmp/kiosk.log- Kiosk app outputjournalctl -u kiosk-app.service- Systemd service logsjournalctl -u radiantwave-update-checker.service- Update checker logsjournalctl -u radiantwave-upgrade.service- Upgrade process logs/var/log/radiantwave-healthcheck.log- Health check results
Manual Commands
# Check current version
cat /etc/radiantwave-version
# Check manifest manually
curl -sf https://attic.radiantwavetech.com/manifest.json | jq
# Trigger manual update
systemctl start radiantwave-upgrade.service
# Check kiosk service status
systemctl --user status kiosk-app-session.service
# View kiosk logs
journalctl -u kiosk-app.service -f
# Manual rollback
nixos-rebuild switch --rollback
Risk Assessment
| Risk | Impact | Mitigation |
|---|---|---|
| Attic server unavailable | Updates fail, kiosk continues running | Update checker continues hourly, kiosk unaffected |
| Manifest corrupted | False update trigger | Version comparison logic, checksums |
| Rebuild fails | Kiosk stays on old version | Automatic rollback via health check |
| Kiosk crashes during update | Plymouth stays on screen | Timeout mechanisms, manual intervention |
| Network partition | No updates possible | Kiosk continues on current version |
Success Criteria
- Kiosk starts automatically on boot (no manual intervention)
- Kiosk restarts automatically if crashed
- RadiantWave updates check every hour
- System updates check on boot and 8PM UTC
- Updates show Plymouth splash during transition
- Failed updates auto-rollback within 2 minutes
- All operations logged for debugging
- Zero manual intervention required for routine updates
Timeline
Day 1 (Today):
- Set up Attic server
- Create manifest hosting
- Implement kiosk-app.nix
- Implement attic-cache.nix
- Test kiosk service locally
Day 2 (Tomorrow):
- Implement update checker modules
- Update CI workflows
- Build test ISO
- Test on hardware
- Deploy to production
Questions for Clarification
Where will the manifest file be hosted?→ Fileserver (nginx/Caddy)What timezone is the kiosk in?→ UTC (all kiosks)Should dev variants also auto-update?→ Yes, all variants- Do you want a manual "check for updates" button in the kiosk UI?
Should we notify admins of failed updates?→ Logs only