# 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).