feat(autotheme): fire at the configured sunrise/sunset, not a 15-min poll
All checks were successful
Check / eval (push) Successful in 4m14s

The timer's OnCalendar now bakes the configured times (falling back
like _hhmm_to_min on a hand-edited state), so the switch lands at the
configured minute instead of up to 15 minutes late. The original
poll's robustness case is covered without it: Persistent catches
transitions missed while powered off, systemd fires elapsed calendar
timers on resume, and OnStartupSec settles the theme after login.

Baked times mean a time edit must rebuild: the menu's Sunrise/Sunset
writes now run `auto --force` when enabled (one rebuild re-bakes the
timer and lands on the right theme, same as the enable flow). Day and
night slugs stay live reads — but with no poll to catch them up,
editing the slot currently on screen applies immediately via a plain
`auto`. New eval check `auto-theme-timer` asserts the bake
state-bridges-style: configured times reach OnCalendar, garbage falls
back to 07:00/20:00, disabled installs no unit. Docs + hardware-queue
entry updated to the new semantics.

V2: checks.auto-theme (KVM VM, passed) covers the auto engine the
timer ticks; checks.auto-theme-timer (eval, passed) covers the unit;
OnCalendar strings systemd-analyze-validated; the rebuilt menu script
passes bash -n with the new branches in place. V3 pending: live timer
firing at the exact configured minute on hardware (existing
"Auto-theme pair flip" queue entry, refreshed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 22:31:02 +01:00
parent ac24ac7c34
commit f658b0087e
6 changed files with 136 additions and 43 deletions

View File

@@ -342,6 +342,48 @@
pkgs.runCommand "nomarchy-state-bridges" { }
"touch $out"; # all ${toString (builtins.length cases)} assertions held at eval
# The baked auto-theme timer (#79 revision): sunrise/sunset land in
# the unit's OnCalendar at rebuild, and nothing in a build fails
# when that bake goes stale or produces a calendar systemd rejects
# — so assert it at eval, state-bridges style.
auto-theme-timer =
let
homeWith = stateFile:
(downstream.homeConfigurations.me.extendModules {
modules = [{
nomarchy.stateFile = nixpkgs.lib.mkForce stateFile;
}];
}).config;
atState = name: at: stateFileOf name { settings.autoTheme = at; };
on = homeWith (atState "state-autotheme-on.json" {
enable = true; day = "summer-day"; night = "summer-night";
sunrise = "06:30"; sunset = "21:45";
});
garbage = homeWith (atState "state-autotheme-garbage.json" {
enable = true; day = "summer-day"; night = "summer-night";
sunrise = "sometime"; sunset = "25:99";
});
off = homeWith absentState; # template carries no autoTheme key
cal = c: c.systemd.user.timers.nomarchy-auto-theme.Timer.OnCalendar;
cases = [
{ n = "configured times are baked into OnCalendar";
ok = cal on == [ "*-*-* 06:30:00" "*-*-* 21:45:00" ]; }
{ n = "garbage times fall back to 07:00/20:00";
ok = cal garbage == [ "*-*-* 07:00:00" "*-*-* 20:00:00" ]; }
{ n = "disabled no timer unit installed";
ok = !(off.systemd.user.timers ? nomarchy-auto-theme); }
];
failed = builtins.filter (c: !c.ok) cases;
in
if failed != [ ] then
throw ''
Auto-theme timer bake broken settings.autoTheme no longer
reaches the unit as expected:
${nixpkgs.lib.concatMapStringsSep "\n" (c: " ${c.n}") failed}''
else
pkgs.runCommand "nomarchy-auto-theme-timer" { }
"touch $out"; # all ${toString (builtins.length cases)} assertions held at eval
# The bridge, end to end on a booted machine: checks.state-bridges
# proves the option flips at eval, this proves a state key alone
# reaches a RUNNING service. The node sets no