Open items
Edit on GitHubWhat 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) BEFOREuser@1000(the scope isAfter=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.2hyprland.shutdownandexec-shutdownfire only from theexitdispatcher; SIGTERM goes straight tostopCompositor()(Compositor.cpp:92). • THE FIX: UWSM runs Hyprland as auser@service, so systemd stops the clients first. greetd'sinitial_sessionreplaces LightDM's autologin, which drops the X server thatservices.xserver.enablebrings 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 isdisplay-manager.servicetoo), 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.nixrecolors 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 frommy.theme.palette, never a typed hex. • THE TRAP: a user sheet loses to Zen's author sheet, so a new line without!importantlooks 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.nixtargets 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 ahosts/<host>/home.nixthat addshome-manager.users.v1cferr.imports. • THE ROUTER'S DHCP RESERVATION still names this machinev1cferr-nixos. Renaming it is a change on the device (uci, thenrouter-sync pull), not a layout one. -
The quarterly app review, first due 31/12/2026 (opened 29/09/2026).
nix run .#usage-auditlists 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
HOMErepo 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 theforget --prunehad 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 withcheck --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:enp7s0negotiated 100 with anr8169, 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.ethtoolis 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 readmy.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.
convcoandprose-stylerun at the commit-msg stage, whichnix flake checkcannot 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 showeven displays192.168.1.1as the endpoint while the file saysvpn.v1cferr.dev. The protocol: phone hotspot, tunnel coming up on its own,ssh t480answering, 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 answeringSUNSHINE_SERVER_BUSYforever, and Moonlight then refuses to open a new session, so from the client the host is simply broken. The remedy is arestartand 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 readsBUSYplus 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> transferbarely 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.6answersDesktop, 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_bitrateIS 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:PasswordAuthenticationis stillyeson an sshd that only listens inside the tunnel, and BitLocker isFullyDecryptedon 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. Meanwhilevpn stats-jsonfalls 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 atip -4 route show dev tun0, ping candidates withping -I tun0 <ip>and pin whichever answers inprobe_candidates(), inmodules/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
wlan0device 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 andpc-nizarioare HIS, used now and then, so both stay) Measured with the newwg-status: in 17 days of router uptime it did not do ONE handshake. And it is not a forgotten passive peer: it haspersistent_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-workstationmount (rclone SFTP) comes up with the FAI VPN, not with this tunnel, so the peer LOOKS orphaned without being it. Check from over there withwgbefore deciding. • Thenotebookpeer (.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 thewgzone reaches the whole LAN. That one had never handshaked at all, which made the call easy. THIS one has apersistent_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 thewgfrom 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 auci 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.netresolved); asudo /etc/init.d/adblock-fast restartbrought it back at once (240394 domains). • CAUSE, CONFIRMED bysudo 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--retryskips 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'andcurl_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 showdnsmasq.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'sdohfeed 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 anddns.googlenow 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, anddrop_invalidwent 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'slogquerieswent 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'sthreatssource 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 pluslogqueries, since a device resolving a C2 domain is the strongest signal a house gets; banIP with Feodo, ThreatFox, Spamhaus DROP anddoh, both ways, logging hits; rate-limited WAN drop logging (80, 443, 2222, 2223, 51820); DHCP events for a never-seen MAC; andlog_ipto this machine. Each step is one UCI section, mirrored byrouter-sync pull, so it reverts by auci deleteplus 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 nolog_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 Auci set: it islog_ip='192.168.1.10'plus a syslog receiver on the desktop, which is a NixOS module and belongs inmodules/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":logreadasv1cferrdoes 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 withoutsudoand it swallows whoever tries. Any collector written for this has to run as root on the router side, and anything that shells out tologreadunprivileged 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.linkis generated withWakeOnLan=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 asrc_ipas 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 = disabledandWake-on: d. The.linkis 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/tooltipDescriptionready 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 withbusctl --user list | grep StatusNotifierItemgives 0, which is FALSE, because an app registered under a unique name (:1.82) does not match. The authoritative source is the watcher'sRegisteredStatusNotifierItems. -
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
impermanencemodule (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@-blankand 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. •@homeas 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/libis 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/sbctlIS 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 -magain, with the BIOS in Setup Mode. Declare it BEFORE turning the ephemeral root on. MISSING only: the@-blanksnapshot (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) andFoundry: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./srvIS 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/srvexplicitly. 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/nixosone 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/logdoes NOT go in: Foundry lists it because it has no subvolume for it; here@logalready handles it, and declaring both would put a bind mount on top of the subvolume.neededForBoot = trueon/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 runsboot.initrd.systemd.enableand the script has two paths, thepostDeviceCommandsone 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 usesroot/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/privateat 0700 +RemainAfterExit = falseon 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.vmWithDiskoformats VIRTUAL disks with THIS repo's disko layout, so the same@ @home @nix @persistexist and a wrong list costs an image instead of the 132 GiB Jellyfin library.nixos-rebuild build-vmis 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 issnapshot @ -> @-old-NBEFORE 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 plusdont-wipe, inert; (2) systemd initrd alone, reboot, confirm; (3)/persistwithneededForBootand 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 createssrv,var/tmp,var/lib/machinesandvar/lib/portablesas 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./persistalso still hasneededForBoot = 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, default000000); 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 patchedxe, 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.1runs in BatchMode), and what was missing wassudo. Today the NOPASSWD ones are/sbin/reboot,/usr/sbin/nft,/sbin/uci,/etc/init.d/dnsmasqand/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: ALLwould be the change that actually escalates privilege, since the current ones do not escalate becausenftalready gives the same reach. Only add a binary with a reason, one at a time. •/usr/bin/wg-statuscame in along with them (a read-only wrapper overwg show); the wholewgbinary does NOT go in, becausewg setreplaces a peer key. • MISSING: the SWITCH, which has never been touched. • NONE OF THIS IS MIRRORED:router-synconly covers/etc/config/. Sudoers and the SSH key live outside the repo, and the router's/home/does not even survive asysupgrade. -
[~] Claude Code, what is LEFT of the two accounts (11/08/2026). The structure was declared in
modules/home/shell/claude-code.nixand the entry is in the august history: theclaude-fai/claude-pessoalwrappers,claude-pick, a versionedsettings.json, a sharedprojects/and plainclaudefalling through to FAI. Three ends are still open, and none of them is declarable: • The/loginfor 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 doctorsays whether the account is signed in. • PRUNE THE REST OF~/.claude. It stopped being an account and became just the archive, buthistory.jsonl,settings.json,settings.local.json,sessions/,shell-snapshots/andplugins/were left there (~40 MB outsideprojects/). 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/loginhappens, those leftovers are the only place where that account's state exists. And NEVER touchprojects/, which is the archive, the target of both accounts' symlinks. (A third bullet lived here until 16/08/2026: the header ofmodules/nixos/services/claude-code.nixclaimed the user'ssettings.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 listanswers 200 with an EMPTY list. Who has a subscription is theBHStenant, 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 whetherazalone 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 throughaz adon 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) andhome-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 haveinputs.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.upgradeNEVER makes that jump, and that is a feature:nix flake updateonly moves INSIDE the pinned branch, and intonixos-26.05only BACKPORTS come, cherry-picked CVE/bugfixes that a maintainer marks with thebackport release-26.05label. 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). Andnixos-26.05(the channel) is notrelease-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. ThestateVersionDOES 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). Usenixos-rebuild bootand NOTswitch: 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 throughunstable.*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
-71and 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.