# Architecture

How panel actually fits together. (An earlier revision of this document
described the original design — SvelteKit, SQLite, Casbin, SFTP; none of
that shipped. This is the real stack.)

## Topology

```
┌───────────────────────────────┐            ┌────────────────────────────┐
│         Controller            │  gRPC/mTLS │      Agent (per host)      │
│  - Embedded web dashboard     │◄──────────►│  - one persistent bidi     │
│  - REST /api/* + SSE events   │   :8443    │    stream (Agent.Connect)  │
│  - Postgres (all state)       │            │  - Docker runtime          │
│  - Internal CA + pairing      │            │  - module loader           │
│  - Scheduler (cron + CEL)     │            │  - RCON adapters           │
│  - Admin HTTP /admin/v1/*     │            │  - state tracker           │
│  - Prometheus /metrics        │            │  - SteamCMD / tar sidecars │
└───────────────────────────────┘            └────────────────────────────┘
        ▲ HTTP :8080                                │ docker.sock
   browser / panelctl / curl                 game containers (one per instance)
```

One controller orchestrates N agents. Agents connect **outbound only**
— an agent box needs no inbound panel ports, just its game ports.

## Components

**Controller** (`controller/cmd/controller`) — a single Go binary:

- **Embedded vanilla-JS dashboard.** No framework, no build step:
  `static/new.html` (shell) + `static/rcc.js` + `static/rcc.css`,
  compiled in via `//go:embed` and served hash-stamped with immutable
  caching. The legacy single-file `static/index.html` is a frozen
  "classic" UI, opt-in via the `panel_ui=classic` cookie. Live data
  reaches the browser over **SSE** (`GET /api/events`, with
  `Last-Event-ID` resume), not WebSockets.
- **Postgres** for everything durable: users/sessions, agents,
  instances + assigned ports, schedules, backup index, pair-token
  hashes, Steam credential ciphertext. Migrations are embedded and run
  at startup. (No SQLite anywhere.)
- **Internal CA** (`controller/internal/ca`, material in `data/ca/`) —
  issues the gRPC server cert and, via the pairing flow, each agent's
  client cert. `-tls auto` turns mTLS on whenever the CA exists.
- **Scheduler** — robfig/cron 6-field triggers **and** CEL expressions
  evaluated against the live event bus with sustained-for windows
  ("empty for 30 minutes → stop"). Actions: rcon, instance
  start/stop/restart, backup.
- **Auth** — argon2id + session cookies; roles are just
  `admin`/`user`; optional Sign-in-with-Steam (OpenID). No
  OIDC/WebAuthn/Casbin.
- **Admin HTTP** (`/admin/v1/*`) — out-of-band repair surface, gated by
  loopback or the `X-Panel-Admin-Token` header, usable when the
  dashboard or a session is broken.

**Agent** (`agent/cmd/agent`) — a single Go binary per game host:

- Dials the controller's gRPC port and holds one bidi stream; all
  control traffic (create/start/stop/update/backup/RCON/file ops,
  events, heartbeats) is multiplexed over `oneof` envelope messages
  (`proto/panel/v1`). Reconnect + rehydrate: instance metadata survives
  agent restarts via `--meta-dir` sidecar files rebuilt from the
  controller's DB.
- **Docker-per-game runtime** (`agent/internal/runtime`): one container
  per instance, named volumes and/or bind mounts under `--data-root`,
  optional host networking. `panel-*` images are **auto-built** from
  the module's in-tree Dockerfile on first use; third-party images are
  pulled. Windows-only servers run under Wine + Xvfb in those images.
- **Sidecars**: `steamcmd/steamcmd` for installs/updates (Windows-depot
  and beta-branch capable), alpine tar containers for backup/restore.
- **State tracker** (`agent/internal/state`): container state + RCON
  polling + log-line pattern matching → typed PlayerEvent/AppState
  events streamed upstream and fanned out to the browser (SSE), the
  scheduler (CEL), and `panelctl watch`.
- **RCON adapters** (`agent/internal/rcon`): `source_rcon`, `telnet`,
  `websocket_rcon` (Rust), `stdio`, `be_rcon` (DayZ), `docker_exec_rcon`
  — all redial on EOF.

**panelctl** (`controller/cmd/panelctl`) — operator CLI. gRPC surface
for day-to-day lifecycle (agents/instances/create/start/stop/rcon/
backup/watch), plus a `panelctl admin` HTTP surface for the repair
endpoints. Uses the same mTLS certs as agents.

**Modules** (`modules/<id>/module.yaml`) — declarative manifests: image
or Dockerfile, ports, env, volumes, config files + templated values,
RCON adapter + commands, state-source polls, log event patterns, update
providers, appearance. The loader (`pkg/module`) is strict
(`KnownFields`) — unknown keys are rejected, not ignored. The README's
"Module manifest" section documents the schema by example.

## Security model

- **Control plane:** gRPC with mTLS; agent certs are issued via
  one-time pairing tokens ([PAIRING.md](PAIRING.md)) and verified at the
  TLS handshake (`tls.RequireAndVerifyClientCert`). Plaintext exists
  only as an explicit dev opt-in (`--insecure` / `-tls off`).
- **Browser:** session cookies over your reverse proxy's TLS
  ([INSTALL.md](INSTALL.md)).
- **Secrets:** per-instance generated secrets in instance metadata;
  Steam credentials AES-GCM-encrypted with a key HKDF-derived from the
  CA key ([ADMIN.md](ADMIN.md)).
- **Blast radius:** the agent needs the Docker socket — root-equivalent
  on its host by design. Run agents on hosts you trust and don't expose
  the controller's gRPC port to the internet without cause.

## Data flow, end to end

1. Operator clicks Create → `POST /api/instances` → controller
   allocates host ports (per-agent window, DB-reserved — see
   [NETWORKING.md](NETWORKING.md)) → `InstanceCreate` envelope to the
   agent.
2. Agent resolves the manifest (env filtering, `$INSTANCE_ID` /
   `$DATA_PATH` substitution), auto-builds/pulls the image, creates the
   container, persists sidecar metadata.
3. Update → SteamCMD sidecar into the game volume. Start → container
   start; state tracker begins polling/tailing; readiness regex flips
   the card to Running.
4. Events stream to the controller → Postgres aggregates + SSE fanout →
   dashboard, scheduler CEL evaluation, `panelctl watch`.

## Not implemented (on purpose or yet)

- OIDC / WebAuthn / Casbin ACLs — local auth + Steam OpenID only.
- SFTP — the web file manager (docker exec/cp with bind-mount fast
  path) covers the common case.
- Host-mode (non-Docker) runtime — manifests declare it, the agent
  fails loudly if asked.
- Multi-tenancy/billing — this is for someone running their own boxes.
