dotfiles
Log

Open items

Edit on GitHub

What is still open. A finished item migrates to history/: this file only grows with new work, and shrinks when work ends.

Convention inherited from the single-file era: every item explains the WHAT, the WHY and the known trap. The paragraph is worth more than the title.

AUDITED on 16/08/2026 against the actual tree, because this file had drifted the way rule 16 describes: six items were already DONE and still sitting here, and one was carrying 70 lines of finished work. What was closed is in the august history.

  • The lock repaints while idle (opened 03/11/2026). On the real panels the QML lock uses 7.9% of a core over a 4 h lock, 4x the 1.93% measured in the headless sway; the GPU stays at its floor, so it is CPU-side, likely something animating every frame at the panels' refresh. Find it with the QML profiler on a locked session before changing anything.

  • Hyprland under UWSM, with greetd in place of LightDM (opened 03/11/2026). Every shutdown crashes the session's Wayland clients: logind stops session-N.scope (Hyprland) BEFORE user@1000 (the scope is After=user@1000.service), so hyprpaper and hyprsunset abort, the Hyprland portal segfaults and Sunshine exits 255, all in the first 30 ms. Cosmetic (no state lost, they would be killed 0.1 s later), but they are the red lines on the shutdown splash. • WHY NOT A HYPRLAND HOOK: in 0.56.2 hyprland.shutdown and exec-shutdown fire only from the exit dispatcher; SIGTERM goes straight to stopCompositor() (Compositor.cpp:92). • THE FIX: UWSM runs Hyprland as a user@ service, so systemd stops the clients first. greetd's initial_session replaces LightDM's autologin, which drops the X server that services.xserver.enable brings up only for an unseen greeter, and runs once, so a log out lands on tuigreet instead of racing a new autologin (what kept hyprshutdown out, shutdown). • WHAT HAS TO HOLD: the keyring (greetd's PAM plus the TPM seal), the Plymouth handoff (greetd is display-manager.service too), Sunshine, the lockscreen, and PowerMath's log out (loginctl terminate-user).

  • Refine the Zen theme, by hand and at my own pace (opened 13/10/2026). The minimal one in modules/home/apps/zen/theme.nix recolors Zen through its own variables; what it does NOT do yet is the rest of the old Arch look (archive/arch-final:zen-browser/userChrome.css), which needed cleaning anyway. How it works and how to flip to stock: notes/apps/zen.md. • THE RULE THAT KEEPS IT CLEAN: a variable first, a selector only when no variable reaches the element, and every color from my.theme.palette, never a typed hex. • THE TRAP: a user sheet loses to Zen's author sheet, so a new line without !important looks like it did nothing. Iterate with the Browser Toolbox (devtools.chrome.enabled), then move the result into the Nix file.

  • What the layout move left shared on purpose, to split the day a SECOND host exists (opened 30/09/2026). Decision 0009 made hosts pick their device modules; these three stayed where they are because splitting them now would cut working modules for a machine that does not exist yet. • THE SPEAKER EQ in modules/nixos/hardware/audio.nix targets the Edifier G1500 by node name. It is inert without that speaker, so it costs nothing today; it becomes a device module the host imports, like the mice. • THE HOME MANAGER SIDE has no per-host layer: the Razer OSD, the DDC backlight of the two panels and the Windows games disk reach every host. The answer is a hosts/<host>/home.nix that adds home-manager.users.v1cferr.imports. • THE ROUTER'S DHCP RESERVATION still names this machine v1cferr-nixos. Renaming it is a change on the device (uci, then router-sync pull), not a layout one.

  • The quarterly app review, first due 31/12/2026 (opened 29/09/2026). nix run .#usage-audit lists the apps with no trace of use; the first run left 22. Each one is either deleted in its own commit or kept on purpose, and the answer is the point, not the count: notes/repo/usage-audit.md.

  • [~] NO BACKUP from 24/09/2026 to 04/10/2026 (opened 24/09/2026, reshaped 04/10/2026). The daily restic to Google Drive was RETIRED and its HOME repo permanently deleted. ITS REPLACEMENT IS ON since 04/10/2026: restic to the reformatted Seagate (decision 0011), first run and drill D4 passed (notes/boot-and-storage/restic.md). What is left: a NEWER disk to replace a 2009 Seagate by 2026-12-31, and the OFFSITE copy, an accepted risk until the same date (decision 0008). • WHY IT DIED: a 15 GiB quota holding 130 GiB, of which 109.37 were restic. 30.62 of those were packs the forget --prune had deleted straight into the Drive's TRASH, where a deleted file goes on counting. The prune was working the whole time and the account filled up anyway, which is the part worth remembering when picking the next backend. • WHAT IS EXPOSED MEANWHILE: everything in ~ that is not in git. btrbk still takes hourly snapshots of @home, but they share the disk they protect, so they cover an accidental overwrite and NOTHING else. A dead NVMe, a theft or a fire is total loss. • THE ARCH ARCHIVE moved to the NVMe on 03/10/2026 (verified with check --read-data); its second copy enters the backup repo with the first run. Before that it was ONE COPY on the Seagate, a 44.6 GiB snapshot with no source to regenerate it, on a 2009 disk with 840 thousand load cycles and 348 CRC errors. Giving it a second copy is part of this item, not a separate one. • THE LINK IS 100 Mb/s, found on 24/09/2026: enp7s0 negotiated 100 with an r8169, a gigabit Realtek, and the August measurement of 215 Mbps proves it used to be gigabit. Gigabit needs 4 pairs and 100 needs 2, so a damaged cable downgrades in silence. This caps any offsite restore, so it is worth fixing BEFORE choosing a remote destination. ethtool is not installed on this machine, which is why it went unnoticed.

  • [~] The memory servers exist; what is open is how the providers reach them (opened 24/08/2026, reshaped 25/09/2026). The servers landed on 24/08/2026 and split in two on 25/09/2026 (see notes/apps/basic-memory.md). The knowledge work itself moved to the context repository, which tracks it in its own ADRs and issues, so what stays here is runtime. • THE REMOTE ENDPOINT FOR THE WEB APPS is undecided. ChatGPT web only talks to a public HTTPS MCP server with OAuth, and Basic Memory has no authentication of its own, so it needs a tunnel plus an OAuth layer in front of the GENERAL server only (Cloudflare Tunnel with Access is the candidate). Allowed through it: reads, write_note, edit_note; the destructive tools are refused at the gateway. The FAI server is never exposed. • GEMINI WEB IS OUT FOR NOW: its custom MCP apps are limited to the US (checked 25/09/2026), so Gemini reaches the memory through agy, locally. • CODEX'S ENDPOINT IS A LITERAL in its versioned mirror (modules/home/shell/codex/config.toml), the one consumer that cannot read my.memory.servers.general.url. A port change has to touch it by hand until codex takes a generated fragment.

  • The one follow-up the gate's hardening left open (23/08/2026). The dependabot half was CLOSED the same day (see the august history); this is what stayed. • THE COMMIT MESSAGE IS ONLY CHECKED HERE. convco and prose-style run at the commit-msg stage, which nix flake check cannot reach: there is no message inside that sandbox. So a commit written on another machine, or with --no-verify, is unchecked. The fix is a CI step running both over the PUSHED RANGE, and it is only worth doing the day a second machine starts writing to this repo.

  • [~] The T480 before it leaves the house (opened 22/08/2026). It is my mother's machine, it goes to another city, and after that every mistake here costs a phone call instead of a minute. The Windows side lives in that machine's OWN repo; what is listed here is either this repo's or a joint decision. • CLOSED THE SAME DAY, so nobody reads this item as untouched: the MASQUERADE toward 10.10.10.6, the panel user, the Moonlight pairing, the DoH policy on her browsers and the watchdog's numbers. The reasoning for each is in the august history; what is left below is what genuinely cannot be closed from this side of the house. • THE ONLY VALID TEST IS FROM ANOTHER NETWORK, and none of today's proves anything about her house. With the machine on the home Wi-Fi the tunnel closes by hairpin on the router itself, and wg show even displays 192.168.1.1 as the endpoint while the file says vpn.v1cferr.dev. The protocol: phone hotspot, tunnel coming up on its own, ssh t480 answering, and a Moonlight session lasting past 2 min (a drop at ~4 s is the MTU signature). Do it while the machine is still within arm's reach. • THE GHOST REAPER DOES NOT EXIST OVER THERE, and it bit once already on 22/08: a client that vanishes with no teardown leaves Sunshine answering SUNSHINE_SERVER_BUSY forever, and Moonlight then refuses to open a new session, so from the client the host is simply broken. The remedy is a restart and the pairing survives it, which is what makes a watchdog cheap to write. A flaky public Wi-Fi is a ghost factory, so this is not an edge case at her place. • THE WINDOWS VARIANT NEEDS A DIFFERENT SIGNAL. Here the reaper reads BUSY plus no bound UDP socket; over there the three sockets WERE bound with the client long gone, so that test would never fire. What did not lie is the traffic: wg show <tunnel> transfer barely moved. Condition: BUSY plus a byte delta under a floor, HELD for a few cycles so a session being born is not killed. • IT DROPPED IN PRIORITY THE MOMENT RDP LANDED, and that is worth writing down instead of just doing the work: a ghost used to mean the machine was unreachable, and now it means one service needs restarting through a path that does not depend on it. Convenience, not a lifeline. • NO REAL SESSION HAS RUN YET. Paired is not streaming: moonlight list 10.10.10.6 answers Desktop, which proves the pairing and nothing about video. Until a session runs, the encoder, the packet size and the bitrate are all still theory. • max_bitrate IS A PLACEHOLDER at 10000. The bottleneck out of there is HER upload, which nobody has measured. Raise it after a real session, not before. • THE MACHINE'S REPO HAS NO REMOTE. It is a local git on a laptop that is leaving, so the inventory, the scripts and the reasoning die with the disk. It is not this repo's content and does not belong in it (rule 14, and it holds her machine's specifics), so the answer is a PRIVATE repository of its own, pushed before the trip. • THE LID QUESTION IS ANSWERED AND THE ANSWER COSTS NOTHING: a closed lid stops Sunshine's capture, because there is no panel to duplicate, so RDP is the headless path and both are declared. What remains is the DISPLAY TIMEOUT, 15 min on AC, which is the same class of failure and has not been tested: if a stream comes back black with the lid OPEN, that is the first suspect. • STILL OPEN OVER THERE, decided by whoever owns that machine: PasswordAuthentication is still yes on an sshd that only listens inside the tunnel, and BitLocker is FullyDecrypted on a laptop that will live in someone else's house.

  • Quality probe for the UFSCar VPN: no measured target (opened on 14/08/2026). The pill's hover popover measures latency/jitter/loss by pinging a host INSIDE the tunnel. For FAI the target is pinned and measured (200.136.209.236, see today's history); for UFSCar there is none, because I never brought that tunnel up with the panel ready to find out who answers in there. Meanwhile vpn stats-json falls back (the gateways from the tun routes) and, if nobody answers, the panel says "no probe". • DO NOT GUESS A UFSCar IP to "solve" it: a target that is not routed through the tunnel would measure the home internet. The -I <iface> protects against that (it turns into silence instead of a wrong number), but the silence is the symptom of guessing. • HOW TO CLOSE IT: connect (vpn connect ufscar), look at ip -4 route show dev tun0, ping candidates with ping -I tun0 <ip> and pin whichever answers in probe_candidates(), in modules/nixos/network/vpn.nix, together with the measurement, the way the FAI one is.

  • SEGMENTATION, deferred by the owner on 08/10/2026 (found in the router audit that day). • IoT and guests share the main LAN: the Samsung TV and an unnamed wlan0 device see every PC. The fix is a second SSID with its own zone, internet only. Deferred because every IoT device has to be re-joined to the new network, not because the risk is small. • The brother's PC is exposed on 2223 (DNAT since 18/08/2026, his request: Claude Code from his phone), with ONLY his Microsoft account password, no second factor and no fail2ban, and it sits in the main LAN, so a leaked password reaches the whole house. Recommended: close 2223 and give his phone a WireGuard peer; second best, key-only auth on that port. His PC, so his call too.

  • WireGuard peer fai-workstation (10.10.10.5): alive or legacy? (opened on 10/08/2026; on 08/10/2026 the owner confirmed it and pc-nizario are HIS, used now and then, so both stay) Measured with the new wg-status: in 17 days of router uptime it did not do ONE handshake. And it is not a forgotten passive peer: it has persistent_keepalive = 25, which exists precisely to keep the connection up. • The two possible readings, and they call for opposite actions: either the workstation has WireGuard stopped (and someone counts on that tunnel without knowing it never comes up), or the peer is residue and should leave the router by the zero-legacy rule. • DO NOT DELETE BEFORE CHECKING: the ~/FAI-workstation mount (rclone SFTP) comes up with the FAI VPN, not with this tunnel, so the peer LOOKS orphaned without being it. Check from over there with wg before deciding. • The notebook peer (.2) WAS REMOVED on 19/08/2026, for the reason this item is still open about: nobody could say which machine held its private key, and the wg zone reaches the whole LAN. That one had never handshaked at all, which made the call easy. THIS one has a persistent_keepalive, so somebody expected it to be up, and that is the difference. • MEASURED AGAIN on 13/09/2026, in the hardening audit: still no handshake, now across 14 days of a DIFFERENT uptime. Two independent measurements a month apart agreeing is as close to an answer as this item is going to get without asking the workstation, so the reading has shifted from "unknown" to "residue", and what is missing is the wg from over there to make it final.

  • WireGuard peers have no preshared_key (opened on 13/09/2026, from the hardening audit). All five peers authenticate with the key pair alone. The PSK is the standard second layer: it defends against harvest-now-decrypt-later, and it is what keeps a LEAKED private key from being enough on its own. • WHY IT WAS NOT DONE WITH THE REST: it is not a router-side change. A peer whose PSK is set on one end and not the other stops handshaking, so every client config has to be updated inside the same window, and two of those clients live in other people's houses (the mother's T480, my brother's PC). That is a scheduled operation, not a uci set. • ORDER THAT DOES NOT LOCK ANYBODY OUT: one peer at a time, starting with the phone, which is the one I hold and the one that is now the only admin source for the router (guides/router-hardening.md).

  • [~] adblock-fast did not come up after the router's boot of 07/10/2026 18:46 (found 08/10/2026). Enabled and in /etc/rc.d (S20), yet nothing was blocked for ~25 h (doubleclick.net resolved); a sudo /etc/init.d/adblock-fast restart brought it back at once (240394 domains). • CAUSE, CONFIRMED by sudo logread -e adblock: at 18:46:10 "failed to discover WAN gateway", retried at 18:46:16 when the WAN came up, both lists failed to download at 18:46:28 (the resolver was not ready yet), and it NEVER tried again. curl's --retry skips DNS failures, which it does not count as transient. • FIX APPLIED 08/10/2026, UCI only so the mirror shows it: curl_additional_param='--retry-all- errors --retry-delay 15' and curl_retry='5' (was 3), about 2 minutes of retrying at boot. adblock-fast appends both to its curl command (/lib/adblock-fast/adblock-fast.uc), and the router's curl 8.21 was tested with them before applying. • NOT DONE, on purpose: compressed_cache='1' would restore the list at boot with no network, but the list is 7 MB (~2 MB gzipped) against 3 MB of free flash, rewritten on every update. • TO CLOSE: after the next router reboot, ssh router 'ls /var/run/adblock-fast/' must show dnsmasq.servers. Then mark it done. • WHY IT MATTERS BEYOND ADS: phase 1 of the threat monitoring below puts the HaGeZi TIF threat list in this same service, so a silent failure here is a silent hole in the detector. The glance band's network page should say whether it is blocking.

  • THREAT MONITORING of the house's network, phases 1 and 2 (planned 08/10/2026; it absorbs the 13/09 "no log that survives a reboot" item below). The owner asked for a log of the LAN and the internet link that catches suspicious connections from any device, and chose: phases 1 and 2, alerts by ntfy plus a NETWORK page on the glance band plus a daily summary, and a 30-day retention. EVERY router step waits for an explicit go: it is the whole house's internet. • MEASURED 08/10/2026, what the router allows: Cudy WR3000 v1, OpenWrt 25.12.5, MT7981 (2x A53), 234 MB RAM (~118 MB free), 3.5 MB free flash, flow_offloading_hw='1'. So nothing inline like Suricata: after its first packets a flow leaves the CPU, and only what acts on NEW connections (DNS, IP sets, the conntrack NEW event) works on the router. banIP's floor is 256 MB, so only small feeds, with RAM measured after loading. • BLIND SPOTS no router setting removes: LAN-to-LAN traffic is switched (a worm moving PC to PC never crosses netfilter), and devices with their own DoH bypass dnsmasq (banIP's doh feed pushes them back to the router). • PARTLY DONE 08/10/2026, measured: HaGeZi TIF mini (239k) and HaGeZi DoH servers (3.3k) joined OISD Big and AdAway in adblock-fast, 240394 -> 339542 domains blocked; dnsmasq RSS 19 -> 27 MB, available RAM 84 -> 71 MB (TIF medium at 572k would not leave room for banIP). Proven: a TIF domain and dns.google now NXDOMAIN, 20 everyday domains still resolve, no UFSCar/FAI name in any list. Same day: the second DoH upstream moved from Google (unfiltered) to Cloudflare's security resolver, and drop_invalid went on. Later that day the router's syslog went to the desktop (log_ip, kept 30 days: docs/notes/network/router-log.md) and dnsmasq's logqueries went on, proven by a blocked domain logged as NXDOMAIN. Then banIP 1.9.0 with three small feeds (feodo and threat both ways, dshield inbound): 1460 elements, available RAM 72 -> 65 MB, every drop logged in both directions (which is the WAN drop logging), its log monitor off (ban_loglimit=0: the router's sshd is LAN-only and it would grep every DNS line). Its first inbound drops arrived within a minute. Phase 1 is DONE. Phase 2 the same night: glance-feed's threats source digests the log into SQLite (TIF and DoH blocks told from ads, banIP drops in and out, clients named over IPv4 and IPv6), the NETWORK page shows it, and ntfy pushes threat contacts, new devices and a daily summary (docs/notes/desktop/glance-feed.md). Still open in phase 2: CrowdSec. • PHASE 1, the router as a light sensor: HaGeZi TIF (mini or medium, by RAM) as its own adblock-fast list plus logqueries, since a device resolving a C2 domain is the strongest signal a house gets; banIP with Feodo, ThreatFox, Spamhaus DROP and doh, both ways, logging hits; rate-limited WAN drop logging (80, 443, 2222, 2223, 51820); DHCP events for a never-seen MAC; and log_ip to this machine. Each step is one UCI section, mirrored by router-sync pull, so it reverts by a uci delete plus a visible diff. • PHASE 2, on this machine, declarative: a syslog receiver (see the trap below), SQLite matching in the glance-feed pattern (block, hit, drop, DHCP event, each with the device by MAC and a known-devices list in the repo), 30-day purge, local only; CrowdSec on Caddy, sshd and the router's firewall log, its decisions exported as a list banIP pulls (no bouncer in the router's RAM); ntfy for C2 or malware hits, unknown devices and bans, a daily summary, and a NETWORK page on the band. • PHASE 3, only if 1 and 2 justify it: a managed switch with port mirroring and Suricata (ET Open) plus Zeek on a sensor. ONT-side mirroring sees every device, Wi-Fi included, but after NAT; LAN-side keeps the device but misses Wi-Fi. With TLS, metadata only (SNI, JA4, DNS). • PRIVACY: the DNS log is everyone's browsing; it never leaves this machine, is purged at 30 days, and an alert carries the threat and the device, never the query history. • SOURCES: banIP https://github.com/openwrt/packages/blob/master/net/banip/README.md, HaGeZi https://github.com/hagezi/dns-blocklists, CrowdSec on OpenWrt https://openwrt.org/docs/guide-user/services/crowdsec, mirroring for an IDS on OpenWrt https://forum.openwrt.org/t/can-you-port-mirror-on-openwrt-with-use-with-an-ids/121780.

  • The router keeps no log that survives a reboot (opened on 13/09/2026, from the same audit; phase 1 of the threat monitoring item above closes it). system.@system[0].log_size='128' KB in RAM and no log_ip, so the evidence of a compromise dies with the next power cut. Everything the audit fixed is PREVENTION; there is currently no DETECTION at all on this device. • THE FIX IS NOT A uci set: it is log_ip='192.168.1.10' plus a syslog receiver on the desktop, which is a NixOS module and belongs in modules/nixos/network/. The router half is one line and the useful half is the other one. • THE TRAP TO SIZE FIRST: the desktop is also the DNAT target for 80/443/2222, so a log pipe from the router into it is a service listening on the LAN, and it has to refuse everything that is not 192.168.1.1. A log receiver open to the house is a way to forge the record you installed it to trust. • AND IT IS WORSE THAN "NOT DURABLE": logread as v1cferr does not fail, it HANGS. Measured 13/09/2026 while looking for auth failures: the command returned empty on this side because the ssh call timed out, and TWO invocations were still sitting on the router afterwards, blocked on the log socket. So the log is not merely lost at the next reboot, it is unreadable without sudo and it swallows whoever tries. Any collector written for this has to run as root on the router side, and anything that shells out to logread unprivileged needs a timeout or it leaks processes.

  • [~] ACTUALLY TEST Wake-on-LAN (opened on 10/08/2026). The config is applied and the 40-enp7s0.link is generated with WakeOnLan=magic, but NONE of that proves the machine wakes up. Only powering it off and sending the packet proves it. • DO NOT CLOSE THIS ITEM WITHOUT THE TEST. It is the same mistake as today's HEVC, in reverse: there I closed by report without measuring; here the temptation is to close by applied config without triggering it. Correct config and real effect are different things, which is exactly what fw4 taught today by ACCEPTING a src_ip as a list and DISCARDING the section. • SCRIPT: sudo poweroff, then from the phone over WireGuard, ssh v1cferr@192.168.1.1 'sudo wake-desktop', and the machine has to come on. • CURRENT STATE, measured: power/wakeup = disabled and Wake-on: d. The .link is only applied by udev when the interface APPEARS, that is, from the next boot on. To arm it before that, without dropping the link: ethtool -s enp7s0 wol g. • IF IT DOES NOT WAKE, the next suspect is the BIOS: "Wake on LAN / Wake on PCIe" has to be on there too. The OS arms the NIC; the motherboard decides whether it accepts the signal. • THIS WILL NEVER COVER A POWER OUTAGE, and that is not a WoL failure: a real cut takes away the +5VSB and the NIC loses the armed register. For "the power went out" the answer is Restore on AC Power Loss in the BIOS, and then WoL becomes irrelevant, because the machine turns itself on.

  • Tray: the TOOLTIP, which does not exist anywhere on the bar (opened 30/07, narrowed 16/08/2026). Everything else in this item was DONE and its reasoning now lives in notes/desktop/bar.md and notes/desktop/quickshell.md: the click, the hover fix (PopupWindow to a layer surface, Hyprland#6682), the visible-hover contrast tokens, the XEmbed bridge and the ghost-icon diagnosis. The item had been carrying 70 lines of finished work, which is the drift rule 16 describes. • Quickshell's SNI exposes tooltipTitle/tooltipDescription ready to use; the pattern to follow is the popovers (an anchored PanelWindow, see UsagePopover.qml). • MEASURED on the live items, and it is why a single field will not do: Discord publishes a ToolTip with the title "Discord", Sunshine leaves it EMPTY and only has Title="sunshine", and the icon from the XEmbed bridge has NEITHER. So the cascade is tooltipTitle to title to id, and for the bridged ones it has to resolve the X11 WM_CLASS. • MEASUREMENT NOTE: counting tray items with busctl --user list | grep StatusNotifierItem gives 0, which is FALSE, because an app registered under a unique name (:1.82) does not match. The authoritative source is the watcher's RegisteredStatusNotifierItems.

  • IMPERMANENCE on the Kingston: my idea (30/07), inspired by https://github.com/Misterio77/Foundry. An ephemeral root (tmpfs or a subvolume wiped at boot) plus an EXPLICIT list of what persists. It fits two things this repo already has: rule 6 (Nix = app+config; state is not declared) would stop being a convention and become ENFORCED by the system, since whatever is not declared as persistent simply does not survive the boot; and it absorbs what used to be a separate one-line item ("check whether encrypted declarative state is possible"), because the natural pair is impermanence + LUKS, and LUKS is already decided against below. POINTS TO DECIDE FIRST, measured today: the 567 GiB of non-Nix (Bottles 319, Jellyfin 132, Games 47) are LARGE and legitimate state. Impermanence does not erase them, but it forces declaring every path, and getting the list wrong means losing a save or a prefix on reboot. Candidates: the impermanence module (nix-community) or the Foundry scheme. THE MIGRATION ALREADY HAPPENED (01/08/2026) without turning impermanence on, so the original premise ("do it together, on a fresh install") has expired: now it is converting a machine in use, which was precisely the path I wanted to avoid. The btrfs layout saves most of the cost: what is missing is @-blank and the list, not a reinstall. DECIDED on 01/08/2026, while putting hosts/nixos-kingston together (the LAYOUT is already done): • btrfs YES, not out of taste, but because /nix and /persist need to be separate volumes FROM the install onward; flat ext4 would cost a second reinstall. Subvolumes created: @ @home @nix @persist @log @swap. It matches Foundry (root = a subvolume wiped at boot, not tmpfs; tmpfs would cap the root at the 15 GB of RAM). • LUKS NO: a passphrase at boot would kill the autologin that Sunshine depends on for remote access after a power outage. It is the deliberate difference from Foundry. • @home as a PERMANENT subvolume (Foundry does not have one), an intermediate stage on purpose: turn impermanence on at the root first, and only then decide whether to extend it to home. It locks nothing in: extending it is wiping @home through the same mechanism, with no reinstall. It is the answer to the 567 GiB risk above, since declaring everything on the first try is where you lose. • /var/lib is NOT a subvolume, and that is deliberate: if it were permanent, nothing would force declaring anything. A consequence to remember: the service state the cutover copies in there (uid-map, NetworkManager, bluetooth...) is WIPED on reboot once the feature lands, so it has to move to /persist and be declared. That is the work, not a bug. /var/lib/sbctl IS FIRST ON THE LIST and the only one that makes the machine NOT BOOT if it is forgotten: those are the Secure Boot keys (see modules/nixos/core/secureboot.nix). Without them the next switch does not sign Limine, and with Secure Boot on the firmware refuses the bootloader. Recovery = turn SB off in the BIOS + sbctl create-keys + enroll-keys -m again, with the BIOS in Setup Mode. Declare it BEFORE turning the ephemeral root on. MISSING only: the @-blank snapshot (the base of the rollback) and the persistence list. The blank is NOT now-or-never, since an empty subvolume created later is identical to a blank snapshot. ── HAVING READ THE FOUNDRY CODE (02/08/2026), what changes in the plan ────────────── PREMISE CORRECTION: btrfs is NOT mandatory for impermanence. The most common path in the community is a tmpfs root, and with a bind mount it even runs on ext4. The choice is still RIGHT, but for the right reason: btrfs gives an ephemeral root without spending RAM, and RAM is exactly what is short here (15 GB). Do not repeat "it is mandatory". It is only two files in Foundry, and the paths below are THEIRS, not paths in this repo: Foundry:hosts/common/optional/ephemeral-btrfs.nix (the wipe) and Foundry:hosts/common/global/optin-persistence.nix (the list). The rest of the persistence is DISTRIBUTED: each service module declares what it needs to keep (openssh.nix, podman.nix, jellyfin.nix...). That is the pattern to copy, and it matches modules/nixos/services/*.nix. /srv IS THE BIGGEST RISK, and it was not written down: it is NOT a subvolume, it lives in @, and it is where the Jellyfin library lives (132 GiB). Foundry persists /srv explicitly. Turning the ephemeral root on without that ERASES the library on the first boot. Before sbctl on the list. Foundry's minimum list, all of it applicable here: /etc/machine-id (a file, not a dir), /var/lib/systemd, /var/lib/nixos, /srv. The /var/lib/nixos one is the UID/GID MAP, and losing it means uid reassignment, which is the SAME class of bug that broke Docker/Postgres/Sunshine on the cutover (see the cutover damage). /var/log does NOT go in: Foundry lists it because it has no subvolume for it; here @log already handles it, and declaring both would put a bind mount on top of the subvolume. neededForBoot = true on /persist (Foundry sets it; today it is false in disko). dont-wipe: a marker file at the top of the filesystem that makes the script SKIP the wipe. COPY IT, it is the difference between "boot loop" and "I touch a file from the live USB". SYSTEMD INITRD FIRST, in a separate commit: Foundry runs boot.initrd.systemd.enable and the script has two paths, the postDeviceCommands one being the legacy path. Turn the systemd initrd on by itself, reboot and confirm, and only then add the wipe. Two boot risks in a single commit is how you end up not knowing which of the two broke. Adapt the names in the wipeScript: Foundry uses root/root-blank/persist; here it is @/@-blank/@persist. Getting that wrong does not produce an evaluation error, it produces a broken boot. Copy the impermanence#254 workaround too (/var/lib/private at 0700 + RemainAfterExit = false on systemd-tmpfiles-resetup), otherwise a service with DynamicUser breaks. And @snapshots (btrbk) survives by design: it is top-level, it does not live inside @. ── HOW TO DE-RISK THE SWITCH (23/08/2026), because the fear here is the correct one ── The premise of this item is a persistence list that has to be right on the FIRST boot, and it does not have to be: the ORDER is what makes a wrong list cheap. • IN A VM FIRST, and there is a tool for exactly this: nix build .#nixosConfigurations.ex-b560m-v5.config.system.build.vmWithDisko formats VIRTUAL disks with THIS repo's disko layout, so the same @ @home @nix @persist exist and a wrong list costs an image instead of the 132 GiB Jellyfin library. nixos-rebuild build-vm is the WRONG tool here: with a disko layout it hangs waiting for the root partition (disko issue #668). • BTRFS GIVES A ROLLBACK THAT TMPFS CANNOT, and that is what dissolves the fear: the wipe is snapshot @ -> @-old-N BEFORE restoring @-blank, keeping the last N roots. A path forgotten on the list stops being a loss and becomes "mount the previous snapshot and copy what was missing". Retaining N is the most important line of the wipeScript, more than the wipe itself. • ONE BOOT RISK PER COMMIT, which extends what is already written above: (1) the script plus dont-wipe, inert; (2) systemd initrd alone, reboot, confirm; (3) /persist with neededForBoot and the declared list, reboot WITHOUT the wipe, which proves the binds work while the system is still persistent; (4) only then the wipe. • PROVE A RESTORE BEFORE ANY OF IT: sudo restore-state --verify (drill D4) once the local backup is on; until then there is nothing to restore FROM. Impermanence is rule 6 becoming law, and turning that law on while the state has no backup at all is the one version of this that costs real money. THE NEW STORAGE COMES FIRST. • MEASURED 23/08/2026 IN THE DISKO VM, and it confirms the /srv worry with evidence: systemd creates srv, var/tmp, var/lib/machines and var/lib/portables as SUBVOLUMES INSIDE @, so a wipe of @ takes them along. btrfs subvolume list / on a fresh install shows them at "top level 256", which is @ itself. /persist also still has neededForBoot = false, and it has to be true before the wipe lands.

  • Turn off every LED on every piece of hardware in AFK mode. The GPU's LEDs are declared and applied AT BOOT (my.rgb.color, default 000000); what is still open is the AFK trigger and every device that is not the GPU. • THE BLOCKER IS GONE (19/09/2026). The Steel Legend B580's RGB is reachable, and it took no reverse engineering in the end: the LED sits behind the card's AMC at 0x36 on the GPU's OWN internal I2C bus, not on the chipset SMBus, and it answers the protocol that OpenRGB's ASRock GPU controller already implements for the Radeon Steel Legend cards. Upstream took it, the packaging debt is declared as an overlay carrying master plus the four patches, and the colour is applied by a boot service. The whole story, including when that overlay dies: docs/notes/hardware/gpu-rgb.md. • THE FANS LEFT THIS ITEM (14/10/2026), and the line here that said they stay uncontrollable was wrong the moment it was written down as permanent. They are controllable, over a path that has nothing to do with the LEDs: the PCODE mailbox, through a patched xe, plus the board's own NCT6798D for the other two. Measured on the card, 0 to 3926 RPM: docs/notes/hardware/fans.md. • WHAT IS STILL TRUE is that this takes TWO programs, OpenRGB for the light and CoolerControl for the air, because no single one speaks both i2c LEDs and hwmon fans. One program for both was the original hope and it is not on offer.

  • [~] Passwordless remote maintenance on the router and on the switch (OpenWrt). • ROUTER, done: SSH was already key-based (ssh v1cferr@192.168.1.1 runs in BatchMode), and what was missing was sudo. Today the NOPASSWD ones are /sbin/reboot, /usr/sbin/nft, /sbin/uci, /etc/init.d/dnsmasq and /etc/init.d/firewall (this last one came in on 10/08/2026, with the justification and the method in the history). • A DECISION TO KEEP: an arbitrary command STILL asks for the password. Widening it to (ALL) NOPASSWD: ALL would be the change that actually escalates privilege, since the current ones do not escalate because nft already gives the same reach. Only add a binary with a reason, one at a time. • /usr/bin/wg-status came in along with them (a read-only wrapper over wg show); the whole wg binary does NOT go in, because wg set replaces a peer key. • MISSING: the SWITCH, which has never been touched. • NONE OF THIS IS MIRRORED: router-sync only covers /etc/config/. Sudoers and the SSH key live outside the repo, and the router's /home/ does not even survive a sysupgrade.

  • [~] Claude Code, what is LEFT of the two accounts (11/08/2026). The structure was declared in modules/home/shell/claude-code.nix and the entry is in the august history: the claude-fai/claude-pessoal wrappers, claude-pick, a versioned settings.json, a shared projects/ and plain claude falling through to FAI. Three ends are still open, and none of them is declarable: • The /login for each account. I did NOT restore .credentials.json (neither the one from the Arch backup nor the one in ~/.claude): a token is not something you copy with a script, and the Arch one was 7 weeks old, from a machine that is decommissioned. Check later without spending quota: claude-fai doctor says whether the account is signed in. • PRUNE THE REST OF ~/.claude. It stopped being an account and became just the archive, but history.jsonl, settings.json, settings.local.json, sessions/, shell-snapshots/ and plugins/ were left there (~40 MB outside projects/). What was worth keeping was copied to ~/.claude-fai; the rest is legacy (rule 16). DO NOT PRUNE BEFORE the new account proves it walks: until the FAI /login happens, those leftovers are the only place where that account's state exists. And NEVER touch projects/, which is the archive, the target of both accounts' symlinks. (A third bullet lived here until 16/08/2026: the header of modules/nixos/services/claude-code.nix claimed the user's settings.json "CANNOT become a symlink", which read as contradicting the module that links it. The rule 2 sweep rewrote that header, so the drift is gone.)

  • The Azure MCP has nothing to work ON yet (14/08/2026). The server is declared, connected and authenticated (see the august history), but its 68 tools operate over a SUBSCRIPTION, and the day-to-day tenant (FAIUFSCar) is a directory only: azmcp subscription list answers 200 with an EMPTY list. Who has a subscription is the BHS tenant, and getting into it requires MFA: az login --tenant 92247c24-8a8c-47f3-a7f1-85df939ad4b6, which the browser resolves. Only after that is it possible to say whether the MCP pays for itself here or whether az alone would have been enough; until then it is ALIVE and IDLE, which is different from broken. The App Registration does NOT depend on this: it already works through az ad on FAIUFSCar, which was the goal.

  • Release jump 26.05 to 27.05 (~may/2027). It is NOT a reinstall: it is TWO STRINGS in flake.nix, nixpkgs.url (nixos-27.05) and home-manager.url (release-27.05), which change TOGETHER (the HM release branch matches the base, otherwise there is an option mismatch). The other ~9 inputs have inputs.nixpkgs.follows = "nixpkgs" and come for free: the dedup that already exists because of the lock size is what makes the jump trivial, 1 input changes and 9 follow. Without it, each flake would drag its own nixpkgs 26.05 and I would end up with two bases coexisting after the jump. upgrade NEVER makes that jump, and that is a feature: nix flake update only moves INSIDE the pinned branch, and into nixos-26.05 only BACKPORTS come, cherry-picked CVE/bugfixes that a maintainer marks with the backport release-26.05 label. A new package version does NOT come in, except for the browser and the kernel (upstream only gives security support to the new version, so backporting a Firefox patch would mean rewriting Firefox). And nixos-26.05 (the channel) is not release-26.05 (the branch): the channel is a pointer that only advances after Hydra builds and the test suite passes, the same gating as nixos-unstable. The stateVersion DOES NOT CHANGE, not on the jump, not ever. It stays "26.05" forever in hosts/ex-b560m-v5/default.nix and modules/home/default.nix. The name misleads: it is not "the version of my system", it is "the NixOS version my STATE ON DISK is compatible with". 54 nixpkgs modules read that value, and the canonical case is postgresql.nix, which picks the Postgres MAJOR from it (versionAtLeast stateVersion "26.11" gives postgresql_18; "25.11" gives postgresql_17, and so on). Bumping it makes the module point at a major that does NOT READ the existing datadir: the service does not come up, and if something reinitializes the cluster the database is gone. A generation rollback does not save you: it brings back the SYSTEM, not the /var/lib that was already touched (the same class as the cutover damage, when copying /var/lib broke Docker/Postgres/Sunshine silently). That is why it exists in the config even being immutable: it is the state's BIRTH CERTIFICATE, not a button, and the modules need to know when the state was born precisely because they cannot migrate it on their own. It only changes if the release notes say so, and together with the manual migration (pg_upgrade and the like). Use nixos-rebuild boot and NOT switch: it applies on the next boot and the 26.05 generation stays in Limine's menu as the emergency exit. And wait ~2 to 4 weeks after the release (the branch stabilizes as the backports arrive); the cost of waiting here is low, because what I want fresh already comes through unstable.* and through the direct upstream inputs.

  • The webcam's ONE remaining test, and it is a cable (opened 04/09/2026). The viewer and the diagnosis are declared and the camera is recognized, but it does not stream: it fails at the UVC probe control with -71 and reboots itself, and the measurements that rule out the driver, the format, the bandwidth and USB autosuspend are in notes/hardware/webcam.md. It is here and not in a module because the remaining suspicion is not declarable: the camera hangs off a bus-powered USB 2.0 hub that advertises 100 mA, shared with a USB Audio and HID device, and its own descriptor asks for 500 mA. IT IS INTERMITTENT, so ONE result decides nothing: 1 stream came through in 17 attempts on 04/09/2026, and the one that worked left no trace in the log at all. Whatever is tested has to be tested several times, in both directions, or the conclusion is noise. THE SOFTWARE SIDE IS CLOSED, so what is left is four physical steps, cheapest first. The probe for all of them is the same, and it must write to a path the USER owns: v4l2-ctl -d /dev/video0 --stream-mmap --stream-count=3 --stream-to=~/cam.raw, then check that the file is NON-EMPTY, because v4l2-ctl exits 0 even when the stream fails. 1. Pull the USB Audio and HID device off the hub, leaving only the camera. The only current-budget test that does not need a different port. 2. A different port on the same hub. 3. A REAR MOTHERBOARD PORT, no hub. This is the decisive one; bus 2, the USB 3 root, has no devices on it at all, so ports are free. 4. Boot Windows and open the camera there. That settles "is it the device" without any Linux in the picture: if it fails there too, this is an RMA and not a configuration. The trap to recognize at every step is that the MICROPHONE keeps working the whole time, so "the camera is fine, PipeWire sees it" is not evidence of anything.

No Headings