Systems — jobs (launchd/cron/n8n), network thresholds and monitors, invoking skills

This article covers the three Systems tabs: where the Jobs list comes from and what "flagged" means, which monitors feed the Network tab and the thresholds behind its colours, and how the Skills tab finds skills and how to invoke one. It absorbs the on-screen subtitles and note that the content audit slates for trimming: "Pepper's background agents · Mac Mini launchd + cron, and n8n (Hetzner)", "Healthy: Rx −28…−8 dBm · Tx 2…7 dBm · temp <65°C. Data from unifi-sfp-monitor (5s polls)", and ".claude/skills · invoke with /slug in Claude Code". Everything is as the code does it on 2026-09-01.

Jobs

The list is assembled by pepper-ui/backend/panels.js (getScheduled) from four sources, merged into one table and grouped by category. The server caches it for 25 seconds; the console re-reads it every 90 seconds for the nav badge and every 20 seconds while the Jobs tab is open.

SourceWhere it runsWhat the row carries
launchd LaunchAgentsMac Mini, ~/Library/LaunchAgents, labels com.pepper.*, com.blfo.*, co.blfo.*, co.pepper.*running and last exit from launchctl list; cadence and next run parsed from the plist; description and category from pepper-ui/backend/schedule-manifest.json (71 entries); health from data/schedule-health.json
Cron (Mac Mini)the Mini's own crontab -lcadence and next run from the cron expression; no running / exit semantics, so status is always ok
Cron (MacBook)the MacBook's crontab, pushed as text to data/macbook-crontab.txt by scripts/push-macbook-crontab.sh (LaunchAgent com.pepper.macbook-crontab-push)same as above; every row turns warn when the file is older than 3 days, with the reason "MacBook hasn't synced in {n}d"
n8n (Hetzner)n8n.blfo.co, read over its API with N8N_API_KEYcadence from the trigger node (schedule, Gmail poll, webhook); status warn when the workflow is inactive or its last execution was not a success; "log" is the last 10 executions

The Mini reads only its own crontab. The MacBook's is visible only through the push above; nothing else crosses machines.

Cadence labels

Plist keyLabelNext run
StartIntervalEvery {n} min / Every {n}h / Every {n}snone shown
StartCalendarInterval, one entryDaily · HH:MM, Weekly · {day} HH:MM, Monthly · day {d} at HH:MM, Yearly · HH:MMearliest matching future time
StartCalendarInterval, several entries{n}x dailyearliest across the entries
KeepAliveAlways running—
none of the aboveManual / on demand—

What "flagged" means

A launchd job is ok unless one of these holds: the plist is on disk but not registered with launchd; its last exit code is non-zero (the status cell then reads "exit {n}"); or the watchdog marked it unhealthy. The Running and Flagged counts and the nav badge exclude the "Dev / Test / Diagnostic" category, so ad-hoc test triggers cannot train anyone to ignore the badge; they stay visible under the category filter.

The watchdog is scripts/schedule-health.py, run by the schedule-health job every 15 minutes. For each LaunchAgent it:

  • reloads a job that has dropped out of launchd (a missing registration is never intentional, so this auto-heals);
  • reports, but never fixes, a non-zero last exit on a job that is not running;
  • for jobs with a real schedule, checks the log was written within a grace window of max(2.5 × interval, interval + 2 hours) — "log stale {h}h (expected every ~{cadence})";
  • treats a log that exists but has never been written as "runs silently — cannot verify from logs", reported healthy but unverified;
  • treats a declared log that was never created, on a plist older than the grace window, as "job may not be executing despite a clean last-exit status";
  • probes whether the Mini's claude CLI is still authenticated.

Telegram alerts fire only after 3 consecutive failing checks (about 45 minutes), then re-alert at 3, 12, 48, 96 and 288 checks (45 min, 3 h, 12 h, 24 h, 3 days) and every 288 checks after that. A job that has been unverifiable for 96 checks (a day) is mentioned once, then daily.

Opening a row shows next run, whether it is running now, the manifest description, the health reason, the last exit code, and the last 40 lines of its stdout and stderr log (from StandardOutPath / StandardErrorPath, or an inline >> file.log redirect in the command).

Network

The tab reads /api/network every 10 seconds. The backend reads the monitor daemons' own status files rather than polling anything itself.

PanelSourceProducer
VLAN Gateway, UDM Pro, WAN / Internet/tmp/att-network-status.jsonatt-monitor — pings the VLAN gateway, the UDM Pro and 8.8.8.8 every second, classifies a drop as a UDM reboot, a WAN-only outage or a full outage, and alerts Telegram
Active WAN, SFP+ / XGS-PON fiber/tmp/unifi-sfp-status.jsonunifi-sfp-monitor — polls the XGS-PON ONU stick's optical Rx/Tx power, loss of signal and temperature every 5 seconds via the UniFi API, and alerts Telegram on marginal or critical signal
Events (network)/tmp/att-monitor.loglines matching DOWN, UP, RECOVER, DROP or reboot; the newest 12 on the overview, up to 300 in the browser
Device monitors, device eventsUptime Kuma's SQLite database, ~/uptime-kuma/data/kuma.db, opened read-onlythe latest heartbeat per active monitor; events are heartbeats Kuma marked important (status changes). Full monitor at uptime.blfo.co

The subtitle shows the network status file's own timestamp and adds "status stale {n}m" when that file is more than 120 seconds old — the monitor has stopped writing, which is a different fault from the network being down. The old att-dashboard (port 3300) is still served under /netmon behind the console's own login.

Thresholds behind the colours

ReadingGoodWarnCritical
Rx power (dBm)−25 to −8below −25above −8 or below −28
Tx power (dBm)2 to 9below 2 or above 9—
Temperature (°C)up to 65above 65above 75
WAN latency (ms)up to 25above 25above 60
Availability (%)99 and abovebelow 99—
Active WANfiberanything else; the meta line says whether the 5G backup is standby, carrying traffic, or down—

The on-screen note and the code comment beside the threshold functions both give the healthy Tx range as 2…7 dBm; txCls only warns below 2 or above 9. [check: which Tx ceiling is intended — the note's 7 or the code's 9.] A LOS flag from the ONU is printed next to the section heading regardless of the power readings.

Device monitors are grouped by name pattern, not by Kuma tags: Cameras, Access (door reader, access hub), KDS, POS (POS, Stripe, label printer), Smart home (ecobee, Aqara, Homey, Lutron, Shelly, Abode, Blaze), Network (AirPort, Time Capsule, UDM, switch, AP), Appliances (dishwasher, Hobart), Other. Kuma's maintenance state counts as up; pending shows as "checking".

Skills

The Skills tab lists native Claude Code skills read from .claude/skills/ in the Pepper checkout on the Mini. Each skill is a directory holding a SKILL.md; the directory name is the invocation slug. The card shows the frontmatter name, description and category (default "Other"). A flat .claude/skills/*.md file is not discovered and cannot be invoked.

To run one, type /{slug} in Claude Code — for example /asset-intake. The console does not run skills itself. Categories are ordered Coffee, Business Ops, People & Payroll, Comms & Docs, Rhythm & Planning, Research, Personal, Pepper System; any other category found in the data is inserted alphabetically before Other, which is always last (a category unknown to the list used to be dropped silently — fixed 2026-08-13).

Every skill run ends with a retro: python3 scripts/skill-retro.py log --skill {slug} --outcome clean|partial|blocked|error --summary "…", with --finding and --proposal when the SKILL.md itself was wrong. Proposals are staged for review, never applied by the session. Three consecutive non-clean runs of the same skill is a dead loop; stop re-running and fix the blocker upstream.


Was this article helpful?