ScheduledJobMetricsAbsent is pending on the monitoring host and fires within
the hour. It is correct, and what it has found is that #77 was never finished
deploying.
What is actually true on the host
$ systemctl list-unit-files 'homelab*'
0 unit files listed.
$ ls /var/lib/node_exporter/textfile_collector/
No such file or directory
$ node_textfile_scrape_error{job="prometheus-metrics"} 1
$ node_textfile_scrape_error{job="oracle-metrics"} 0
make install-timers — step 77 of
runbooks/schedule-maintenance.md —
has not been run here. Nothing is installed, so nothing writes the textfile
directory, so the directory does not exist, so the collector errors on every
scrape. (oracle reads 0 because its agent still runs the pre-#77
config.alloy and has no textfile directory configured at all.)
What that costs
No scheduled maintenance has ever run. The most recent volume backup is
backups/volumes/20260829T064124Z, taken by hand on 2026-08-29. The weekly
volume backup, the nightly firewall backup, the nightly backup verification
and the weekly snmp-verify have between them run zero times.
Four of the five rules meant to catch that cannot fire. Every other rule in
backup.rules.yaml joins against homelab_job_max_age_seconds, which
install-timers.sh --install is what writes:
| Rule |
Reads |
ScheduledJobStale |
... > on(homelab_job) group_left() homelab_job_max_age_seconds |
ScheduledJobFailed |
homelab_job_last_exit_code |
ScheduledJobNeverRan |
homelab_job_max_age_seconds unless on(homelab_job) ... |
SecretsKeyBackupUnproven |
same join, scoped to verify-key-backup |
With the declaration series absent they are all silent, which is exactly what
ScheduledJobMetricsAbsent's own comment predicts — "Nothing else in this file
can fire while this is true." That rule is the one unconditional statement in
the file and it is the only reason any of this is visible. It worked.
The gap worth fixing
make validate passes. install-timers.sh --check derives each timer's
period from systemd-analyze calendar over the .timer files in the
repository and asserts the declared max_age is at least twice it. That is a
good check and it is entirely repo-internal: it compares two copies of the
schedule that both live in git, and cannot see whether either has ever reached
the host.
So the repository agrees with itself, CI is green, make validate prints
scheduled job cadences, thresholds and units agree — and nothing runs. That is
the same shape as the fault #81
was filed about: everything checked except reality.
The runbook already anticipates the neighbouring symptom — its troubleshooting
table has "Timers exist but never fire" — but not this one, where the timers do
not exist at all.
Also wrong
docs/roadmap.md states in the present tense, under Done,
that "Four systemd timers on the monitoring host run make backup weekly ...".
They do not. check_docs.py cannot catch this — it guards counted claims, and
this is a claim about the state of a host.
Suggested fixes
- Run
make install-timers on the monitoring host, and take a backup — it has
been two days and the last one was manual.
- Give
validate.sh a host-aware check that is skipped in CI and runs locally:
systemctl list-unit-files 'homelab*' returning nothing on a host that has
the stack deployed is a failure, not a pass. The existing have/skip
pattern in validate.sh already models exactly this distinction.
- Correct the roadmap entry to say what is deployed rather than what was built.
ScheduledJobMetricsAbsentis pending on the monitoring host and fires withinthe hour. It is correct, and what it has found is that #77 was never finished
deploying.
What is actually true on the host
make install-timers— step 77 ofrunbooks/schedule-maintenance.md—has not been run here. Nothing is installed, so nothing writes the textfile
directory, so the directory does not exist, so the collector errors on every
scrape. (
oraclereads 0 because its agent still runs the pre-#77config.alloyand has no textfile directory configured at all.)What that costs
No scheduled maintenance has ever run. The most recent volume backup is
backups/volumes/20260829T064124Z, taken by hand on 2026-08-29. The weeklyvolume backup, the nightly firewall backup, the nightly backup verification
and the weekly
snmp-verifyhave between them run zero times.Four of the five rules meant to catch that cannot fire. Every other rule in
backup.rules.yamljoins againsthomelab_job_max_age_seconds, whichinstall-timers.sh --installis what writes:ScheduledJobStale... > on(homelab_job) group_left() homelab_job_max_age_secondsScheduledJobFailedhomelab_job_last_exit_codeScheduledJobNeverRanhomelab_job_max_age_seconds unless on(homelab_job) ...SecretsKeyBackupUnprovenverify-key-backupWith the declaration series absent they are all silent, which is exactly what
ScheduledJobMetricsAbsent's own comment predicts — "Nothing else in this filecan fire while this is true." That rule is the one unconditional statement in
the file and it is the only reason any of this is visible. It worked.
The gap worth fixing
make validatepasses.install-timers.sh --checkderives each timer'speriod from
systemd-analyze calendarover the.timerfiles in therepository and asserts the declared
max_ageis at least twice it. That is agood check and it is entirely repo-internal: it compares two copies of the
schedule that both live in git, and cannot see whether either has ever reached
the host.
So the repository agrees with itself, CI is green,
make validateprintsscheduled job cadences, thresholds and units agree— and nothing runs. That isthe same shape as the fault #81
was filed about: everything checked except reality.
The runbook already anticipates the neighbouring symptom — its troubleshooting
table has "Timers exist but never fire" — but not this one, where the timers do
not exist at all.
Also wrong
docs/roadmap.mdstates in the present tense, under Done,that "Four systemd timers on the monitoring host run
make backupweekly ...".They do not.
check_docs.pycannot catch this — it guards counted claims, andthis is a claim about the state of a host.
Suggested fixes
make install-timerson the monitoring host, and take a backup — it hasbeen two days and the last one was manual.
validate.sha host-aware check that is skipped in CI and runs locally:systemctl list-unit-files 'homelab*'returning nothing on a host that hasthe stack deployed is a failure, not a pass. The existing
have/skippattern in
validate.shalready models exactly this distinction.