fjx-mgmt deployment recipe

fjx-mgmt is the long-running operator surface for FJX (see openspec/changes/afk-mode/specs/mgmt-service/spec.md). It serves the mobile-first dashboard and the MCP endpoint over the per-project telemetry on disk. It is read-only at MVP — no Forgejo or telemetry mutations.

Image

Built and published by .forgejo/workflows/images.yaml on every v* tag:

  • Image: ${REGISTRY}/${OWNER}/fjx-mgmt:${VERSION} (also :latest)
  • Local build: just images::build fjx-mgmt
  • Local smoke: just images::smoke fjx-mgmt

The Dockerfile is multi-stage. The builder runs deno run -A dev.ts build to produce the Fresh _fresh/ bundle; the runtime stage carries only the deno binary, the source tree, and the prebuilt bundle. It runs as the unprivileged deno user shipped by the base image.

Required configuration

Environment variables (read once at startup by mgmt/lib/config.ts):

Variable Required Default Notes
FJX_PROJECTS_DIR yes Path inside the container that holds one subdirectory per project; each must contain fjx.json
FJX_MGMT_PORT no 8000 HTTP listen port

Volume mounts

fjx-mgmt reads two things from each project directory:

  • <project>/fjx.json — project config (schema)
  • <project>/telemetry/*.jsonl and <project>/fjx.sqlite — telemetry shards and (optional) index

It writes nothing. Mount the projects directory read-only:

docker run -d --restart=unless-stopped \
  --name fjx-mgmt \
  -p 8000:8000 \
  -v /srv/fjx/projects:/projects:ro \
  -e FJX_PROJECTS_DIR=/projects \
  ${REGISTRY}/${OWNER}/fjx-mgmt:latest

The dashboard renders an empty-state when FJX_PROJECTS_DIR is empty — useful before the first project lands.

Routes

  • GET / — dashboard (active runs, recent failures, stuck work, tokens today)
  • GET /runs — cross-project runs list with filters
  • GET /projects/<project>/runs/<run_id> — run detail with event timeline
  • GET /projects/<project>/issues/<id> — issue view (brief + per-role ledgers)
  • POST /mcp — MCP JSON-RPC endpoint (tools: fjx-recent-activity, fjx-failed-runs, fjx-token-usage, fjx-stuck-work)

Authentication

Deferred. The MVP ships with no auth in front of the service. Deploy it on a network that is not reachable from the public internet — a Tailscale tailnet or a VPN-only host. The spec records this as an explicit non-goal (openspec/changes/afk-mode/proposal.md, "Out of scope: auth design for the mgmt service beyond Tailscale-only"). Revisit when adding mutating endpoints.

Upgrades

The service is stateless. Roll forward with a fresh container; the SQLite index and JSONL shards live in the mounted projects directory and survive the swap. Downtime during the swap is the time to pull and restart — measured in seconds for a warm registry.

Healthcheck

GET / returns 200 with HTML once the routes are mounted. A minimal Docker healthcheck:

HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
  CMD wget -qO- http://127.0.0.1:8000/ >/dev/null || exit 1