- TypeScript 94.2%
- Svelte 5.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The tick is edge-triggered off `prevFailed` and skips everything while it is null, which is what keeps units already failing at module start from re-alerting on every restart. But the set lived in module memory, so it came back null on every boot and the first tick recorded whatever it found as "always been failing" — a unit that died while OpsDeck was down was never announced at all. Being quiet about state the root page already shows is the point. Being quiet about a failure nobody has seen yet is the opposite of it. The set is now stored in `ctx.storage`, loaded from inside the first tick (`scheduler.every` queues that tick as a microtask when it is CALLED, so awaiting after the call races the tick it was meant to prepare) and written only when it moves — a host whose units are all healthy takes no write per minute. A row written by an older version reads as "no baseline" rather than throwing at load. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
| backend | ||
| frontend | ||
| CLAUDE.md | ||
| opsdeck.module.json | ||
| PROMPT.md | ||
| README.md | ||
module_systemd
OpsDeck module managing the host's systemd units — the system manager AND every running user manager. Schema-driven: every page renders natively on web and mobile from the portable UI schema. The only custom web code is the dashboard overview card, which the schema cannot express.
Pages
- Root — system/user service counts, failed counts, host-access status, the merged failed-unit list (empty on a healthy host) and a chart.
- Services — system services plus every user's services, labelled by user, drill-down per unit.
- Timers — system and user timers merged: next fire, last run, last result, linking to the activated unit.
- Unit detail (
unit/<scope>/<name>, scopesystemor a username) — state, unit file, memory/tasks/PID/restarts, journal tail, and admin actions: start / stop / restart / enable / disable. Each action shows only while it applies (whenconditions) and confirms.
User managers
Users with a live user manager are enumerated from <hostRoot>/run/user/<uid>/systemd
plus /etc/passwd — pure file reads (needs the host mount with rslave
propagation so runtime dirs mounted after container start stay visible).
Their units are reached by becoming the user:
runuser -u <user> -- env XDG_RUNTIME_DIR=/run/user/<uid> systemctl --user ….
Not --machine=<user>@.host — through the chroot that dies with
"Failed to start transient service unit: Transport endpoint is not
connected"; and plain root with XDG_RUNTIME_DIR is rejected by the user
manager's peer-uid check. User-unit journals are matched by
_SYSTEMD_USER_UNIT + _UID — as root, journalctl reads every user's
journal without a bus.
How it reaches the host
The container has its own mount namespace, so systemctl run plainly would
configure nothing. Host access comes from the core SDK's ctx.host (shared
by all modules; probes chroot → nsenter (ns files) → nsenter -t 1 →
direct with a functional left-the-container test and keeps the first that
works). On top of that, the module checks that systemctl actually exists on
the other side before declaring itself available. Needs OpsDeck core with
SDK ctx.host (2026-07 or later).
systemctl additionally needs the manager bus, and PID 1 rejects peers from
a foreign PID namespace — the container must run with pid: host (set in
production). Without it the module degrades to read-only: journal tails keep
working (journalctl reads journal files directly, no bus), and the root page
says what is missing instead of failing per click.
Alerts
Edge-triggered only: a unit entering failed state — system or user — raises one notification (deduped by key, linking to the unit page). Units already failed at module start are visible on the root page but not re-alerted, so dismissals stick.
The set of units already known to be failing is stored (ctx.storage), so a
restart is not a fresh start. It used to be held in memory alone: the first
tick after every restart recorded whatever it found as the baseline, which
kept dismissals intact but also meant a unit that failed while OpsDeck was
down was never announced at all.
Metrics
units.running, units.failed, units.userRunning and units.userFailed,
sampled every 60 s.
Config
OPSDECK_MOD_SYSTEMD_JOURNAL_LINES— journal tail length (default 200, clamped 10–1000).
Dev loop
cp -r . <core>/packages/modules/systemd
cd <core>
deno check packages/modules/systemd/backend/mod.ts
OPSDECK_AUTH=disabled OPSDECK_MODULES=systemd deno task start
# http://localhost:8080/m/systemd — schema at /api/core/modules/systemd/ui
Remove the copy afterwards; this repo is the source of truth.
Production loading: OPSDECK_EXTERNAL_MODULES=<git-url>#<sha> on the host.