Ideas¶
Things considered, references, and what has not become a decision yet. What already did is in history/; what is still to do is in open-items.md.
Quickshell: DECIDED, I migrated everything to Quickshell. Customizable in QML with hot-reload; Hyprland became hot-reload too (hyprland.lua through mkOutOfStoreSymlink). For inspiration: https://github.com/Misterio77/Foundry Nix wallpapers: https://github.com/NixOS/nixos-artwork/tree/master/wallpapers Centralized themes:
home/desktop/palette.nix(my.theme). nix-colors was discarded (archived + base16 caps it at 16 colors).
Blue light filter and eye strain¶
If hyprsunset gains a gradual transition (the Graduated transition issue, opened on
08/08/2026), the 13 profiles in home/desktop/hyprsunset.nix collapse into 3 (day, night
and late night) and the tool interpolates. Today it jumps abruptly, and the small steps are
what disguises the jump.
The priority order against eye strain is the opposite of the intuition: reducing
BRIGHTNESS comes before color temperature, and night mode does not replace adequate
brightness. That is what motivated a ddc.nix that no longer exists: gamma darkens the signal, not
the light being emitted. The DDC/CI brightness curve was BUILT and REVERTED on 08/08/2026,
so there is no module to open; it worked, but only on the main monitor, and the HDMI TV has no automatic path. See the august
history. What is still open:
- Progressive gamma from 18:00 on, the next step if the color curve is not enough, and it is the one that would finally apply the priority above. On 13/08/2026 the post-18:00 curve came down ~200 to 400K per step (the 2nd descent) and the BRIGHTNESS axis was left out on purpose: automatic dimming through gamma existed and was reverted on 08/08 along with the DDC. The Kelvin curve got close to the useful floor, since it already crosses the ~3200K where the color ruins media, so going further down in K makes the color worse without a proportional relief. If the discomfort comes back, the adjustment is gamma, not more orange.
- Bias lighting, a light behind the monitor. It is the recommendation that shows up most in the literature and the only one that is not software: it reduces the contrast between the screen and a dark wall.
- PWM: a monitor that dims through PWM flickers at low brightness and makes fatigue worse. Check whether the panels are flicker-free before lowering the backlight too far.
NetBird: a CGNAT contingency, not a replacement¶
https://github.com/netbirdio/netbird, WireGuard with a control plane: peer discovery, automatic NAT traversal, per-device ACL and SSO. Evaluated on 10/08/2026, the same day the Moonlight direct access landed.
DECISION: stay with the router's WireGuard alone. Do not switch, and do not run both.
The framing that matters is not "switch or not", it is that the current design rests on a single premise: Alcans gives a real public IP. Port forwarding, WireGuard on the router and DDNS all three depend on it. If that changes, they fall TOGETHER, in the same minute. NetBird is the plan for that day, and this analysis exists so it does not have to be redone under pressure.
Why not now¶
- It requires an agent on every machine, which is exactly what was refused: the FAI
notebook already runs nxBender + openconnect, and the NetBird agent manages routes
dynamically, the same class of conflict, harder to debug than a static
wg-quick. - It reintroduces the relay. It falls back to Relay/TURN when P2P fails, which is Tailscale's DERP under another name, the reason Tailscale went out on 08/08. And on the FAI network, which drops SYN-ACK, a P2P failure is the LIKELY scenario: it would relay precisely there.
- The router cannot be the server. The management side asks for "1 CPU and 2 GB"; the WR3000 has 128 MB of RAM and ~1.3 MB of free flash. The agent runs on OpenWrt, the control plane does not.
- Self-hosting creates a circular dependency: management + signal + relay would go to the PC, which is the machine you are trying to reach. Today that loop does not exist, since the router is a separate device that is always on. Using their cloud solves the circle by adding the third party back.
What it would actually solve (and why it stays written down)¶
- CGNAT, the trigger. With a relay, it survives what would kill everything today.
- Adding a peer without editing UCI by hand. Today that is SSH into the router;
router-syncis pull-only. Real friction. - Per-device ACL. Today the Sunshine rule trusts the ENTIRE
10.10.10.0/24range, so the phone, the notebook and the workstation have exactly the same access. This is a real limitation of the current design, independent of CGNAT.
Trigger to reopen¶
The home public IP no longer answering from outside. A test that does not lie (an independent external vantage point, never from inside the LAN, because the ISP does hairpin):
curl -s "https://check-host.net/check-tcp?host=<ip>%3A2222&max_nodes=3" \
-H "Accept: application/json"
"Everything on the router": what it already is, and what it cannot be¶
My idea (10/08/2026): concentrate services on the router so that nothing stops when the PC goes down (power outages and the like).
GOOD NEWS: the ACCESS layer already is exactly that. These run on the router and do not
depend on the PC: WireGuard (wg0, the router is the server), DHCP, DNS with adblock-fast
and https-dns-proxy, firewall/SQM, and Wake-on-LAN (/usr/bin/wake-desktop). The PC can be
off and the VPN still comes up and the home network still works.
What CANNOT migrate: Jellyfin, Sunshine, Caddy, qBittorrent and Ollama. It is not a matter of willingness, it is 128 MB of RAM and 1.3 MB of free flash. Screen streaming and media transcoding do not fit by any order of magnitude.
And the bottleneck in the "power went out" scenario is NOT the router, it is the PC
coming back on its own. That is BIOS (Restore on AC Power Loss = Power On), not Nix,
and it is not declarable in this repo. WoL does NOT replace it: after a real power cut the
NIC loses the state armed by ethtool, and it only comes back if the BIOS itself keeps
wake enabled. The right order to attack it: (1) the BIOS powers back on by itself, (2) WoL
as the rescue for a normal shutdown, (3) a UPS on the router and modem if the intent is to
keep the internet up DURING the outage, but that does not turn the PC on, because the PC is
not on the UPS.
router/uci/etherwake.conf is DEAD CONFIG: name='example',
mac='11:22:33:44:55:66', the factory placeholder of the LuCI app, never filled in. What
works is /usr/bin/wake-desktop, with the MAC baked in. Do not trust the LuCI screen.
DeepSeek Harness: the local agent works, the harness is the risk¶
https://github.com/deepseek-ai/deepseek-harness, an agent harness where "everything is a
plugin" over the Cordis kernel: models, tools, skills, sandbox, storage, loop and the UI are
all swappable rows in a patch tree. It ships a Web UI, a one-shot headless mode and a
Python SDK. Tested on 20/08/2026 against the local Ollama.
DECISION: not adopted. It stays a scratch experiment, with no NixOS module.
This evaluation separated two questions that look like one. "Can this machine run a local coding agent" is now answered YES, and that answer outlives the harness. "Should dsh be the harness" is a NO with a reopen condition.
What the test proved, and this part is keepable¶
Two real tasks finished end to end with qwen3.5 (9B) on the B580: find and fix a bug in a
file, then write a unittest suite, run it with python3 and report the outcome. The second
took 25 s over 5 requests. The 9B never fumbled a tool schema, which is exactly what I
expected it to fail at.
The number that decides everything: the harness sends around 8.2k tokens of FIXED prefix
per turn, 1051 of system prompt plus 7128 for 25 tool definitions (bash, edit,
subagent, workflow, web_search, skill and others). Out of a 32k window, a quarter is
spent before a single file is read. That is a ceiling on ANY local agent here, not a dsh
detail, and it is why OLLAMA_CONTEXT_LENGTH had to move off the 4k default.
Why it is not adopted¶
- The documented quickstart does not boot.
npx @deepseek-ai/dshinstalls the launcher at 0.1.0-rc.7 and every bundle at rc.8, because a caret on a prerelease accepts it, and the boot dies on--expose-internals is required for HMR serviceeven though the headless bundle marks that rowdisabled: true(confirmed with--dump-config). It only runs by callingnode --expose-internalsonbin.jsby hand. - A documented config path is refused in SILENCE.
reasoningEffortswith theoffkey, documented in both of its spellings, makes the wholellm-pi-aisection be discarded. No error is printed: the only symptom isNO_ADAPTER: no adapter registered for provider "ollama"at request time, and finding it took bisecting the YAML field by field. That key is not cosmetic here, because with thinking ON the model drops its answer into thereasoningfield and returns an EMPTYcontent, intermittently and worst right after a tool result. The workaround is to declare another level whose wire spelling isnone, andPARAMETER thinkdoes not exist in Ollama 0.32.3, so it cannot be baked into the model. - Version 0.1.0-rc.8, one week old, with "THERE WILL BE COMPATIBILITY-BREAKING CHANGES" in the README. Issues are disabled, feedback goes through Discussions.
- It is imperative by design.
dsh pluginforwards to pnpm and installs into$DSH_HOME/profilesat runtime, so its plugins cannot be declared in this repo. - Headless approves everything. It wrote files and ran
python3with no gate at all, so it belongs in a disposable workspace and nowhere near a real checkout.
What is worth stealing from it¶
The session log is append-only and records everything the model saw, context injections
included, which is the honest answer to "why did it do that". And the profile model, an
ordered stack of patch layers with --dump-config to inspect the composed tree before
booting, is a good idea whatever harness ends up winning.
Trigger to look again¶
A tagged release that is not a release candidate, with the npx quickstart booting exactly
as written. Until then, the local-model half of this is served by Ollama on its own.
The CI, and what would make it last (researched on 23/08/2026)¶
The question behind this section is not "is NixOS good today", it is whether this repo can keep being the SSOT of my infrastructure until 2032. The blind spots of the GATE were closed the same day (see the august history and notes/repo/flake.md); what is below was researched and NOT decided, in the order the research put it.
- DONE the same day:
nix-community/cache-nix-action@v7in the gate workflow, keyed onflake.lock, withgc-max-store-size-linux: 5Gand a weekly purge to stay inside GitHub's 10 GB per repository. The canary deliberately gets NO cache, because resolving every input at HEAD would miss every week and evict the gate's entry..github/dependabot.ymlwent in beside it, since a hash pin does not follow a security release on its own. Both are in notes/repo/flake.md. - DONE the same day, and the shape got simpler than this sketch: the canary is
.github/workflows/canary.yml, weekly, with--recreate-lock-file --no-write-lock-fileinstead of one--override-inputper input. Same guarantee, one flag: every input at its branch head and the committed pin untouched. MEASURED at 1m41s locally, all checks passing, so today nothing upstream is broken. The reasoning is in notes/repo/flake.md, and the external link half is in notes/repo/link-checker.md. system.build.toplevelin the CI: researched and NOT recommended. A free runner has ~20 GB free and this closure drags Quickshell (Qt/C++) in, while the cache above caps at 10 GB. The three ways out arenix-fast-build --skip-cachedplus an action that reclaims disk, a self-hosted runner on the Kingston itself (attractive, since the machine is always up, but on a PUBLIC repo it has to be restricted topushand neverpull_request, or a fork runs code on my PC), or not doing it. Rule 8 already requiresnixos-rebuild buildbefore the switch, which is the same guarantee bought with hardware I already own.- Automating the
update:update-flake-lockv3 no longer installs Determinate Nix, so the vendor objection is gone, and Renovate does updateflake.lock. Both trip on the PRIVATE input: they would need the deploy key of Plan B. Rule 13 keepsupdateas the USER, and the canary above is what says when it is worth running. - DONE the same day, and the prediction about qmllint was right: the Lua goes through
lua-lsreading the repo's own.luarc.json, the loose Python throughruff, and the QML through a parse check that keeps ONE qmllint category, because the rest of it produced 2267 findings that are almost all false. The measurements are in notes/desktop/quickshell.md. - Tags for the milestones. There are two tags in the repo and neither marks a state. Tagging the cutover, the GPU swap and the day impermanence lands makes "the state that worked" addressable, which is worth more the further 2032 gets.
- DONE the same day, all three stages: D1 is the weekly boot test, D2 is
nix run .#disko-vm(the real layout formatted from scratch, with a report), and D3 is the secrets rehearsal from a clean clone plus the vault. The protocol is guides/disaster-recovery.md. What remains an idea is automating D3, which would need a throwaway age key and a fakesecrets.yamlso a test can prove the sops PLUMBING without ever touching a real value; today those two activation failures sit in the boot test's ALLOWED list instead.
Onion routing and dVPN: where they fit, and the number that settles half of it¶
Raised on 13/09/2026, alongside the router audit. Nothing decided, nothing applied.
On the router, it does not fit, and it is not an architecture call. Measured that day: 1.3 MB free on the overlay and no USB port on the Cudy WR3000 v1. No USB means no extroot, and Tor plus a data directory does not go into 1.3 MB even trimmed. Every "Tor transparent proxy on OpenWrt" guide assumes a router with storage to spare, and the ones that talk numbers ask for 8 MB of flash and 64 MB of RAM as a floor. This one has the RAM and not the flash.
So the question is only ever about the desktop, which has both and is already the gateway the
router forwards to (system/net/fai-gateway.nix is exactly that shape).
Three options, and they are not variations of one thing:
services.torwith SOCKS on localhost, used per application: ALREADY DONE, and this entry first said otherwise.system/net/tor.nixhas been there all along, client only,ClientOnlyandSafeSockslocking the role, enabled throughmy.services.toronnixos-kingston, withmega-toras the consumer. Verified running on 13/09/2026:tor.serviceactive, listening on127.0.0.1:9050. So the cheapest option was never open, it was shipped, and the terrain to understand is what sits ABOVE it.- An onion service in front of sshd. The only one that is a SECURITY gain and not just a
privacy one, and the reason it is worth writing down: it would let the 2222 DNAT leave the WAN
entirely, and a port that does not exist cannot be scanned. The audit measured 5411 packets on
that DNAT in 14 days against 17 on the brother's, which is the scanning it would delete. The
price is Tor's latency on every SSH session, and it does not retire
ssh.v1cferr.dev, because Caddy still needs the name and the address. - Transparent proxy for the whole LAN. What almost every tutorial teaches, and the one to refuse here. It collides head-on with Sunshine, Jellyfin and everything that depends on latency and on a stable address, which is most of what this house runs.
dVPN is a different product than the name suggests, and for this setup it is a no. Mysterium, Sentinel and Orchid are bandwidth marketplaces with a token, not privacy infrastructure with a threat model. As a CLIENT the exit is an anonymous stranger's connection, which is strictly worse than my own WireGuard back home for everything I actually do remotely. As an EXIT OPERATOR the abuse traffic leaves through this IP, and this IP is the one that has to stay clean and reachable for Caddy, Sunshine and SSH: the two goals pull in opposite directions, so running both is paying for one to damage the other. The single case where a dVPN wins is censorship circumvention on a hostile network, where a residential exit passes where a datacenter one does not, and with the FAI and UFSCar tunnels plus WireGuard home already in place, that need does not exist here.
What would change the answer: a router with storage (extroot or a different device) reopens the first question, and travelling somewhere that blocks WireGuard reopens the last one.
MkDocs 2.0 split the ecosystem, and Zensical is the exit (measured on 18/09/2026)¶
The site of notes/repo/site.md runs on MkDocs 1.6.1 plus Material for
MkDocs 9.7.6, and Material's build prints a warning about MkDocs 2.0 on every run. It is not
marketing. The numbers below were checked against the upstream repositories that same day, not
taken from the warning.
MkDocs 1.x is frozen. Last release 1.6.1 on 30/08/2024, so 24 months with no release. Last
commit on master on 20/10/2025, 11 months. BSD-2-Clause, 22.4k stars, NOT archived, which is the
worst of the three states: nobody has declared it over, and nobody is shipping.
MkDocs 2.0 is a different project wearing the same name. A pre-release on PyPI as of 30/08/2026: the plugin system REMOVED, the theming rewritten, the config moved from YAML to TOML, no migration path, contributions closed ("do not open issues or pull requests"), and no license declared at all. Material's analysis of 18/02/2026 is that nothing built on the current architecture survives it.
Material itself is alive, and that is what makes waiting cheap: MIT, 27.5k stars, release 9.7.7 on 17/07/2026, pushed 15/09/2026.
Zensical is the same team's replacement. MIT, repository opened 18/05/2025, 5.7k stars, and
shipping constantly: v0.0.62 on 13/09/2026, with releases every few days. It is already in
nixpkgs, 0.0.43 on 26.05 and 0.0.59 on unstable. It reads an existing mkdocs.yml, and nav,
theme and extra carry over unchanged.
Why not switch now¶
The version number is the whole argument. 0.0.62 with a release every two days is a project finding its shape, and rule 13 pins dependencies precisely so that this repo does not ride somebody's daily churn. A 0.0.x in the lock means a bump that can break the build on any week.
The one piece that would not carry over is scripts/mkdocs-hooks.py. Zensical's
compatibility page documents plugin REPLACEMENTS, not the MkDocs hook API, so the 134 links to
source files would need whatever mechanism replaces it, and that mechanism is not documented yet.
Nothing is at risk while waiting. A frozen generator is not a running service: it has no
network exposure, it is pinned in flake.lock, and the only thing it ever touches is markdown in
a build sandbox. The failure mode of "unmaintained" here is a bug that never gets fixed, not a
CVE that gets exploited.
Why the cost of being wrong is small, by construction¶
The tree did not move to match the generator, which was decided for other reasons and pays off
exactly here. What is MkDocs-specific is mkdocs.yml, one hook and one derivation. docs/ is 88
markdown files that any generator in this class consumes, so switching is a config change, not a
migration.
Trigger to migrate¶
Zensical reaching 1.0 with a stable config format, or Material 9.x announcing an end date,
whichever comes first. Prototype then: build both, diff the output, and keep whichever one still
resolves the links to source. Check nix flake check passes before switching the deploy over.