Skip to content

reference

deputypwa HTTP API

The deputyOS PWA is a small axum web app that provides a LAN-trusted dashboard, log viewer, key rotation surface, and Web Push subscription endpoint. Routes are registered in deputypwa/src/routes.rs.

The deputyOS PWA is a small axum web app that provides a LAN-trusted dashboard, log viewer, key rotation surface, and Web Push subscription endpoint. Routes are registered in deputypwa/src/routes.rs.

The PWA shells out to deputyctl <subcmd> --json for everything it displays. There is no server-side state beyond the VAPID keypair and the in-memory single-shot flash slot.

[TOC]

Trust model

The PWA is LAN-trusted. Same trust model the wizard hands off when it finishes — operators on the LAN are assumed allowed. Production bakes can layer Tailscale or Cloudflare Tunnel for stronger auth without changing this code.

There is no token or session check on the PWA’s protected routes today. (The wizard’s auth flow is independent; see wizard HTTP.)

Public routes

MethodPathResponse
GET/302 Location: /app/dashboard
GET/healthz200 ok (text/plain)
GET/manifest.webmanifestThe PWA’s web app manifest (application/manifest+json).
GET/sw.jsThe service worker (text/javascript).
GET/static/style.cssBundled CSS.
GET/static/icon.svgBundled icon.

Application routes

All return HTML rendered by the bundled templates module.

MethodPathRendersUnderlying deputyctl calls
GET/app/dashboardDashboard viewdeputyctl status --json, version --json, limits --json, cost --json, doctor --json
GET/app/logs?lines=<n>Journal tail viewerdeputyctl status --json then journalctl -u <unit> -n <lines>
GET/app/keysProvider key listdeputyctl model list --json
POST/app/keys/rotateFlash → 302 to /app/keysdeputyctl model set --provider <p> --key-from-stdin --yes
POST/app/push/subscribe201 Created (text)– (writes to subscriptions file)
GET/app/push/vapid-publicVAPID public key, base64url encoded–

Per-route detail

GET /app/dashboard

Calls data::fetch_dashboard on a blocking thread. The function shells out to the five deputyctl ... --json subcommands and deserialises each into a typed view; failures fill the corresponding slice with defaults and set stub=true (so the UI shows “stub” badges rather than crashing).

In dev-stub mode (DEPUTYPWA_DEV_STUB=1) or when deputyctl is not on PATH, the function returns a synthetic dashboard so the UI is exercisable without an image.

Response shape: HTML — the dashboard template renders status, version, limits summary, cost summary, and doctor verdict.

GET /app/logs?lines=<n>

Query: lines (optional, defaults to 100; clamped to 10..1000).

Calls data::fetch_dashboard for the active unit name, then data::fetch_journal_tail(unit, lines) which shells out to journalctl -u <unit> -n <lines> on a blocking thread.

Falls back to openclaw.service if the active profile is unknown.

Response: HTML — the logs template renders the journal output as preformatted text.

GET /app/keys

Calls data::fetch_providers (which shells deputyctl model list --json). Renders the provider list, each row with a “Rotate” form that POSTs to /app/keys/rotate. The page also surfaces a single-shot flash message stored in memory.

In dev-stub mode, returns a synthetic catalogue.

POST /app/keys/rotate

Form body:

provider=<id>&api_key=<value>

Validation: both fields non-empty.

Action: spawns deputyctl model set --provider <id> --key-from-stdin --yes on a blocking thread, pipes api_key to stdin. On success flashes “Rotated successfully.”; on failure flashes the captured stderr.

In dev-stub mode, no subprocess is run — flashes (dev-stub) would rotate <id> via ….

Response: 302 Location: /app/keys so the flash renders on next render of the keys page.

POST /app/push/subscribe

Body: application/json. The full Web Push subscription object as emitted by PushManager.subscribe():

{
  "endpoint": "https://fcm.googleapis.com/fcm/send/...",
  "expirationTime": null,
  "keys": {
    "p256dh": "BNcRdr...base64url...",
    "auth":   "BTBZi...base64url..."
  }
}

Action: appends one JSON line to <data_dir>/push-subscriptions.jsonl (default /var/lib/deputyos/push-subscriptions.jsonl; tests pin via AppState::with_subscriptions_path). Mode 0640.

Response: 201 Created with body ok on success; 500 Internal Server Error with an HTML error page on failure.

GET /app/push/vapid-public

Returns the VAPID public key as base64url-encoded text/plain. Used by the service worker / front-end to pass to PushManager.subscribe().

If push is disabled (openssl wasn’t available at boot, or the keypair hasn’t been generated), returns the empty string with text/plain.

The VAPID keypair is generated lazily on first PWA startup at <data_dir>/vapid.pem (mode 0600). Generation shells out to openssl ecparam -genkey -name prime256v1. See deputypwa/src/push.rs.

Web Push subscription flow

sequenceDiagram
  participant Browser
  participant SW as Service worker
  participant PWA
  participant Push as Push provider

  Browser->>SW: register('/sw.js')
  SW->>PWA: GET /app/push/vapid-public
  PWA-->>SW: <base64url public key>
  SW->>Push: PushManager.subscribe({applicationServerKey})
  Push-->>SW: PushSubscription
  SW->>PWA: POST /app/push/subscribe (JSON)
  PWA-->>SW: 201 Created
  Note over PWA: appends to push-subscriptions.jsonl

The PWA itself doesn’t send notifications today — cost-alert and update-applied hooks are the future emitters. The subscription file is the durable record they’ll consume.

Dev-stub mode

When DEPUTYPWA_DEV_STUB=1 is set (or when deputyctl is not on PATH):

  • /app/dashboard renders a synthetic dashboard with a “stub” badge.
  • /app/keys renders a synthetic provider list.
  • /app/keys/rotate flashes (dev-stub) would rotate ... instead of shelling out.
  • /app/logs may show “(journal task failed)” if journalctl isn’t on PATH.

This makes make pwa exercisable on a developer laptop without a baked deputyOS image.

Side-effect map

OperationFiles writtenSubprocesses spawned
GET /app/dashboard–5× deputyctl ... --json
GET /app/logs–1× deputyctl status --json + 1× journalctl
GET /app/keys–1× deputyctl model list --json
POST /app/keys/rotate/etc/deputyos/secrets.env (via deputyctl)1× deputyctl model set
POST /app/push/subscribe<data_dir>/push-subscriptions.jsonl (append)–
GET /app/push/vapid-public(lazy) <data_dir>/vapid.pem (first call only)(lazy) openssl ecparam (first call only)

See also

Search

Search index isn't built yet. Run npm run build to generate it (Pagefind writes to dist/pagefind/).