No description
  • TypeScript 63.4%
  • Svelte 36.6%
Find a file Use this template
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Julian Imhof 9cbd3bf890
All checks were successful
Verify / verify (push) Successful in 28s
ci: verify a module the way the host loads it
A module repository has no build of its own, so nothing checked anything
here: format, lint and types were enforced nowhere, and the two things
that actually disable a module at startup — a manifest core rejects, a
frontend the builder refuses — were first observed on a server, in a
notification.

The workflow runs the standalone checks in this repo (`deno fmt --check .`,
`deno lint`), then clones core and reproduces the host's own path against
it: `readManifest` for the manifest, `deno check` over every .ts file,
svelte-check for what lives inside a template, the module tests, the
module builder for the frontend, and one import of the backend entry to
catch an error thrown while the module file is evaluated.

Core is cloned at its default branch rather than a pin — a module that
stops building against current core is the thing worth hearing about —
and the Deno installed is the version core's docker/Dockerfile pins, so
the toolchain is the one the module will run under. Nothing here needs a
secret, which is why the clone is over HTTPS and a fork's pull request
gets the full check.

The markdown reflow in README.md and CLAUDE.md is `deno fmt`: those files
had never been formatted, since this repo lives outside a core checkout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 10:34:45 +02:00
.forgejo/workflows ci: verify a module the way the host loads it 2026-08-19 10:34:45 +02:00
backend feat: adopt host-owned task runs and schedules 2026-08-11 23:28:29 +02:00
frontend feat: adopt host-owned task runs and schedules 2026-08-11 23:28:29 +02:00
CLAUDE.md ci: verify a module the way the host loads it 2026-08-19 10:34:45 +02:00
opsdeck.module.json feat: OpsDeck module template 2026-07-17 15:37:13 +02:00
README.md ci: verify a module the way the host loads it 2026-08-19 10:34:45 +02:00
tsconfig.check.json feat: adopt host-owned task runs and schedules 2026-08-11 23:28:29 +02:00

OpsDeck module template

Template for an OpsDeck module as an external git repository. It runs out of the box with fake data and touches every module capability, so adapting means deleting what you don't need and renaming what you keep.

Adapt it

  1. Create your repo from this template (Forgejo: Use this template).

  2. Rename the placeholder — one manifest edit plus a sweep:

    # manifest: name, title, icon, order
    $EDITOR opsdeck.module.json
    # code + docs reference the module by name in API paths and topics
    grep -rl "my-module" backend frontend | xargs sed -i 's/my-module/your-name/g'
    grep -rl "My Module" backend frontend | xargs sed -i 's/My Module/Your Title/g'
    

    The name must match [a-z0-9-]+; per-module env config derives from it (OPSDECK_MOD_YOUR_NAME_*).

  3. Replace the fake Thing data in backend/mod.ts with your real source (an API, ctx.exec() over allow-listed host commands, host files via ctx.paths.hostProc/hostSys/hostRoot, or ctx.dockerFetch).

Run it

Fast loop — drop it into a core checkout as a built-in:

git clone <core> && cd core
cp -r ../your-module packages/modules/your-name
deno task build            # builds shell + module frontends
OPSDECK_AUTH=disabled OPSDECK_MODULES=your-name deno task start
# http://localhost:8080/m/your-name

Production — served from git, pinned by commit:

OPSDECK_EXTERNAL_MODULES=https://git.example.com/you/your-module.git#<sha>

The host clones, validates and builds it at container startup (cached by commit hash). A module runs with the server's privileges — pin by SHA and read core/docs/security.md before installing third-party modules.

Checks

.forgejo/workflows/ci.yml runs on every push and pull request. A module has no build of its own — the host clones it and builds it at startup — so without this the first thing to notice a mistake is a server where the module is disabled, reported in a notification.

Two steps need nothing but this repository, and they are the two to run while working:

deno fmt --check .   # `deno fmt .` fixes it; the `.` is required, since this
deno lint            # repo has no deno.json and fmt then refuses to write

The rest need a core checkout: the SDK types, the Svelte runtime, the component library and the module builder all live there. Same layout as the dev loop above — the module sits at <core>/packages/modules/<name>:

cp -r . <core>/packages/modules/<name>
cd <core>
deno install --frozen
deno check $(find packages/modules/<name> -name '*.ts')
deno run -A npm:svelte-check@^4 --workspace packages/modules/<name> \
  --tsconfig ./tsconfig.check.json --threshold error
deno test --allow-read --allow-write --allow-env packages/modules/<name>
# what the host itself runs: the builder rejects any import outside the
# module contract, and a frontend that fails to build disables the frontend
deno run -A packages/module-builder/main.ts \
  packages/modules/<name> /tmp/<name>-dist <name>

CI additionally validates the manifest with the host's own validator (name pattern, SDK major) and imports the backend entry once, which is the only thing that catches an error thrown while the module file is evaluated.

It clones core at its default branch, not a pin: a module that stops typechecking or building against current core is exactly what should go red. The Deno it installs is the version core's docker/Dockerfile pins, so the toolchain matches the one the host runs. Set CORE_REPO/CORE_REF in the workflow env when core lives elsewhere.

What's in here

opsdeck.module.json   manifest: name/title, sdk major, entry points, nav
backend/mod.ts        everything server-side (see below)
frontend/             OPTIONAL custom web UI; delete it (and the manifest
                      "frontend" line) if the schema pages are enough
tsconfig.check.json   editor/svelte-check config ONLY — the builder never
                      reads a tsconfig from this repo, and the name keeps
                      Vite from picking it up (a tsconfig.json with an
                      `extends` outside this repo breaks the frontend build)
.forgejo/workflows/   CI: format, lint, typecheck, Svelte templates, tests,
                      and the host's own manifest validation and frontend
                      build against a fresh core clone (see Checks below)

backend/mod.ts — the tour

Feature Where
Portable UI schema (renders on web and Android) ctx.ui.register — fields, per-tag chart, stacked chart, list with drill-down link, detail page with :id params, admin actions with confirm
Host-owned task runs (steps, keyed progress, cancel/retry, per-field projection, Activity view) ctx.tasks.define("rescan", …); the /rescan route starts it and answers { runId } — clients follow core:task:<runId>
Persisted schedules (survive restarts, last_run intact) ctx.schedules.declare({ action: "rescan", every: "6h" })
Crash recovery (interrupted is terminal; only the module knows what re-running means) ctx.tasks.onInterrupted — retries with a retry_of link
Time-series metrics (history + rollups + live SSE) ctx.metrics.write in the ctx.scheduler.every collector (sub-5m intervals stay plain timers; slower periodic work belongs in an action + schedule)
Alerts with dedupe + click-through ctx.notifykey updates in place, link targets the affected page (web notification center + mobile push tap)
Live page refresh ctx.events.publish("things", …) + schema refresh.topics
Auth routes are authenticated by the host; gate mutations with ctx.auth.requireRole("admin"); the run records c.get("user").sub as its actor
Per-module config ctx.config.get("GREETING")OPSDECK_MOD_MY_MODULE_GREETING

Import SDK types type-only (import type … from "@opsdeck/sdk") — they are erased at runtime, so this repo has no build-time dependency on the host. The host checks the manifest's sdk major version at load.

Design rule: keep the schema dumb. Compute health states, counts and formatting server-side and expose flat fields — every client then renders the same truth.

frontend/ — the tour

Custom Svelte 5 pages, preferred by the web shell over the schema renderer. Shipping any custom frontend replaces the schema for the whole module on the web — cover every page, including param routes. Mobile always renders the schema.

Feature Where
Page registration + dashboard widget index.tsdefineModule({ pages, overview })
Command-palette entries (Ctrl/Cmd-K) registerPaletteCommands in index.ts
Typed module API fetch apiFetch("my-module", "/things")
Start an action and follow its run (keyed progress via ProgressList, survives reload) rescan card in ThingsPage.sveltesubscribeLive("core:task:<id>") + mergeProgress
Charts: history + live ticks, global time-range aware liveMetrics in ThingsPage.svelte
Refresh on backend events (push channel + polling fallback) liveRefresh("my-module:things", load, 60_000) in ThingsPage.svelte
Param routes + stale-link fallback ThingDetailPage.svelte — 404 → navigate to module root
Overview widget constraints OverviewWidget.svelte — fixed ~260px slot, clipped

Imports are restricted to svelte, @opsdeck/ui, @opsdeck/sdk/client and relative files — the builder rejects anything else so every module shares one Svelte runtime and component library. Style with the --od-* design tokens; plain same-origin <a href> links get client-side routing for free.

Reference

  • Module authoring guide: core/docs/modules.md
  • UI schema spec (all block types): core/packages/sdk/ui.ts
  • Component library: core/packages/ui/src/components/
  • Built-in modules to crib from: core/packages/modules/ (demo is the minimal one, docker the most complete)