# Changelog v0.3.67 - 2026-05-17

### Added

- **🐳 Docker Packaging**: First-class Docker support so you can run FCM without installing Node.js. The official image is published to `ghcr.io/vava-nessa/free-coding-models` on every release and tag push.
  - Multi-arch friendly `Dockerfile` based on `node:20-alpine`, running as a non-root `fcm` user.
  - `docker-entrypoint.sh` auto-generates `~/.free-coding-models.json` from any `*_API_KEY` / `*_API_TOKEN` env vars you pass to the container — no manual config step.
  - `docker-compose.yml` template wired up for every supported provider.
  - GitHub Actions workflow (`.github/workflows/docker.yml`) handles build + publish to GHCR on `release: published`, `push: v*.*.*` tags, and `workflow_dispatch` (with a `test_mode` dry-run input).
  - Trivy vulnerability scan blocks releases that introduce any CRITICAL/HIGH CVEs.
  - Quick start: `docker run -p 19280:19280 -e OPENROUTER_API_KEY=... ghcr.io/vava-nessa/free-coding-models:latest`.
- **🌐 Combined Daemon + Web Dashboard**: The router daemon now serves the web dashboard from the same port — no more juggling two processes. New REST surface area baked into the daemon:
  - `GET /api/models` — full model catalog with latency stats, status, p95, jitter, stability, verdict, uptime, and `inRouterSet` flag.
  - `GET /api/config` — provider catalog with masked API keys (`••••••••XXXX`) and enabled state.
  - `GET /api/events` — SSE stream the dashboard subscribes to for live updates.
  - `GET /api/key/<provider>` — reveal the raw API key for a configured provider (same-origin only).
  - `POST /api/settings` — save API keys and per-provider enabled flags from the dashboard, then trigger a probe burst.
  - When you add a new provider's API key from the dashboard, FCM now mirrors `--sync-set` behavior and automatically adds that provider's best-tier model to your active router set.

### Changed

- **`--web` flag removed**: replaced by `--daemon`, which now serves both the OpenAI-compatible router API and the dashboard on the same port. Existing tooling using `--web` should switch to `--daemon`.
- **Preserve user-created router sets on daemon start**: previously the daemon rebuilt the active set from favorites or defaults on every restart, silently overwriting sets created with `--sync-set`. Named sets are now preserved.
- **Faster config reload**: `CONFIG_RELOAD_INTERVAL_MS` shortened from 60s → 10s so dashboard-driven changes (toggling providers, adding keys) propagate quickly.
- **Contributors**: welcome [@stgreenb](https://github.com/stgreenb) 🎉 — author of the daemon-web merge and Docker packaging work.

### Fixed

- **🔒 Path traversal in dashboard static file serving (security)**: requests like `GET /../../etc/passwd` could escape `web/dist/` and read arbitrary files reachable by the daemon user. The mitigation (`127.0.0.1` bind) was bypassed inside Docker where the daemon binds `0.0.0.0`. All static paths are now resolved against `WEB_DIST_DIR` and rejected with 403 when they escape.
- **🔒 Cross-site write / key exfiltration on dashboard endpoints (security)**: `POST /api/settings` (writes API keys) and `GET /api/key/<provider>` (reveals raw keys) previously accepted any request, including those triggered by malicious tabs visiting a page that fetched `http://localhost:19280/...`. Both now enforce a same-origin / loopback `Origin` header check. Header-less CLI callers (curl, native apps) keep working.
- **🔒 Config file permissions tightened in Docker**: the entrypoint previously created `~/.free-coding-models.json` with mode `0666`. Tightened to `0600` since it stores plaintext API keys.
- **🐛 `/api/key/:provider` route never matched**: the handler compared `url.pathname === '/api/key/:provider'` literally, so the endpoint was unreachable. Now correctly matches via `startsWith` and 404s unknown providers.
- **🐛 "Excellent" verdict for fully-down models**: when a model had probe history but every ping failed, `avg` collapsed to `0` and the dashboard showed "Excellent". Verdict now correctly returns `—` whenever no usable latency sample exists.
- **🐛 `--daemon-bg` mode signalled "down" for everything in dashboard**: the type-mismatch comparison (`===` between string code and number) made every model render as `down`. Codes are now compared as strings.
- **⚙️ Docker GitHub Actions workflow YAML repaired**: the top-level `env:` block and the GHCR login step were incorrectly indented and would have prevented GitHub from parsing the workflow at all.
- **⚙️ Docker container lifecycle**: the container no longer outlives a crashed daemon. The entrypoint now runs the daemon in the foreground (`--daemon` instead of `--daemon-bg`) so Docker's restart policy can recover from crashes instead of waiting for the healthcheck to time out.

### Internal

- Removed a dead `createReadStream` import in `router-daemon.js`.
- Hoisted `routerConfig()` and `getSet()` lookups out of the per-model loop in `getWebModelsPayload()` — saves ~200 redundant runtime calls per dashboard refresh — and uses a `Set` index for in-set membership checks.
- Renamed a local `window` variable to `probeWindow` to avoid shadowing globals.
- Removed a nested `router` shadow in `/api/settings`.
- Added `X-Content-Type-Options: nosniff` header on all static dashboard responses.
- Added 6 new tests covering path-traversal blocking, cross-origin write rejection, same-origin write success, CLI key fetch, and unknown-provider 404. Total: 371 tests, all green.
