No description
  • TypeScript 93.9%
  • Svelte 6.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Julian Imhof ae80f29248
feat: run systemctl through host-owned task runs
Core deprecated doing host work inside a route handler (core#21): the module
declares an action, the host owns the run's state, ordering and clock.

The reason this matters for systemd is the mutex. A run's identity is
(module, action, target) and the target here IS the unit, so two verbs on the
same unit can no longer interleave — a stop can never overtake the restart it
was clicked after. onSelfConflict is "wait" rather than "drop" for the same
reason: the operator asked for a specific verb, and silently discarding it
because another one is in flight leaves the unit in a state nobody asked for.

Both routes are bound to schema actions, and a schema client expects the
response to BE the outcome — a 500 carrying `error` is what puts the failure
next to the button, and the Android app has no notion of a run yet. So they
start the run and wait for its verdict (the pattern core's docker module uses
for pinning) instead of answering { runId } and leaving the caller to follow
it. `runId` still rides along for the web shell, which follows the run to show
its steps; clients that do not know the key ignore it.

Validation stays in front of the run: an unknown verb is a bad request, not a
failed operation, and starting a run for it would put a red row in Activity
for something that never touched the host. What the run re-checks is what can
go stale while it sits in the queue — the scope's user manager, and whether
systemd is still reachable.

`expose` names args and result: "restart nginx" is exactly what a viewer
should see in Activity, while systemctl's stderr names unit paths and drop-in
files, so error and stepErrors stay withheld.

Also drops the stale ctx surface in PROMPT.md, which still advertised
scheduler.every with no mention of tasks or schedules.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 12:33:33 +02:00
backend feat: run systemctl through host-owned task runs 2026-08-12 12:33:33 +02:00
frontend feat: dashboard overview card 2026-07-27 12:06:15 +02:00
CLAUDE.md feat: dashboard overview card 2026-07-27 12:06:15 +02:00
opsdeck.module.json feat: dashboard overview card 2026-07-27 12:06:15 +02:00
PROMPT.md feat: run systemctl through host-owned task runs 2026-08-12 12:33:33 +02:00
README.md feat: dashboard overview card 2026-07-27 12:06:15 +02:00

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>, scope system or 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 (when conditions) 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 chrootnsenter (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.

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 101000).

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.