Generate an aerc styleset from config.colorscheme.colors, matching how
tmux, zathura and the rest are themed. Keys are quoted throughout since
unquoted dotted names build nested attrsets and fail the module's type
check.
Home Manager writes the whole aerc.conf, so aerc's shipped filters never
applied: bring back colorize, calendar and html, and point the viewer at
less. Pin the mailbox order per account instead of taking plain
alphabetical, and fold subfolders under their parent.
Assisted-by: pi (claude-opus-5)
Give each host a wallpaper whose derived colorscheme sits at its own position on an evenly spaced hue wheel:
- Atlas: aenami-dawn; Celaeno: mountain-yellow-red
- Alcyone: autumn-leaves-orange; Pleione: plains-flowers-green-red
- Electra: aenami-northern-lights; Merope: aenami-bright-planet
- Maia: deer-lunar-fantasy; Taygeta: nebula-purple-pink
Reserve the remaining turquoise position for a future asterope host, and regenerate the website colorscheme data.
Assisted-by: pi (gpt-5.6-sol)
Keep the appendix heading visible while placing its sustainability fragment table in a details disclosure that is collapsed by default.
Assisted-by: pi (gpt-5.6-sol)
Keep the publications navigation item selected on paper pages and align their DOI links with the publication listing while removing the redundant back link.
Assisted-by: pi (gpt-5.6-sol)
Render publication figures on a stable light surface so their original colors and labels remain legible regardless of the site theme.
Assisted-by: pi (gpt-5.6-sol)
Publish complete Markdown editions of both papers with linked citations, references, tables, original figures, appendices, acknowledgments, and scholarly citation metadata. Include the sustainability fragment table and the updated SESoS ecosystem figure while preserving the PDFs as versions of record.
Assisted-by: pi (gpt-5.6-sol)
Move the RSS link from its brittle absolute position above the blog listing into the global social footer, and remove the obsolete positional selector.
Assisted-by: pi (gpt-5.6-sol)
Increase spark core size, palette contrast, halo, and shimmer range so the ambient layer remains subtle but actually visible.
Assisted-by: pi (gpt-5.6-sol)
Layer sparse palette-tinted sparks behind the site with slow staggered shimmer, no pointer interaction, and explicit reduced-motion and print fallbacks.
Assisted-by: pi (gpt-5.6-sol)
Replace the aliased circular outline with an inset antialiased shadow ring and a faint outer glow for both selection and keyboard focus.
Assisted-by: pi (gpt-5.6-sol)
Use conventional hash markers for heading permalinks, frame the site identity with decorative stars, and render constellation points as luminous star glyphs while preserving their 48px controls and interaction behavior.
Assisted-by: pi (gpt-5.6-sol)
Add accessible self-links to rendered h2-h6 headings without embedding HTML in Markdown. Reveal the star marker on hover or keyboard focus, keep it visible on touch devices, and omit it from print.
Assisted-by: pi (gpt-5.6-sol)
Carry the constellation's glow and motion into links, navigation, and form controls, add palette-lit structural rules, and glint anchored headings on arrival. Respect reduced-motion preferences throughout.
Assisted-by: pi (gpt-5.6-sol)
Mix native light surfaces with each scheme's primary role instead of its near-white primary container, preserving a distinct atmospheric tint across all eight themes.
Assisted-by: pi (gpt-5.6-sol)
Adopt the new top-level colorscheme structure and emit each scheme's native dark and light Material roles directly. Remove the manual light-role remapping and obsolete print scheme, update the legacy colorscheme page, and promote Electra to an interactive constellation theme.
Assisted-by: pi (gpt-5.6-sol)
Align the seven theme stars more closely with the Pleiades reference and add muted, non-interactive Electra and Sterope stars to complete the cluster.
Assisted-by: pi (gpt-5.6-sol)
Give each constellation star dark and light preview roles from its own palette, switching automatically with prefers-color-scheme.
Assisted-by: pi (gpt-5.6-sol)
Mix each selected palette into the light and dark surface hierarchy so theme changes affect the page atmosphere again without altering semantic role mapping.
Assisted-by: pi (gpt-5.6-sol)
Expose each selected scheme as a stable palette, then map that same palette to dark or light semantic roles according to prefers-color-scheme. Preserve constellation selection, persistence, live system changes, and the dedicated print palette without adding a mode switch.
Assisted-by: pi (gpt-5.6-sol)
Remove the explanatory heading and current-theme readout, leaving only a quiet centered invitation beneath the constellation.
Assisted-by: pi (gpt-5.6-sol)
Replace the colorscheme select with a full-width Pleiades constellation whose seven accessible, touch-friendly star buttons preview and apply their matching themes. Keep active state, persistence, keyboard labels, reduced motion, and JavaScript-disabled behavior in sync.
Assisted-by: pi (gpt-5.6-sol)
Keep every page article at the shared content width while constraining only non-listing prose sections to the narrower reading measure.
Assisted-by: pi (gpt-5.6-sol)
Narrow document pages to a comfortable reading measure, tighten the masthead and heading rhythm, reduce display scale, and use regular Fira Sans for friendlier section headings while preserving full-width archive layouts.
Assisted-by: pi (gpt-5.6-sol)
Replace Water.css with a classless technical-editorial design for the about, blog, publications, CV, and post pages. Add responsive document and listing layouts, Fira Sans Condensed display typography, semantic page regions, and a persistent JavaScript-gated colorscheme picker.
Assisted-by: pi (gpt-5.6-sol)
Allow authorization to consume either a raw ID token or the complete Jagex successful-login URL from stdin when no desktop URL handler is available.
Assisted-by: pi (gpt-5.6-sol)
Replace the legacy chained OAuth flow with the official launcher beta's single ID-token exchange and game session API. Register the rshub callback and permit its narrowly scoped qutebrowser handoff.
Assisted-by: pi (gpt-5.6-sol)
MemoryHigh is enforced by reclaim, and reclaiming anonymous pages means
writing them to swap. Holding sabnzbd near 4G resident therefore pushed 5.2G
into the 8G swapfile: the limit did not contain the growth, it converted a
RAM problem into swap exhaustion, which is the failure that actually
threatens the box. Without it sabnzbd grows in RAM, where oomd's pressure
rule can see it stall.
ManagedOOMSwap=kill registers this unit alone as a swap-monitoring
candidate. oomd ranks swap candidates by usage, so scoping it here means the
early trigger at 90% swap can only ever select sabnzbd, while jellyfin's
cold pages stay ineligible. The pressure rule on system.slice remains the
broader net for anything else.
Assisted-by: pi (claude-opus-5)
earlyoom only acts when available memory AND free swap are both under
threshold. During the 2026-08-11 swap exhaustion, swap crossed 5% at 02:14
but RAM did not until 06:10, so it watched the swapfile drain to zero for
four hours. It then sent 88 SIGTERMs without landing a kill: SIGKILL
escalation is gated at half the threshold (2.5%), RAM bottomed at 4.74%,
and sabnzbd's graceful shutdown took 12 minutes under thrash.
systemd-oomd was already running but monitoring zero cgroups, since every
unit defaults to ManagedOOMSwap=auto. It acts on PSI stall time, which does
not distinguish waiting on reclaim from waiting on swap I/O, and it SIGKILLs
the cgroup rather than negotiating. Candidates are ranked by reclaim
activity, so a service holding cold pages in swap is picked last rather
than first.
Set on system.slice directly because systemd.oomd.enableSystemSlice
hardcodes ManagedOOMSwap=kill, which would rank jellyfin's ~1.2G of cold
swap ahead of whatever is actually thrashing.
Assisted-by: pi (claude-opus-5)
earlyoom SIGTERMed sabnzbd at 06:10 after it grew to 4.2G resident plus
5.2G of swap, exhausting the box's 8G swapfile. sabnzbd caught the signal,
saved its queue and exited 0 twelve minutes later, and with Restart=no it
stayed dead for the next three and a half hours.
Restart=on-failure would not have helped: the graceful shutdown path is a
clean exit.
Assisted-by: pi (claude-opus-5)
Every server was on defaults: timeout=60, and max_art_tries=3 across six
servers. That is up to eighteen rounds, each willing to wait a minute,
before SAB concludes an article is gone. Nothing legitimate takes that
long -- TLS handshakes to all six hosts complete in under a second.
Drop timeout to 20s and max_art_tries to 2. This is a straightforward
reduction in the cost of the failure path, not a fix for any particular
incident.
Explicitly not addressed: a stall found while investigating, where the
queue sat at ~0.13 MB/s with every server idle -- eweka and blocknews
holding ten established connections each with no article assigned to any
of them, and frugal at zero of thirty. Nothing was requesting articles at
all. An earlier draft of this commit blamed connections blocked on
timeouts and marked the blocknews pair optional; the connection dump
showed those sockets were idle rather than busy, so both the mechanism and
the remedy were wrong and have been dropped. Root cause still unknown.
Assisted-by: pi (claude-opus-5)
par2/unrar routinely take a full core each and the media disk hits 83%
utilisation during a grab, both competing with jellyfin direct play and
library scans. sabnzbd was still at the default weight of 100, tied with
jellyfin, while the *arrs sit at 50.
Drop it to 20 so its pipeline yields under contention and keeps the whole
box when nothing else wants it. Children inherit the cgroup weight, so
this covers par2 and unrar without touching sabnzbd's own nice setting.
Assisted-by: pi (claude-opus-5)
The Cortex-A72 in the BCM2711 implements no ARMv8 crypto extensions, so
TLS runs entirely in software. Measured on merope: AES-256-GCM manages
53 MB/s per core against 208 MB/s for ChaCha20-Poly1305, roughly 4x.
All six providers default to TLS_AES_256_GCM_SHA384 and all six accept
ChaCha20, so at the 43 MB/s this box was pulling, about 0.8 of a core was
going to decryption alone -- on a machine already sitting at 93% CPU with
the network only a third utilised and the disks coasting. Usenet
throughput here is bounded by the cipher, not by bandwidth or IO.
Setting ssl_ciphers makes SABnzbd cap the connection at TLS 1.2, since
Python never exposed SSL_CTX_set_ciphersuites() for the 1.3 suites. That
costs an extra handshake round trip, amortised across long-lived
connections, and an unencrypted certificate that SNI already gives away.
ECDHE preserves forward secrecy and ChaCha20-Poly1305 is the same AEAD
1.3 would have picked.
Preferred over exporting OPENSSL_CONF, which reaches the same cipher
while keeping TLS 1.3: this is the supported knob, so when upstream can
select 1.3 ciphersuites the cap lifts on its own, whereas overriding a
library's config file to reorder one list is the kind of thing that
breaks silently.
Assisted-by: pi (claude-opus-5)
/srv/media is now a single btrfs subvolume on the media disk holding the
libraries and both staging trees, so imports are same-filesystem renames.
426 torrents were re-pointed through the deluge UI and are seeding without
a recheck, and the M.2 went from 164G to 39G used.
The layout is described by hardware-configuration.nix, deluge.nix and
sabnzbd.nix better than prose could, so the plan has nothing left to say.
Two follow-ups it was carrying, recorded here rather than kept as a file:
copying across filesystems flattened every reflink between the music
library and the torrent payloads, so those trees hold 170 GiB where they
held ~124 and duperemove would reclaim it; and the media filesystem has
still never been scrubbed.
Assisted-by: pi (claude-opus-5)
Both documents had accreted a running commentary -- findings, then
corrections to those findings, then corrections to the corrections. That
is a changelog, and it is the wrong shape for something read mid-migration
at 2am.
Rewritten to carry only current state and what remains to be done. The
boot-chain work, the swap incident, the USB 2.0 discovery and the
successive revisions of the throughput numbers are all gone; git holds
that history if anyone wants it.
The migration is now a runbook rather than a description: what can be done
in advance without downtime, what needs the media stack stopped and
roughly how long each step takes, and what must wait until the result is
proven.
Documents the reflink structure between music and torrents, measured
rather than assumed. Both trees cost ~124 GiB today and ~170 GiB once
copied, because reflinks cannot cross filesystems. Sharing is whole-file
and strictly bimodal -- no file is fragmentarily shared, and the 23% that
sit at 90-99% differ only by a ~115 KiB tag block Lidarr rewrote after
importing -- so duperemove recovers nearly all of it afterwards. Warns
against file-level duplicate finders, which compare content rather than
extents and would trade 564 seeds for zero bytes. Replaces a fabricated
claim that music was heavily hardlinked: it has none.
Drops host SSH keys from the backup set, where they were listed as tier 1
on the false premise that sops needs them. Every creation rule already
lists the GPG key as a recipient beside the per-host age keys.
Corrects the claim that alcyone needs no dumps: firefly and headscale are
both sqlite there, and only its postgres (roundcube) is excluded.
Assisted-by: pi (claude-opus-5)
The escape came from tuigreet's shell-quoted README example. Nix needs
no escape here, so the backslash reached greetd.toml and its command
lexer rejected \$ as an unknown escape sequence -- greetd has failed to
start on every host without regreet since e4fa2194 (2026-06-16).
Assisted-by: pi (claude-opus-5)
The firmware partition now lives on a microSD card instead of the M.2, and
nixos-hardware repopulates it on every switch.
Two reasons for moving it off the M.2. It sits inside the Argon case, so a
bad bootloader meant opening hardware to recover; on removable media that
becomes a card swap in any laptop. And the partition was 30M, which cannot
hold the GPU blobs, device trees and overlays the install script places.
Boot chain, for the record, since none of it is reproducible from this
flake: the EEPROM boots SD first (BOOT_ORDER=0xf41) and excludes the two
data drives by VID:PID, so the media disk can occupy a USB 3 port without
hanging the bootloader. Verified booting from the card on 2026-08-09 by
setting gpu_mem on the SD copy of config.txt and watching MemTotal drop
174 MiB -- the firmware exposes no boot-source to Linux, because U-Boot
rebuilds /chosen and discards it.
config.txt is now generated. Relative to the file it replaces it keeps
arm_64bit, enable_uart and disable_overscan, carries avoid_warnings over
explicitly, and picks up the nixos-hardware defaults -- including
arm_boost, which lifts this board (Pi 4B rev 1.4) from 1.5 to 1.8 GHz.
It drops enable_gic and armstub=armstub8-gic.bin. Both are vestigial on
current Pi 4 firmware, and the install script does not copy the armstub
binary, so declaring it would point config.txt at a file that is absent.
U-Boot is pinned to ubootRaspberryPi4_64bit rather than the module default
ubootRaspberryPiAarch64: it is the exact binary verified booting today,
when replacing U-Boot 2021.04 fixed a boot failure where the legacy
distro_bootcmd only ever tried USB device 0.
Assisted-by: pi (claude-opus-5)
Adds raspberry-pi/common/firmware.nix (upstream 2026-07-10, cross-compile
fix 07-14), which installs the Pi firmware partition from the flake on
every switch instead of leaving it hand-populated.
Also drops raspberry-pi/4/cpu-revision.nix upstream. That overlay
hardcoded linux,revision = 0x00d03114; merope reports d03114 from the
firmware DTB regardless, so removing it changes nothing here.
Assisted-by: pi (claude-opus-5)
Device letters on merope follow USB enumeration order and are not stable;
they changed three times on 2026-08-08/09 as drives were re-plugged. Both
documents now address disks by label throughout.
Corrections to the migration plan:
- The ~28 MiB/s attributed to the media disk was a saturated USB 2.0 link,
not a seek-bound spindle. On USB 3 it sustains 183 MB/s, 6.5x faster.
Sizing arguments were built on the wrong number.
- The 04:42 reset was a UAS device reset on the M.2 bridge (174c:2362),
not swap: swap stayed flat at 2.27 GiB with 4.4 GiB available. The
watchdog held it to 105s against 48min the previous day.
- Both drives now sit on the VL805 SuperSpeed side, so they share one
PCIe lane rather than having independent paths.
Boot, resolved 2026-08-09 in two stages, both firmware-level and
therefore invisible to nixos-rebuild:
- EEPROM walks USB in port order and looped when the drive in 2-1 had no
bootable partition. Excluded both data drives by VID:PID. Note
'rpi-eeprom-config --apply' is unusable on NixOS, since the wrapper
points FIRMWARE_ROOT at a nonexistent path; build the image with
--config and stage pieeprom.upd/.sig by hand.
- U-Boot 2021.04 predates bootstd and declares USB as a single instance,
so boot_targets held one usb0 and only devnum=0 was ever tried.
Replaced with 2026.04, which enumerates every bootdev. Old binary kept
as u-boot-rpi4.bin.bak.
Backups doc records that the M.2 has 2233 corruption errors and has never
been scrubbed, so they were found passively rather than by verification.
Music (92 GB, the only irreplaceable data there) has been copied to the
backups disk; a scrub is now the open item.
Assisted-by: pi (claude-opus-5)
No backup tooling exists anywhere in the flake today, and sdc1 is a
932GB disk holding 5.9MB.
Covers what is actually irreplaceable -- firefly, radicale, deluge
state, mail, git, immich, music -- rather than the 4.6TB of re-acquirable
video, and specifies database dumps rather than live file copies, which
would otherwise produce backups that silently will not restore.
Mail and calendars are already replicated across three hosts, so they are
included for history rather than redundancy: sync propagates deletion,
and the useful protection there is retention depth, not another copy.
Assisted-by: pi (claude-opus-5)
Consolidates all media onto sdb in a single btrfs subvolume mounted at
/srv, so every import becomes a rename or hardlink rather than a
cross-device copy, and the disk holding the swapfile sees no media I/O.
Written up after the 2026-08-07 stall, whose root cause was swap
contending with a Radarr import on the same device.
Assisted-by: pi (claude-opus-5)
Fold back UI edits from the hosts dashboard:
- RAM Usage: rename the used-memory series to "Memory", add a "Swap"
series (SwapTotal - SwapFree), and retarget the configFromData
transformation from byFrameRefID to byType so Total Memory applies as
max to both series. Show the legend, since there are now two series.
- Rename the Storage gauge to "Disk Usage".
- New "Disk IO" timeseries: rate of node_disk_io_time_weighted_seconds_total,
joined against node_btrfs_info via label_replace to strip the partition
suffix, so series are labelled by filesystem label.
The UI export predated d9a6c23e and would have reverted its Storage
query and threshold fixes; those hunks were dropped rather than folded.
Assisted-by: pi (claude-opus-5)
The top threshold step rendered a 100%-full filesystem in plain text
colour rather than red, so the alarm state was unreachable -- red only
covered 90-99.99%.
Both targets pulled a full range while groupingToMatrix keeps one value
per cell, so switch them to instant queries.
Join on (instance, uuid) rather than uuid alone; with uuid-only matching,
a multi-value $hosts could cross-match filesystems between hosts.
Assisted-by: pi (claude-opus-5)
The 2026-08-07 stall presented as a CPU problem but was I/O underneath:
a Radarr cross-disk import saturated the USB SSD holding both the rootfs
and the swapfile, and everything else queued behind it.
The existing Nice values were aimed at the wrong thing twice over. They
only ever addressed CPU scheduling, which was the symptom -- renicing a
process blocked in D-state achieves nothing. They were also a layer too
low: with the cgroup v2 cpu controller enabled, CPU is split between
service cgroups by weight, and every service on this host sat at an
identical 100. Nice only reorders tasks within a cgroup, so a lone
service being polite to itself changed nothing.
Replace them with CPUWeight/IOWeight, which is the layer that actually
arbitrates between services. Nice and IOSchedulingPriority are dropped
rather than kept alongside: nothing here escapes the cpu controller, and
ioprio depends on BFQ exactly as IOWeight does, so it is not a fallback
for anything -- if BFQ is absent both are inert together.
deluged is included despite never having had a Nice; it writes to the
same device continuously and was the other half of the load that day.
recyclarr loses its Nice without replacement -- it only makes a handful
of API calls to the *arrs and never contends for anything.
Requires BFQ. All three disks were on mq-deadline, which has no notion of
I/O weights at all and would have made IOWeight a silent no-op.
Assisted-by: pi (claude-opus-5)