Clone this repository
For self-hosted knots, clone URLs may differ based on your setup.
Zitadel 2.71 from nixpkgs as a federated IdP with open self-registration:
registration, external IdPs and passkeys allowed in the default login
policy, first instance "lassul.us" with admin user lass (lass@lassul.us).
The masterkey and the admin's initial password come from the zitadel clan
vars generator, the password via an extra steps file so it never hits the
store.
Postgres via peer auth over the local socket. ensureDatabases/ensureUsers
create the database and owner role, so the zitadel role gets no
CREATEROLE/CREATEDB (on PG15 CREATEROLE is close to superuser). That needs
`init zitadel` + start-from-setup instead of the module's start-from-init,
whose init always tries CREATE USER/CREATE DATABASE and fails on the
permission check before the "already exists" check.
nginx terminates TLS and talks h2c to Zitadel via grpc_pass, as Zitadel's
reverse-proxy docs require for its gRPC APIs. Machine identification by
hostname, since the default private-IP/GCP-webhook probes don't fit
neoprism.
The database is pg_dumped by the borg preHook; gatus checks
/debug/healthz. Zitadel 2.71 does not work with PostgreSQL 18 (migration
34_add_cache_schema creates unlogged partitioned tables), neoprism runs 15.
For when.lassul.us (timeful) to show lass's availability without event
details, at https://cal.lassul.us/busy/<token>.ics.
With --busy-token-file, GET /busy/<token>.ics returns the busy time book
itself uses for free slots (CalDAV.busy plus held requests), so the feed
and the booking page agree on what blocks time: recurrences expanded,
all-day events from local midnight, TRANSPARENT and CANCELLED events
left out, TENTATIVE events (own ones and pending booking requests) kept
as busy, since book holds those slots too.
Intervals are merged so overlapping events cannot be counted, and each
VEVENT carries only DTSTART/DTEND in UTC, DTSTAMP (required by RFC
5545), SUMMARY "Busy", TRANSP OPAQUE and a UID hashed from its times.
The window runs from 7 days back to 90 days ahead: book's own 28 day
horizon is too short for polls planned weeks ahead, and the past week
keeps polls that are still open showing sensible data.
The feed is generated per request, so it is as fresh as the calendar;
an unreadable calendar gives 503, never an empty feed. Any other path
under /busy/ gives 404; the token is compared in constant time.
configs/book.nix: the token comes from the prompt-less vars generator
busy-feed and reaches book as a systemd credential, so it never lands in
the Nix store; nginx proxies all of cal.lassul.us/busy/ to book.
book.lassul.us returns 404 for /busy/ so the feed has a single URL.
In the benchmark run (PR 68) the neoprism job spent 29 s between
fetching lazy-store and mounting: the dry-run for the paths to
substitute (13 s in the shared job for the same machine) and their
narinfo closure (the rest, 2,878 paths), both after the 21 s hestia
prefetch of the drvs, which the dry-run needs.
The eval job has the drvs and dry-runs the machines anyway for the
shared derivations; it now keeps each machine's "will be fetched" list
and uploads it with the shared ones. The machine job collects the
closure of that list in the background while hestia prefetches the
drvs, then mounts. Computing the closure in the eval job too would only
move its time onto the eval job, before every machine job starts.
For a single machine the eval job now dry-runs it (about 13 s), against
about 29 s less in the machine job; with more machines the dry-runs ran
already, and in parallel.
The shared job dry-ran every machine to find the derivations needed by
more than one, after a runner start and a prefetch of all drv closures
from hestia. In the benchmark run (PR 68, only neoprism uncached) it
built nothing and still cost the machine job 61 s: 5 s queueing, 6 s
setup, 21 s prefetch (mostly the 47 MiB nixpkgs source), 13 s dry-run,
then 6 s queueing of the machine job.
The eval job has the drvs already, so it dry-runs the machines itself,
in parallel, and only for two or more (one machine shares nothing). It
hands the list to the shared job as an artifact; with an empty list the
shared job is skipped, and the machine jobs wait for the manifest
version of the eval job instead of the shared job's.
`nix run nixpkgs#nix-eval-jobs` resolves nixpkgs through the global
registry (nixpkgs-unstable): a second nixpkgs, fetched as a GitHub
tarball and unpacked into the git cache on every eval job, next to the
locked one the flake evaluation substitutes from cache.nixos.org
anyway. A cold nixpkgs tarball fetch took 18.6 s locally; on the
runner it hides inside the 99 s matrix step of the benchmark run
(PR 68), which prints no progress. With --inputs-from . the eval job
builds nix-eval-jobs from the locked nixpkgs (substituted from
cache.nixos.org) in the tools step and hands the binary to the matrix
step.
The worker count stays at 4: locally nix-eval-jobs over ciToplevels
took 101 s with 4 workers (1.8 cores busy), 72-83 s with 8 and 137 s
with 6 x 2 GiB, too noisy on a shared 16-core host to say anything
about the 4-vCPU runner.
audio-matrix (./matrix.py) watches PipeWire (pw-dump --monitor) and
publishes per source and room an MQTT discovery switch
switch.audio_route_<source>_<room> (attributes source, speaker, kind),
grouped as one HA device per source. Sources are AirPlay (shairport's
"Shairport Sync" stream), LAN (roc-lan) and every connected Bluetooth
phone (bluez_input.*, keyed by MAC, named by its alias); a phone's switches
are removed when it disconnects. The daemon links the source node directly
to roc-<room> with pw-link and re-applies the links whenever nodes come
back (phone reconnects, PipeWire restarts).
The routes are kept per source key in /var/lib/audio-matrix/routes.json, so
a reconnecting phone gets its previous routing. A source never seen before
plays on all rooms: a freshly paired phone should just make sound.
The hub null sink, the "Audio room" select, its buttons, the audio-room
service and command are gone. WirePlumber must not link the sources itself
any more: node.autoconnect = false for bluez_input.* (monitor.bluez.rules
run after name-node.lua sets it true) and roc-lan (source.props), and
PIPEWIRE_AUTOCONNECT=false for shairport-sync, whose pipewire backend
always connects with PW_STREAM_FLAG_AUTOCONNECT. Groups without the null
sink are driven by the phone's node or PipeWire's Dummy-Driver.
The old discovery configs stay retained on mosquitto; clear them once on
acheron:
for t in homeassistant/select/audio_room/config \
homeassistant/button/audio_room_{shodan,icarus,both,off}/config \
gg23/audio-room/set gg23/audio-room/state; do
mosquitto_pub -h localhost -u gg23 -P gg23-mqtt -r -n -t "$t"
done
A Pixel 9a with HD audio negotiated a high-quality codec, and WirePlumber
failed to create the bluez_input node ("mod.adapter: can't get format:
Invalid argument"), so no audio reached the hub and the phone retried
every second. With SBC it works. AAC decoding is available: the
nixpkgs pipewire bluez5 aac plugin links fdk-aac. sbc_xq shares the
sbc endpoint and has a decoder.
llm-agents now ships orca 1.4.227, whose release exists again, so the
1.4.224 pin (for the deleted 1.4.225/1.4.226 releases) and its assert
are no longer needed.