diff --git a/docs/superpowers/plans/2026-09-23-host-config.md b/docs/superpowers/plans/2026-09-23-host-config.md new file mode 100644 index 0000000..cf87755 --- /dev/null +++ b/docs/superpowers/plans/2026-09-23-host-config.md @@ -0,0 +1,1024 @@ +# Host-local Configuration (`/etc/vanlink.conf`) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the tracked, hand-edited `deploy.conf` with one versioned file per host under `hosts/`, reached through an `/etc/vanlink.conf` symlink, so the Pi 5 and the R5C share one git checkout and cannot drift. + +**Architecture:** `deploy.sh` sources `/etc/vanlink.conf`, validates it, and renders `@TOKEN@` placeholders in every template (generic loop over a variable list). `install_rendered` refuses output that still contains a token. Optional services are gated by `ENABLE_*` flags. A plain-bash test script (`tests/check-templates.sh`) statically enforces the rules that were broken on 2026-09-23. + +**Tech Stack:** bash (`set -euo pipefail`), sed, awk, systemd units, networkd/NetworkManager/hostapd/dnsmasq/nftables configs. No test framework: `tests/check-templates.sh` prints `ok`/`FAIL` lines and exits non-zero on failure. + +**Spec:** `docs/superpowers/specs/2026-09-23-host-config-design.md` + +## Global Constraints + +- Host config path is `/etc/vanlink.conf` (override with env `VANLINK_CONF`); host files are `hosts/.conf`: `hosts/van.conf` (R5C, hostname `van`) and `hosts/wan.conf` (Pi 5, hostname `wan`). +- Host files contain **no secrets** (they are in git and read by root). +- Required variables: `WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE STARLINK_IFACE WIFI_WAN_IFACE GATEWAY_IP GATEWAY_PREFIX GATEWAY_NETWORK DHCP_RANGE_START DHCP_RANGE_END DNS_RESOLVERS MODEM_USB_VENDOR ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE`. +- Optional variables: `ONBOARD_LAN_IFACE NVME_DEVICE NVME_FS_DEVICE USB_BACKUP_ROOT_PART USB_BACKUP_MOUNT SD_BACKUP_UUID`. `NVME_DEVICE` and `NVME_FS_DEVICE` are required only when `ENABLE_NVME_WATCH=1`. +- `ENABLE_*` flags are exactly `0` or `1`. +- `LAN_USB_IFACE` is renamed `LAN_IFACE` everywhere; the JSON key `lan_usb_iface` in `router-status/config.json` is **not** renamed (consumer-facing). +- `deploy.sh` never creates or edits `/etc/vanlink.conf` or `hosts/*.conf`. +- `deploy.sh` `--check`, `--templates`, `--diff` and `render` need no root and install nothing. +- LAN bridge file installs as `/etc/systemd/network/05-van-lan.network` (sorts before netplan's `10-` file). +- Commit messages end with `Co-Authored-By: Claude Sonnet 5 `. +- Do not touch the `.bak` files, `ap/50-van-wan.yaml`'s PSK, or `power/thermal-config.json` (out of scope per spec). + +## File Structure + +| File | Responsibility | +|---|---| +| `deploy.sh` (modify) | load/validate host conf, render, install, flag gating, `--check/--templates/--diff` | +| `hosts/vanlink.conf.example` (create) | documented reference; defines exactly the variables `deploy.sh` knows | +| `hosts/van.conf`, `hosts/wan.conf` (create) | real values for the two hosts | +| `deploy.conf` (delete) | replaced by the above | +| `tests/check-templates.sh` (create) | static checks, run from anywhere, no root | +| `ap/*`, `failover/*`, `router-status/config.json`, `power/nvme-watch-config.json` (modify) | hardcoded host values become `@TOKEN@`s | +| `failover/sync-usb-backup.sh`, `failover/sync-sd-backup.sh` (modify) | read backup settings from the host conf | +| `README.md`, misc comments (modify) | describe the shared model | + +--- + +### Task 1: Config core, host files, test harness + +**Files:** +- Modify: `deploy.sh` (top section, lines 1-47) +- Create: `tests/check-templates.sh`, `hosts/vanlink.conf.example`, `hosts/van.conf`, `hosts/wan.conf` +- Delete: `deploy.conf` +- Modify (rename token): every file containing `LAN_USB_IFACE` except `.bak` and `docs/` + +**Interfaces:** +- Produces (used by all later tasks), in `deploy.sh`: arrays `REQUIRED_VARS`, `OPTIONAL_VARS`, `DERIVED_VARS`, `TOKEN_VARS`; string `TOKEN_RE`; functions `die`, `load_conf `, `render `, `check_rendered `, `install_rendered [mode]`, `template_files`; sub-commands `--check [conf]`, `--templates`, `render `. Derived var `ONBOARD_LAN_UNMANAGED` (`;interface-name:` or empty). +- Produces in `tests/check-templates.sh`: helper functions `ok ` / `bad ` and variable `fail`; later tasks append numbered check sections before the final `exit "$fail"`. + +- [ ] **Step 1: Commit the pending `deploy.sh` fix** + +The working tree has an uncommitted fix from 2026-09-23 (dnsmasq and br0 rendered; LAN file installed as `05-`). + +Run: `git diff --stat` — expected: only `deploy.sh`, ~8 insertions / 3 deletions. + +```bash +git add deploy.sh +git commit -m "deploy: render dnsmasq/br0 configs, install LAN file as 05-van-lan + +van-ap-dnsmasq.conf and 21-van-br0.network carry @TOKEN@ placeholders but +were installed unrendered. Armbian's netplan-generated 10-netplan-* file +matched the LAN port before 22-van-lan.network, so it was never bridged. + +Co-Authored-By: Claude Sonnet 5 " +``` + +- [ ] **Step 2: Write the test script (checks 1, 3, 4) and see it fail** + +Create `tests/check-templates.sh` (mode 0755): + +```bash +#!/usr/bin/env bash +# Static checks for the host-config templating. No root needed. +# ./tests/check-templates.sh +set -euo pipefail +cd "$(dirname "$(readlink -f "$0")")/.." + +fail=0 +ok() { echo "ok - $*"; } +bad() { echo "FAIL - $*"; fail=1; } + +# 1. shell syntax +for f in deploy.sh failover/sync-usb-backup.sh failover/sync-sd-backup.sh tests/check-templates.sh; do + if bash -n "$f"; then ok "bash -n $f"; else bad "bash -n $f"; fi +done + +# 3. every host file passes deploy.sh --check (validates vars, renders all templates) +for c in hosts/*.conf; do + if out=$(./deploy.sh --check "$c" 2>&1); then + ok "--check $c" + else + bad "--check $c"; echo "$out" | sed 's/^/ /' + fi +done + +# 4. the example defines exactly the variables deploy.sh knows about +known=$(sed -n '/^REQUIRED_VARS=(/,/^)/p;/^OPTIONAL_VARS=(/,/^)/p' deploy.sh \ + | grep -vE '^[A-Z_]+=\(|^\)' | tr -s ' \t' '\n' | grep -v '^$' | sort || true) +defined=$(grep -oE '^[A-Z][A-Z0-9_]*=' hosts/vanlink.conf.example 2>/dev/null | tr -d = | sort || true) +if [ -n "$known" ] && [ "$known" = "$defined" ]; then + ok "hosts/vanlink.conf.example defines exactly the variables deploy.sh knows" +else + bad "hosts/vanlink.conf.example differs from deploy.sh's variable lists" + diff <(echo "$known") <(echo "$defined") | sed 's/^/ /' || true +fi + +exit "$fail" +``` + +Run: `chmod +x tests/check-templates.sh && ./tests/check-templates.sh; echo "exit=$?"` +Expected: FAIL lines for `--check hosts/*.conf` (glob matches nothing / no `--check`) and for the example; `exit=1`. + +- [ ] **Step 3: Create the host files** + +`hosts/vanlink.conf.example`: + +```bash +# hosts/vanlink.conf.example — reference for /etc/vanlink.conf. +# +# Each host has hosts/.conf (hostname -s) in this repo, and +# /etc/vanlink.conf is a symlink to it: +# sudo ln -sfn "$PWD/hosts/$(hostname -s).conf" /etc/vanlink.conf +# Plain KEY=value (it is sourced by bash). NO SECRETS — this file is in git. +# Validate without root: ./deploy.sh --check hosts/.conf +# Find interface names with `ip -br link` (wired) / `iw dev` (wifi); USB IDs with lsusb. + +# --- Interfaces ------------------------------------------------------------- +WIFI_5G_IFACE=wlp1s0 # 5GHz AP radio (hostapd) +WIFI_2G_IFACE=wlx000000000000 # 2.4GHz AP radio (hostapd-2g) +LAN_IFACE=enp1s0 # wired LAN port, bridged into br0 +STARLINK_IFACE=enp2s0 # Starlink uplink (NetworkManager WAN) +WIFI_WAN_IFACE=wlan0 # Wi-Fi uplink (NetworkManager WAN) +ONBOARD_LAN_IFACE= # optional: extra onboard LAN port bridged into br0 (e.g. eth0); empty = none + +# --- Addressing --------------------------------------------------------------- +GATEWAY_IP="10.42.9.1" +GATEWAY_PREFIX="24" +GATEWAY_NETWORK="10.42.9.0/24" +DHCP_RANGE_START="10.42.9.10" +DHCP_RANGE_END="10.42.9.254" +DNS_RESOLVERS="1.1.1.1 8.8.8.8" + +# --- Hardware ----------------------------------------------------------------- +MODEM_USB_VENDOR=2c7c # cellular modem USB vendor id +NVME_DEVICE= # only when ENABLE_NVME_WATCH=1, e.g. nvme0 +NVME_FS_DEVICE= # only when ENABLE_NVME_WATCH=1, e.g. /dev/nvme0n1p2 + +# --- Backup (read by failover/sync-*-backup.sh; leave empty if unused) ---------- +USB_BACKUP_ROOT_PART= # sync-usb-backup.sh: partition to mount, e.g. /dev/sda2 +USB_BACKUP_MOUNT= # sync-usb-backup.sh: local mount point, or host:/path for NFS +SD_BACKUP_UUID= # sync-sd-backup.sh: filesystem UUID of the backup SD card + +# --- Feature flags (0 or 1) --------------------------------------------------- +ENABLE_COCKPIT=0 # Cockpit "Van Router" plugin +ENABLE_LI3=0 # Li3 house-battery BLE -> MQTT service +ENABLE_NVME_WATCH=0 # NVMe timeout watchdog (needs an NVMe drive) +ENABLE_HA=0 # Home Assistant + ESPHome (podman quadlets) +ENABLE_FRIGATE=0 # Frigate NVR (podman quadlet) +``` + +`hosts/van.conf` (R5C; values from its current, interface-swapped `deploy.conf`, addressing and flags from what runs there today): + +```bash +# hosts/van.conf — FriendlyElec NanoPi R5C, hostname "van" (Armbian). +# /etc/vanlink.conf -> this file. See hosts/vanlink.conf.example for the variable list. + +WIFI_5G_IFACE=wlp1s0 # MediaTek MT7925E (PCIe); also carries the 2.4GHz AP +WIFI_2G_IFACE=wlx3c3332002066 +LAN_IFACE=enP1p17s0 +STARLINK_IFACE=enP2p33s0 +WIFI_WAN_IFACE=wlan0 # not present on this host (kept for the failover config) +ONBOARD_LAN_IFACE= + +GATEWAY_IP="10.42.1.1" +GATEWAY_PREFIX="24" +GATEWAY_NETWORK="10.42.1.0/24" +DHCP_RANGE_START="10.42.1.10" +DHCP_RANGE_END="10.42.1.254" +DNS_RESOLVERS="1.1.1.1 8.8.8.8" + +MODEM_USB_VENDOR=2c7c +NVME_DEVICE= +NVME_FS_DEVICE= + +USB_BACKUP_ROOT_PART= +USB_BACKUP_MOUNT= +SD_BACKUP_UUID=39118305-4927-47a4-acc7-ba022f37af22 + +ENABLE_COCKPIT=0 +ENABLE_LI3=1 +ENABLE_NVME_WATCH=0 # no NVMe on this board +ENABLE_HA=1 +ENABLE_FRIGATE=1 +``` + +`hosts/wan.conf` (Pi 5; values from its uncommitted `deploy.conf` and live state): + +```bash +# hosts/wan.conf — Raspberry Pi 5, hostname "wan". +# /etc/vanlink.conf -> this file. See hosts/vanlink.conf.example for the variable list. + +WIFI_5G_IFACE=wlxc83a35a4ee55 # Realtek RTL8852BU, rtw89 +WIFI_2G_IFACE=wlxf4f26d1760a7 # D-Link DWA-171, RTL8821CU +LAN_IFACE=enx00e04c331140 +STARLINK_IFACE=enx3c8cf861752b # TrendNet USB dongle +WIFI_WAN_IFACE=wlan0 +ONBOARD_LAN_IFACE=eth0 + +GATEWAY_IP="10.42.0.1" +GATEWAY_PREFIX="24" +GATEWAY_NETWORK="10.42.0.0/24" +DHCP_RANGE_START="10.42.0.10" +DHCP_RANGE_END="10.42.0.254" +DNS_RESOLVERS="1.1.1.1 8.8.8.8" + +MODEM_USB_VENDOR=2c7c +NVME_DEVICE= +NVME_FS_DEVICE= + +USB_BACKUP_ROOT_PART=/dev/sda2 +USB_BACKUP_MOUNT=10.42.0.44:/media/andreas/writable-nvme +SD_BACKUP_UUID= + +ENABLE_COCKPIT=0 +ENABLE_LI3=0 +ENABLE_NVME_WATCH=0 # boots from mmcblk0; no NVMe fitted +ENABLE_HA=1 +ENABLE_FRIGATE=0 +``` + +- [ ] **Step 4: Replace the `deploy.conf` block at the top of `deploy.sh`** + +In `deploy.sh`, replace everything from the line `# deploy.conf holds hardware-instance identifiers` through the closing `fi` of the `render` sub-command (currently lines 11-45) with: + +```bash +# Host-specific values (interface names, addressing, feature flags) live in +# /etc/vanlink.conf — a symlink to hosts/.conf in this repo. Config +# files below carry @TOKEN@ placeholders substituted from those variables via +# render()/install_rendered() — edit hosts/.conf, not the configs. +VANLINK_CONF=${VANLINK_CONF:-/etc/vanlink.conf} + +# Must be defined (non-empty) in the host file; deploy aborts naming any missing. +REQUIRED_VARS=( + WIFI_5G_IFACE WIFI_2G_IFACE LAN_IFACE STARLINK_IFACE WIFI_WAN_IFACE + GATEWAY_IP GATEWAY_PREFIX GATEWAY_NETWORK DHCP_RANGE_START DHCP_RANGE_END + DNS_RESOLVERS MODEM_USB_VENDOR + ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE +) +# May be empty or absent; NVME_* are required only when ENABLE_NVME_WATCH=1. +OPTIONAL_VARS=( + ONBOARD_LAN_IFACE NVME_DEVICE NVME_FS_DEVICE + USB_BACKUP_ROOT_PART USB_BACKUP_MOUNT SD_BACKUP_UUID +) +# Computed in load_conf; usable as @TOKEN@ like the rest. +DERIVED_VARS=(ONBOARD_LAN_UNMANAGED) +TOKEN_VARS=("${REQUIRED_VARS[@]}" "${OPTIONAL_VARS[@]}" "${DERIVED_VARS[@]}") +TOKEN_RE='@[A-Z][A-Z0-9_]*@' + +die() { echo "deploy: $*" >&2; exit 1; } + +load_conf() { # load_conf — source and validate a host config + local f=$1 v flag missing=() + [ -e "$f" ] || die "$f not found (missing or dangling symlink). Create it for this host with: + sudo ln -sfn \"$PWD/hosts/$(hostname -s).conf\" /etc/vanlink.conf" + for v in "${TOKEN_VARS[@]}"; do unset "$v"; done + # shellcheck disable=SC1090 + . "$f" + for v in "${REQUIRED_VARS[@]}"; do + [ -n "${!v:-}" ] || missing+=("$v") + done + [ ${#missing[@]} -eq 0 ] || die "$f is missing required variable(s): ${missing[*]}" + for flag in ENABLE_COCKPIT ENABLE_LI3 ENABLE_NVME_WATCH ENABLE_HA ENABLE_FRIGATE; do + case ${!flag} in + 0|1) ;; + *) die "$f: $flag must be 0 or 1 (got '${!flag}')" ;; + esac + done + if [ "$ENABLE_NVME_WATCH" = 1 ]; then + { [ -n "${NVME_DEVICE:-}" ] && [ -n "${NVME_FS_DEVICE:-}" ]; } \ + || die "$f: ENABLE_NVME_WATCH=1 needs NVME_DEVICE and NVME_FS_DEVICE" + fi + ONBOARD_LAN_UNMANAGED=${ONBOARD_LAN_IFACE:+;interface-name:$ONBOARD_LAN_IFACE} +} + +render() { # render -> stdout, with @TOKEN@ placeholders substituted + local args=() v + for v in "${TOKEN_VARS[@]}"; do + args+=(-e "s|@${v}@|${!v-}|g") + done + sed "${args[@]}" "$1" +} + +check_rendered() { # check_rendered — fail on a leftover @TOKEN@ + local left + left=$(grep -oE "$TOKEN_RE" "$2" | sort -u | tr '\n' ' ') || true + [ -z "$left" ] && return 0 + echo "deploy: $1: unrendered token(s) after render: $left" >&2 + return 1 +} + +install_rendered() { # install_rendered [mode] + local tmp + tmp=$(mktemp) + render "$1" > "$tmp" + check_rendered "$1" "$tmp" || { rm -f "$tmp"; exit 1; } + install -D -m"${3:-0644}" "$tmp" "$2" + rm -f "$tmp" +} + +template_files() { # every repo file that carries an @TOKEN@ placeholder + grep -rlE "$TOKEN_RE" --exclude-dir=.git --exclude-dir=__pycache__ \ + --exclude-dir=docs --exclude-dir=tests --exclude-dir=hosts \ + --exclude='*.bak' --exclude='*.md' --exclude=deploy.sh . | sed 's|^\./||' | sort +} + +case "${1:-}" in + --templates) + template_files + exit 0 ;; + --check) # ./deploy.sh --check [conf] — validate conf, render every template, install nothing + load_conf "${2:-$VANLINK_CONF}" + rc=0 + tmp=$(mktemp) + for f in $(template_files); do + render "$f" > "$tmp" + check_rendered "$f" "$tmp" || rc=1 + done + rm -f "$tmp" + [ "$rc" -eq 0 ] && echo "check OK: $(template_files | wc -l) templates render cleanly" + exit "$rc" ;; + render) + [ -n "${2:-}" ] || { echo "Usage: $0 render "; exit 1; } + load_conf "$VANLINK_CONF" + render "$2" + exit 0 ;; +esac +``` + +Then, directly after the existing line `[ "$(id -u)" = 0 ] || { echo "Run with sudo (writes to /etc, /usr)."; exit 1; }`, add: + +```bash +load_conf "$VANLINK_CONF" +echo "== host config: $(readlink -f "$VANLINK_CONF") ==" +``` + +- [ ] **Step 5: Rename `LAN_USB_IFACE` to `LAN_IFACE` in the remaining files and delete `deploy.conf`** + +```bash +grep -rlI 'LAN_USB_IFACE' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . | grep -v '^./deploy.conf$' +``` +Expected: `ap/22-van-lan.network`, `ap/van-ap-unmanaged.conf`, `router-status/config.json`, `cockpit/vanrouter/vanrouter.js` (and possibly others). Rename in exactly those files: + +```bash +grep -rlI 'LAN_USB_IFACE' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . \ + | grep -v '^./deploy.conf$' | xargs sed -i 's/LAN_USB_IFACE/LAN_IFACE/g' +git rm -q deploy.conf +grep -rnI 'LAN_USB_IFACE' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . || echo "none left" +``` +Expected last line: `none left`. (The lowercase JSON key `lan_usb_iface` is untouched by design.) + +- [ ] **Step 6: Run the test and fix any leftover-token findings** + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: all `ok`, `exit=0`. If `--check` reports `unrendered token(s)` for a file, that file has an `@…@` string that is not a defined token: either it is a typo (fix it) or legitimate text (rare; then change `template_files`' exclude list and note why in a comment). + +Also run: `./deploy.sh --check hosts/van.conf && VANLINK_CONF=hosts/wan.conf ./deploy.sh render ap/van-ap-dnsmasq.conf | grep dhcp-range` +Expected: `check OK: N templates render cleanly` then `dhcp-range=10.42.0.10,10.42.0.254,255.255.255.0,12h`. + +- [ ] **Step 7: Commit** + +```bash +git add deploy.sh tests hosts +git add -u +git commit -m "deploy: host config from /etc/vanlink.conf (hosts/.conf) + +deploy.conf is replaced by one versioned file per host plus an example. +deploy.sh validates the config, renders every template with a generic +token loop, refuses to install output with leftover @TOKEN@s and gains +--check/--templates. LAN_USB_IFACE is renamed LAN_IFACE. + +Co-Authored-By: Claude Sonnet 5 " +``` + +--- + +### Task 2: Template every remaining hardcoded host value + +**Files:** +- Modify: `ap/nftables.conf`, `ap/van-br0-setup.service`, `failover/config.json`, `router-status/config.json`, `ap/50-van-wan.yaml`, `failover/van-wlan-watchdog.service`, `ap/van-ap-unmanaged.conf`, `ap/23-van-lan-eth0.network`, `power/nvme-watch-config.json`, `deploy.sh` (install lines) +- Modify: `tests/check-templates.sh` (add checks 2 and 5) + +**Interfaces:** +- Consumes (Task 1): `./deploy.sh --templates`, `--check`, `render`, token vars `WIFI_WAN_IFACE`, `ONBOARD_LAN_IFACE`, `ONBOARD_LAN_UNMANAGED`, `NVME_DEVICE`, `NVME_FS_DEVICE`, `GATEWAY_NETWORK`, `LAN_IFACE`, `WIFI_5G_IFACE`. +- Produces: `tests/check-templates.sh` checks 2 (every templated file installed via `install_rendered`) and 5 (rendered host-specific content). + +- [ ] **Step 1: Add checks 2 and 5 to the test script** + +Insert before the final `exit "$fail"`: + +```bash +# 2. every file carrying an @TOKEN@ is installed through install_rendered +# (plain `install` would ship the placeholder unrendered — the 2026-09-23 failure) +exempt=" cockpit/vanrouter/vanrouter.js " # installed by the loop in the cockpit block +for f in $(./deploy.sh --templates); do + case "$exempt" in *" $f "*) continue ;; esac + if grep -qE "^[[:space:]]*install_rendered +$f[[:space:]]" deploy.sh; then + ok "install_rendered $f" + else + bad "$f contains an @TOKEN@ but deploy.sh does not install it with install_rendered" + fi +done + +# 5. host-specific values land in the rendered output +r() { VANLINK_CONF="hosts/$1.conf" ./deploy.sh render "$2"; } +expect() { # expect + if r "$1" "$2" | grep -qF -- "$3"; then ok "$1: $2 has '$3'"; else bad "$1: $2 lacks '$3'"; fi +} +expect van ap/nftables.conf 'ip saddr 10.42.1.0/24' +expect wan ap/nftables.conf 'ip saddr 10.42.0.0/24' +expect van ap/van-br0-setup.service 'brctl addif br0 enP1p17s0' +expect wan ap/van-br0-setup.service 'brctl addif br0 enx00e04c331140' +expect van failover/config.json '"device": "wlan0"' +expect van router-status/config.json '"wifi_wan_iface": "wlan0"' +expect van failover/van-wlan-watchdog.service 'van-wlan-watchdog wlan0' +expect van ap/van-ap-unmanaged.conf 'interface-name:enP1p17s0' +expect wan ap/van-ap-unmanaged.conf 'interface-name:enx00e04c331140;interface-name:eth0' +expect wan ap/23-van-lan-eth0.network 'Name=eth0' +expect van power/nvme-watch-config.json '"nvme_device": ""' +if r van ap/van-ap-unmanaged.conf | grep -q 'eth0'; then + bad "van: unmanaged-devices must not mention eth0 (ONBOARD_LAN_IFACE is empty)" +else + ok "van: unmanaged-devices has no eth0" +fi +``` + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: several `FAIL` lines from check 5 (files not yet templated); check 2 may already pass; `exit=1`. + +- [ ] **Step 2: Turn the hardcoded values into tokens** + +Make these exact edits: + +| File | Old | New | +|---|---|---| +| `ap/nftables.conf` | `ip saddr 10.42.1.0/24 oifname != "br0" masquerade;` | `ip saddr @GATEWAY_NETWORK@ oifname != "br0" masquerade;` | +| `ap/van-br0-setup.service` | `brctl addif br0 enP2p33s0 2>/dev/null \|\| true` | `brctl addif br0 @LAN_IFACE@ 2>/dev/null \|\| true` | +| `ap/van-br0-setup.service` | `brctl addif br0 wlp1s0 2>/dev/null \|\| true` | `brctl addif br0 @WIFI_5G_IFACE@ 2>/dev/null \|\| true` | +| `failover/config.json` | `"device": "wlan0",` | `"device": "@WIFI_WAN_IFACE@",` | +| `router-status/config.json` | `"wifi_wan_iface": "wlan0"` | `"wifi_wan_iface": "@WIFI_WAN_IFACE@"` | +| `ap/50-van-wan.yaml` | ` wlan0:` (the line under `wifis:`) | ` @WIFI_WAN_IFACE@:` | +| `failover/van-wlan-watchdog.service` | `ExecStart=/usr/local/sbin/van-wlan-watchdog` | `ExecStart=/usr/local/sbin/van-wlan-watchdog @WIFI_WAN_IFACE@` | +| `ap/van-ap-unmanaged.conf` | `interface-name:@LAN_IFACE@;interface-name:eth0` | `interface-name:@LAN_IFACE@@ONBOARD_LAN_UNMANAGED@` | +| `ap/23-van-lan-eth0.network` | `Name=eth0` | `Name=@ONBOARD_LAN_IFACE@` | +| `power/nvme-watch-config.json` | `"nvme_device": "nvme0",` | `"nvme_device": "@NVME_DEVICE@",` | +| `power/nvme-watch-config.json` | `"fs_device": "/dev/nvme0n1p2",` | `"fs_device": "@NVME_FS_DEVICE@",` | + +Note: `failover/config.json` has `"device": "wlan0", "metric": 100` — keep the column spacing; only replace the `wlan0` text. The `van-wlan-watchdog` script already takes the device as `argv[1]` (`DEVICE = sys.argv[1] if len(sys.argv) > 1 else "wlan0"`), so no script change is needed. + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: check 2 now FAILs for the files still installed with plain `install`; check 5 passes. + +- [ ] **Step 3: Install them through `install_rendered` in `deploy.sh`** + +Replace these lines (spacing preserved as in the file): + +```bash +install -D -m0644 ap/nftables.conf /etc/nftables.conf +``` +→ +```bash +install_rendered ap/nftables.conf /etc/nftables.conf +``` + +```bash +install -D -m0644 ap/van-br0-setup.service /etc/systemd/system/van-br0-setup.service +``` +→ +```bash +install_rendered ap/van-br0-setup.service /etc/systemd/system/van-br0-setup.service +``` + +```bash +install -D -m0644 failover/van-wlan-watchdog.service /etc/systemd/system/van-wlan-watchdog.service +``` +→ +```bash +install_rendered failover/van-wlan-watchdog.service /etc/systemd/system/van-wlan-watchdog.service +``` + +```bash +install -D -m0644 power/nvme-watch-config.json /etc/van-nvme-watch/config.json +``` +→ +```bash +install_rendered power/nvme-watch-config.json /etc/van-nvme-watch/config.json +``` + +and the `23-van-lan-eth0.network` line: + +```bash +install -D -m0644 ap/23-van-lan-eth0.network /etc/systemd/network/23-van-lan-eth0.network +``` +→ +```bash +if [ -n "${ONBOARD_LAN_IFACE:-}" ]; then + install_rendered ap/23-van-lan-eth0.network /etc/systemd/network/23-van-lan-eth0.network +else + rm -f /etc/systemd/network/23-van-lan-eth0.network +fi +``` + +- [ ] **Step 4: Run tests** + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: every line `ok`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add -A ap failover router-status power deploy.sh tests +git commit -m "Template remaining host-specific values (WAN wifi, LAN/eth0, NVMe, nftables) + +nftables.conf hardcoded the R5C network, van-br0-setup.service the R5C +interfaces and several configs wlan0/eth0. All now come from the host +config; the onboard LAN port is skipped when ONBOARD_LAN_IFACE is empty. + +Co-Authored-By: Claude Sonnet 5 " +``` + +--- + +### Task 3: Feature flags, safer apply step, `--diff` + +**Files:** +- Modify: `deploy.sh` (package list, cockpit/li3/nvme/HA/frigate blocks, enable/start/restart lines, apply step, new `--diff`, dirty-config warning) +- Modify: `tests/check-templates.sh` (add checks 6 and 8) + +**Interfaces:** +- Consumes (Task 1): `load_conf`, `render`, `warn` (defined later in `deploy.sh`, so the dirty-config warning goes right after its definition), flags `ENABLE_COCKPIT|LI3|NVME_WATCH|HA|FRIGATE`. +- Produces: `./deploy.sh --diff [conf]` (prints `NEW ` / `CHANGED ` + unified diff for every `install_rendered /abs/dst` line); all flagged units are guarded by a column-0 `if [ "$ENABLE_X" = 1 ]; then … fi` block with 4-space-indented contents (the awk check in check 6 depends on that formatting). + +- [ ] **Step 1: Add checks 6 and 8 and see them fail** + +Insert before the final `exit "$fail"`: + +```bash +# 6. flagged units/paths only appear inside their ENABLE_* guard in deploy.sh. +# Convention: guards are column-0 `if [ "$ENABLE_X" = 1 ]; then` ... `fi`, +# contents indented (so a nested `fi` never sits in column 0). +if awk ' + BEGIN { + need["van-li3-battery"]="ENABLE_LI3" + need["van-nvme-watch"]="ENABLE_NVME_WATCH" + need["nvme-watch-config"]="ENABLE_NVME_WATCH" + need["homeassistant"]="ENABLE_HA" + need["esphome"]="ENABLE_HA" + need["frigate"]="ENABLE_FRIGATE" + need["cockpit"]="ENABLE_COCKPIT" + } + /^if \[ "\$ENABLE_[A-Z0-9_]+" = 1 \]/ { match($0, /ENABLE_[A-Z0-9_]+/); guard=substr($0, RSTART, RLENGTH); next } + /^fi([[:space:]]|$)/ { guard=""; next } + /^[[:space:]]*(#|echo)/ { next } + { for (u in need) if (index($0, u) && guard != need[u]) { + printf " deploy.sh:%d mentions %s outside an %s guard: %s\n", NR, u, need[u], $0; bad=1 } } + END { exit bad } +' deploy.sh; then ok "flagged units are only touched inside their ENABLE_* guard"; else bad "flag gating"; fi + +# 8. ./deploy.sh --diff runs (no root) and lists rendered files +if out=$(./deploy.sh --diff hosts/van.conf 2>&1) && echo "$out" | grep -qE '^(NEW|CHANGED) '; then + ok "deploy.sh --diff hosts/van.conf" +else + bad "deploy.sh --diff hosts/van.conf"; echo "$out" | sed 's/^/ /' +fi +``` + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: check 6 lists many unguarded mentions, check 8 FAILs (`--diff` unknown), `exit=1`. + +- [ ] **Step 2: Add `--diff` and the dirty-config warning** + +In the `case "${1:-}" in` block (added in Task 1), add before `render)`: + +```bash + --diff) # ./deploy.sh --diff [conf] — what would change in /etc, without installing + load_conf "${2:-$VANLINK_CONF}" + grep -E '^[[:space:]]*install_rendered +[^" ]+ +/' deploy.sh | while read -r _ src dst _; do + if [ ! -e "$dst" ]; then + echo "NEW $dst" + elif ! diff -q <(render "$src") "$dst" >/dev/null; then + echo "CHANGED $dst" + diff -u "$dst" <(render "$src") | sed 's/^/ /' || true + fi + done + exit 0 ;; +``` + +Directly after the `write_warnings() { … }` function definition in `deploy.sh`, add: + +```bash +# A host file with uncommitted edits means this deploy uses unreviewed values. +conf_real=$(readlink -f "$VANLINK_CONF") +if git -c safe.directory='*' -C "$(dirname "$conf_real")" status --porcelain -- "$conf_real" 2>/dev/null | grep -q .; then + warn "$conf_real has uncommitted changes — commit it so the host config stays reviewable" +fi +``` + +- [ ] **Step 3: Gate the optional sections** + +Edit `deploy.sh` (old text is exact; new text indents block contents by 4 spaces): + +**Packages** — remove `podman` from the array and add the conditional. Replace +```bash + zerotier-one + podman +) +``` +with +```bash + zerotier-one +) +if [ "$ENABLE_HA" = 1 ]; then + REQUIRED_PACKAGES+=(podman) +fi +``` + +**Cockpit** — replace the first line `if [ "$SKIP_COCKPIT" -ne "1" ]; then` with `if [ "$ENABLE_COCKPIT" = 1 ]; then` (the block is already indented; also fix its stray trailing spaces after the closing `fi`). + +**Li3** — replace the whole `echo "== li3 …"` section (from `echo "== li3 (RV house battery BMS -> MQTT/HA) =="` through the `python3 -c 'import bleak' …|| warn "python3-bleak missing …"` line) with: + +```bash +if [ "$ENABLE_LI3" = 1 ]; then + echo "== li3 (RV house battery BMS -> MQTT/HA) ==" + # NOT the laptop's own AC/battery monitor — that's power/van-battery (different + # hardware, different concern). This is the Lithionics Li3 12V LiFePO4 house + # battery, over its BLE HM-10 UART module (no pairing) -> Home Assistant MQTT + # discovery. Broker + creds live only on the system (0600, seeded from + # li3/config.json.example — same pattern as gps/pushover). + install -D -m0755 li3/van-li3-battery /usr/local/sbin/van-li3-battery + install -D -m0644 li3/van-li3-battery.service /etc/systemd/system/van-li3-battery.service + install -D -m0644 li3/van-li3-battery-restart-limit.conf /etc/systemd/system/van-li3-battery.service.d/restart-limit.conf + if [ ! -f /etc/van-li3/config.json ]; then + install -D -m0600 li3/config.json.example /etc/van-li3/config.json + warn "seeded /etc/van-li3/config.json (EDIT IT: add MQTT username + password)" + fi + python3 -c 'import bleak' 2>/dev/null \ + || warn "python3-bleak missing (apt install python3-bleak) — van-li3-battery won't start" +fi +``` + +and delete the now-duplicated line +`install -D -m0644 li3/van-li3-battery-restart-limit.conf /etc/systemd/system/van-li3-battery.service.d/restart-limit.conf` +from the router-status section. + +**NVMe** — replace the three-line nvme section (after `echo "== nvme watchdog =="`) with: + +```bash +if [ "$ENABLE_NVME_WATCH" = 1 ]; then + echo "== nvme watchdog ==" + install -D -m0755 power/van-nvme-watch /usr/local/sbin/van-nvme-watch + install_rendered power/nvme-watch-config.json /etc/van-nvme-watch/config.json + install -D -m0644 power/van-nvme-watch.service /etc/systemd/system/van-nvme-watch.service +fi +``` + +**Home Assistant / ESPHome / Frigate** — replace the section from `echo "== home assistant =="` through the `install -d -m0755 /srv/frigate/config /srv/frigate/storage` line with: + +```bash +echo "== home assistant ==" +# Native HA (Podman Quadlet, replaced the ha_van VM). daemon-reload below +# regenerates homeassistant.service; started (not restarted) at the end so a +# deploy never bounces HA — after editing the .container, restart it manually. +if [ "$ENABLE_HA" = 1 ]; then + install -D -m0644 ha/homeassistant.container /etc/containers/systemd/homeassistant.container + install -d -m0755 /srv/homeassistant + # ESPHome dashboard (sibling container, same rationale as HA above — no + # Supervisor/add-on store here). + install -D -m0644 ha/esphome.container /etc/containers/systemd/esphome.container + install -d -m0755 /srv/esphome +fi +# Frigate NVR (sibling container, same rationale as HA/ESPHome above). +if [ "$ENABLE_FRIGATE" = 1 ]; then + install -D -m0644 ha/frigate.container /etc/containers/systemd/frigate.container + install -d -m0755 /srv/frigate/config /srv/frigate/storage +fi +``` + +**Apply step** — replace the long `systemctl enable regdomain.service … van-router-mqtt >/dev/null 2>&1 || true` line with: + +```bash +ENABLE_UNITS=(regdomain.service van-br0-setup.service hostapd hostapd-2g van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-ap-watchdog van-ap-watchdog-2g van-wlan-watchdog van-modem-watch van-sms-watch van-gps-owntracks van-router-mqtt) +if [ "$ENABLE_NVME_WATCH" = 1 ]; then + ENABLE_UNITS+=(van-nvme-watch) +fi +if [ "$ENABLE_LI3" = 1 ]; then + ENABLE_UNITS+=(van-li3-battery) +fi +systemctl enable "${ENABLE_UNITS[@]}" >/dev/null 2>&1 || true +``` + +Replace the three `systemctl start homeassistant/esphome/frigate … || warn …` lines with: + +```bash +if [ "$ENABLE_HA" = 1 ]; then + systemctl start homeassistant || warn "homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)" + systemctl start esphome || warn "esphome failed to start (podman/quadlet — check journalctl -u esphome)" +fi +if [ "$ENABLE_FRIGATE" = 1 ]; then + systemctl start frigate || warn "frigate failed to start (podman/quadlet — check journalctl -u frigate)" +fi +``` + +Replace the restart lines `systemctl restart van-thermal` … `systemctl restart van-router-mqtt` (thermal, nvme-watch, sms-watch, gps-owntracks, li3-battery, router-mqtt) with: + +```bash +systemctl restart van-thermal +if [ "$ENABLE_NVME_WATCH" = 1 ]; then + systemctl restart van-nvme-watch +fi +systemctl restart van-sms-watch +systemctl restart van-gps-owntracks +if [ "$ENABLE_LI3" = 1 ]; then + systemctl restart van-li3-battery +fi +systemctl restart van-router-mqtt +``` + +Replace `systemctl restart van-ap-dnsmasq nftables van-failover van-wlan-watchdog` with (a failing unit is now recorded as a warning instead of aborting the script mid-apply): + +```bash +for u in van-ap-dnsmasq nftables van-failover van-wlan-watchdog; do + systemctl restart "$u" || warn "$u failed to restart (journalctl -xeu $u)" +done +``` + +- [ ] **Step 4: Run tests** + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: all `ok`, `exit=0`. If check 6 still names a line, that line mentions a flagged unit outside a column-0 guard: move it inside a guard (or, if it is a comment/echo, it is already skipped). + +Also: `bash -n deploy.sh && ./deploy.sh --diff hosts/wan.conf | head -5` — expected: `NEW /etc/...` lines (this laptop has none of the files). + +- [ ] **Step 5: Commit** + +```bash +git add deploy.sh tests +git commit -m "deploy: feature flags, --diff, tolerate failed restarts + +Li3, NVMe watchdog, Home Assistant/ESPHome, Frigate and Cockpit are +installed, enabled and restarted only when their ENABLE_* flag is 1. +--diff shows what would change in /etc. A unit that fails to restart is +recorded as a warning instead of aborting the deploy. + +Co-Authored-By: Claude Sonnet 5 " +``` + +--- + +### Task 4: Backup scripts read the host config + +**Files:** +- Modify: `failover/sync-usb-backup.sh`, `failover/sync-sd-backup.sh` +- Modify: `tests/check-templates.sh` (add check 7) + +**Interfaces:** +- Consumes (Task 1): host variables `USB_BACKUP_ROOT_PART`, `USB_BACKUP_MOUNT`, `SD_BACKUP_UUID`; env override `VANLINK_CONF` (default `/etc/vanlink.conf`). +- Produces: both scripts exit non-zero with a message naming the missing variable, **before** the root check, when their setting is absent. + +- [ ] **Step 1: Add check 7 and see it fail** + +Insert before the final `exit "$fail"`: + +```bash +# 7. backup scripts take their settings from the host config and say what is missing +empty=$(mktemp); trap 'rm -f "$empty"' EXIT +for pair in "failover/sync-usb-backup.sh:USB_BACKUP_ROOT_PART" "failover/sync-sd-backup.sh:SD_BACKUP_UUID"; do + s=${pair%%:*}; v=${pair##*:} + if out=$(VANLINK_CONF="$empty" "./$s" 2>&1); then + bad "$s should fail when $v is not set" + elif echo "$out" | grep -q "$v"; then + ok "$s names missing $v" + else + bad "$s failed but did not mention $v"; echo "$out" | sed 's/^/ /' + fi +done +if grep -nE '/dev/sda2|10\.42\.[01]\.44|39118305-4927' failover/sync-*-backup.sh; then + bad "hardcoded host values remain in the backup scripts" +else + ok "no hardcoded devices/UUIDs/addresses in the backup scripts" +fi +``` + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: check 7 FAILs (scripts still hardcode values and demand root first), `exit=1`. + +- [ ] **Step 2: Edit `failover/sync-usb-backup.sh`** + +Directly after `set -euo pipefail`, insert: + +```bash +# Backup settings come from the host config (see hosts/vanlink.conf.example). +# shellcheck disable=SC1090 +. "${VANLINK_CONF:-/etc/vanlink.conf}" +: "${USB_BACKUP_ROOT_PART:?set USB_BACKUP_ROOT_PART in /etc/vanlink.conf}" +: "${USB_BACKUP_MOUNT:?set USB_BACKUP_MOUNT in /etc/vanlink.conf}" +``` + +Replace this block: + +```bash +USB_ROOT_PART=/dev/sda2 +MOUNT_POINT=/mnt/usb-backup-root +MOUNT_POINT=10.42.1.44:/media/andreas/writable-nvme + + +[[ "$MOUNT_POINT" == *":"* ]] +LOCAL_MOUNT=$? +``` + +with: + +```bash +USB_ROOT_PART=$USB_BACKUP_ROOT_PART +MOUNT_POINT=$USB_BACKUP_MOUNT + +# host:/path means an already-exported NFS target: nothing to mount locally. +if [[ "$MOUNT_POINT" == *":"* ]]; then LOCAL_MOUNT=0; else LOCAL_MOUNT=1; fi +``` + +(This also fixes a latent bug: the old `[[ … ]]` line returned 1 for a local mount point and `set -e` aborted the script before `LOCAL_MOUNT=$?` ran.) + +- [ ] **Step 3: Edit `failover/sync-sd-backup.sh`** + +Directly after `set -euo pipefail`, insert: + +```bash +# Backup settings come from the host config (see hosts/vanlink.conf.example). +# shellcheck disable=SC1090 +. "${VANLINK_CONF:-/etc/vanlink.conf}" +: "${SD_BACKUP_UUID:?set SD_BACKUP_UUID in /etc/vanlink.conf}" +``` + +Replace `SD_UUID=39118305-4927-47a4-acc7-ba022f37af22` with `SD_UUID=$SD_BACKUP_UUID`. + +- [ ] **Step 4: Run tests** + +Run: `./tests/check-templates.sh; echo "exit=$?"` +Expected: all `ok`, `exit=0`. + +- [ ] **Step 5: Commit** + +```bash +git add failover tests +git commit -m "Backup scripts read device/UUID/target from the host config + +Co-Authored-By: Claude Sonnet 5 " +``` + +--- + +### Task 5: Documentation and stale references + +**Files:** +- Modify: `README.md` (header), `deploy.sh` (header comment), and comments that mention `deploy.conf`: `failover/van-modem-watch`, `failover/60-van-wan-dns`, `ap/50-van-wan.yaml`, `ap/hostapd-2g.service`, `ap/99-van-router-dns.conf`, `cockpit/vanrouter/vanrouter.js` + +- [ ] **Step 1: Find the stale references** + +Run: `grep -rnI 'deploy\.conf' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs .` +Expected: comment-only hits in the files listed above (and none that are code). + +- [ ] **Step 2: Update them** + +For each hit, replace `deploy.conf` with `/etc/vanlink.conf` (comment text only), e.g.: + +```bash +grep -rlI 'deploy\.conf' --exclude-dir=.git --exclude='*.bak' --exclude-dir=docs . \ + | xargs sed -i "s#deploy\.conf's#/etc/vanlink.conf's#g; s#deploy\.conf#/etc/vanlink.conf#g" +``` + +Replace the first lines of `deploy.sh`: + +```bash +# Deploy vanlink configs/scripts (Pi 4 "wan" port) to their system locations. +# Usage: cd ~/vanlink && sudo ./deploy.sh +# sudo ./deploy.sh render # preview a templated file on stdout +``` +with: +```bash +# Deploy vanlink configs/scripts to their system locations. One checkout is +# shared by every host; host-specific values come from /etc/vanlink.conf +# (symlink to hosts/.conf — see hosts/vanlink.conf.example). +# Usage: cd ~/vanlink && sudo ./deploy.sh +# ./deploy.sh --check [conf] # validate conf + render all templates, no install +# ./deploy.sh --diff [conf] # what would change in /etc +# ./deploy.sh render # preview a templated file on stdout +``` + +Replace the note block at the top of `README.md` (the `> **This clone = the Pi 4 port …` blockquote, 5 lines) with: + +```markdown +> **One checkout, several hosts.** The Pi 5 (`wan`, 10.42.0.1) and the FriendlyElec R5C +> (`van`, 10.42.1.1) run from this same repo and branch. Everything host-specific +> (interface names, addressing, which optional services run) lives in +> `hosts/.conf`; `/etc/vanlink.conf` is a symlink to it: +> `sudo ln -sfn "$PWD/hosts/$(hostname -s).conf" /etc/vanlink.conf`. +> Edit that file (and commit it), not the individual configs. Before a deploy: +> `./deploy.sh --check` and `./deploy.sh --diff`. Static checks: `tests/check-templates.sh`. +``` + +- [ ] **Step 3: Test and commit** + +Run: `./tests/check-templates.sh; echo "exit=$?"` — expected `exit=0`. + +```bash +git add -A README.md deploy.sh failover ap cockpit +git commit -m "Docs: shared checkout model, /etc/vanlink.conf references + +Co-Authored-By: Claude Sonnet 5 " +git push origin wan +``` + +--- + +### Task 6: Roll out to the R5C (`van`) — needs the user's go-ahead + +**Gate — ask the user first.** Applying changes `enP1p17s0` (today the port carrying the management address `192.168.10.211`) into the LAN bridge port. Confirm: (a) the cabling is final (Starlink/upstream cable on `enP2p33s0`, LAN devices on `enP1p17s0`), and (b) the user has console access or another way in (e.g. the van Wi-Fi AP at `10.42.1.1`) in case SSH over `192.168.10.211` drops. + +**Files:** none in the repo; changes on the host `192.168.10.211` (`root@`). + +- [ ] **Step 1: Put the checkout on the host** + +`git` is not installed on the R5C. Install it and use the repo remote; if that key/network is unavailable, rsync the laptop checkout including `.git`: + +```bash +ssh root@192.168.10.211 'apt-get install -y git' +# preferred: +ssh root@192.168.10.211 'cd /home/andreas && git clone -b wan git@git.wrede.ca:andreas/vanlink.git vanlink.new' +# fallback (laptop checkout is up to date after Task 5's push): +rsync -a --exclude __pycache__ /home/andreas/vanlink/ root@192.168.10.211:/home/andreas/vanlink.new/ +ssh root@192.168.10.211 'chown -R andreas:andreas /home/andreas/vanlink.new' +``` + +- [ ] **Step 2: Swap the directory (keep the old one) and link the host config** + +```bash +ssh root@192.168.10.211 'cd /home/andreas && mv vanlink vanlink.old && mv vanlink.new vanlink && + ln -sfn /home/andreas/vanlink/hosts/van.conf /etc/vanlink.conf && ls -l /etc/vanlink.conf' +``` +Expected: `/etc/vanlink.conf -> /home/andreas/vanlink/hosts/van.conf`. + +- [ ] **Step 3: Validate and preview** + +```bash +ssh root@192.168.10.211 'cd /home/andreas/vanlink && ./deploy.sh --check && ./deploy.sh --diff' +``` +Expected: `check OK: N templates render cleanly`, then `NEW`/`CHANGED` entries. Read every `CHANGED` diff: the expected ones are `/etc/van-ap/dnsmasq.conf`, `/etc/systemd/network/21-van-br0.network`, `/etc/nftables.conf` (previously unrendered), `05-van-lan.network` (NEW), and `/etc/systemd/system/van-br0-setup.service`. Anything else needs an explanation before continuing. + +- [ ] **Step 4: Deploy and verify** + +```bash +ssh root@192.168.10.211 'cd /home/andreas/vanlink && ./deploy.sh 2>&1 | tail -25' +``` +Then (from a machine that can still reach it; use `10.42.1.1` if `192.168.10.211` is gone): + +```bash +ssh root@ 'systemctl is-active van-ap-dnsmasq nftables hostapd; ip -br addr show br0; networkctl status enP1p17s0 | grep "Network File"; nft list ruleset | grep masquerade' +``` +Expected: `active` ×3; `br0 … 10.42.1.1/24`; `Network File: /etc/systemd/network/05-van-lan.network`; a masquerade rule for `10.42.1.0/24`. Report anything else to the user (do not paper over failures; `journalctl -xeu `). + +- [ ] **Step 5: Record the outcome in memory** (`multihost-config-plan`): R5C migrated, old copy at `/home/andreas/vanlink.old`. + +--- + +### Task 7: Roll out to the Pi 5 (`wan`) — needs the user's go-ahead + +**Gate — ask the user first.** A deploy restarts `systemd-networkd`/hostapd on a live router (brief Wi-Fi/LAN outage) — agree a moment. Also decide with the user: `ENABLE_NVME_WATCH=0` in `hosts/wan.conf` means deploy no longer manages `van-nvme-watch`; the Pi has no NVMe yet the service is currently running. Ask whether to `systemctl disable --now van-nvme-watch` there (deploy never stops services). + +**Files:** none in the repo; changes on `root@10.42.0.1`. Run git there as `git -c safe.directory=/home/andreas/vanlink` (repo owned by `andreas`, commands run as root). + +- [ ] **Step 1: Back up the local edits, then drop them** + +```bash +ssh root@10.42.0.1 'cd /home/andreas/vanlink && G="git -c safe.directory=/home/andreas/vanlink" && + mkdir -p /home/andreas/vanlink-pre-migration && cp -a deploy.conf deploy.sh failover/sync-usb-backup.sh li3/van-li3-battery /home/andreas/vanlink-pre-migration/ && + $G diff > /home/andreas/vanlink-pre-migration/local-edits.diff && $G status --short' +``` +Expected: the four modified files (`deploy.conf`, `deploy.sh`, `failover/sync-usb-backup.sh`, `li3/van-li3-battery`) and nothing else. Their content is superseded: `li3` and `sync-usb-backup.sh` changes are already in `wan` (`565e8f3`); the host values are now in `hosts/wan.conf`; the `SKIP_LI3` logic became `ENABLE_LI3`. Confirm with the user, then: + +```bash +ssh root@10.42.0.1 'cd /home/andreas/vanlink && G="git -c safe.directory=/home/andreas/vanlink" && + $G checkout -- . && $G fetch origin && $G merge --ff-only origin/wan && $G log --oneline -1' +``` +Expected: fast-forward to the commit pushed in Task 5. + +- [ ] **Step 2: Link the host config, validate, and compare with what is live** + +```bash +ssh root@10.42.0.1 'cd /home/andreas/vanlink && ln -sfn "$PWD/hosts/wan.conf" /etc/vanlink.conf && + ./deploy.sh --check && ./deploy.sh --diff' +``` +Expected: `check OK`. Read every `CHANGED` entry; changes that alter behaviour on the Pi 5 (addresses, interface names, DNS, unit contents) must be explained or `hosts/wan.conf` corrected **before** deploying. Fix by editing `hosts/wan.conf` on the laptop, commit, push, `git merge --ff-only` again. + +- [ ] **Step 3: Deploy in the agreed window and verify** + +```bash +ssh root@10.42.0.1 'cd /home/andreas/vanlink && ./deploy.sh 2>&1 | tail -25' +ssh root@10.42.0.1 'systemctl is-active van-ap-dnsmasq nftables hostapd hostapd-2g van-failover; ip -br addr show br0; cat /run/van-failover/state.json | head -20' +``` +Expected: all `active`, `br0 … 10.42.0.1/24`, failover state healthy. Any `warn` lines from deploy go to the user. + +- [ ] **Step 4: Update memory** (`multihost-config-plan`): both hosts migrated; `/etc/vanlink.conf` symlinks; backup of the old Pi edits at `/home/andreas/vanlink-pre-migration`. Delete `vanlink.old` on the R5C only when the user says so. + +--- + +## Self-Review (done) + +**Spec coverage:** host file + symlink (Task 1, 6, 7); variable table incl. new vars (Task 1 arrays/example, Task 2 tokens); repo changes / `deploy.conf` removal / hosts files / new tokens (Tasks 1-2); README (Task 5); `deploy.sh` behaviour: missing/dangling symlink message + required-var abort + resolved path + dirty warning (Tasks 1, 3), `install_rendered` leftover guard (Task 1), `05-van-lan` and eth0 handling (pending-fix commit, Task 2), feature flags for install/enable/restart (Task 3), `--check` (Task 1), `render` (Task 1), restart-failure warnings (Task 3); backup scripts (Task 4); testing items 1-4 (checks 1-4) plus content checks; migration (Tasks 6-7) incl. pre-deploy diff (`--diff`, Task 3) and Pi 5 backup; out-of-scope items untouched. + +**Placeholders:** none; every code step has its code. Task 6/7 are operational and hold explicit gates. + +**Type/name consistency:** `LAN_IFACE`, `WIFI_WAN_IFACE`, `ONBOARD_LAN_IFACE`/`ONBOARD_LAN_UNMANAGED`, `NVME_DEVICE`/`NVME_FS_DEVICE`, `USB_BACKUP_ROOT_PART`/`USB_BACKUP_MOUNT`, `SD_BACKUP_UUID`, `ENABLE_*` names identical across the spec, `REQUIRED_VARS`/`OPTIONAL_VARS`, host files, templates and tests. Check numbers in the test script are 1, 2, 3, 4, 5, 6, 7, 8 (a section is added in the task that introduces its behaviour; numbering follows the spec's order where it exists and is not required to be contiguous while tasks are in progress).