chore(dashboard): Phase 5 P1 — remove dead SSE log-stream claim

Closes the long-standing SSE TODO. The previous attempt with
`fastify-sse-v2 ^4` was incompatible with Fastify 5 and was never wired
in; the README/DEPLOYMENT.md kept advertising "real-time log streaming"
that didn't exist. The web client never used EventSource — `web/src/
lib/api.ts` already polls `/deployments/:id/logs` via the normal
`apiRequest` helper.

Resolution: remove the claim, not ship the feature.

  - drop `fastify-sse-v2` dep from `backend/package.json` + lockfile
  - delete the commented-out plugin import + register in `server.ts`,
    replace with a NOTE explaining the JSON-polling decision and how
    to add a stream later (`reply.raw`)
  - remove the `TODO: Re-enable SSE` comment in `deployments/routes.ts`;
    the endpoint already returns JSON, document that explicitly
  - rewrite the README "Deployment Log Streaming" section as
    "Deployment Logs" (JSON-polled, no SSE); fix the endpoint table
  - flip the DEPLOYMENT.md bullet from "Real-time log streaming (SSE)"
    to "Deployment log retrieval (JSON polling — no SSE)"
  - mark REVIEW_ACTIONS #4 RESOLVED with the reasoning
  - tick the roadmap checkbox

If a real-time stream is wanted later, ship it explicitly via
`reply.raw` and update README/DEPLOYMENT.md/the route comment in the
same change. Don't reintroduce a half-disabled plugin.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Hermes VM 2026-05-30 07:00:07 +00:00
parent 18180aab78
commit 3fc471e880
8 changed files with 19 additions and 123 deletions

View File

@ -105,7 +105,7 @@ The dashboard includes these production-ready features:
- ✅ Error boundary
- ✅ CSRF protection with token refresh
- ✅ Service CRUD operations
- ✅ Real-time log streaming (SSE)
- ✅ Deployment log retrieval (JSON polling — no SSE; see backend README)
- ✅ Audit logging
- ✅ Structured logging
- ✅ Database migrations

View File

@ -23,7 +23,7 @@ dashboard/
- **Service Registry**: Manage all ByteLyst services (trading, notes, clock, etc.)
- **Deployment Orchestration**: Trigger deployments via existing bash scripts
- **Health Monitoring**: Real-time health checks for all services with caching
- **Deployment History**: Audit trail of all deployments with log streaming
- **Deployment History**: Audit trail of all deployments with captured logs (JSON-polled by the web client; no SSE)
- **Cross-Navigation**: One-click link to Platform Admin dashboard
- **Hermes Mission Control**: Read-only mock dashboard for portfolio-wide execution, task ledger, product health, history, agents, and settings
- **Testing**: Vitest for backend, React Testing Library for frontend
@ -50,11 +50,9 @@ dashboard/
- Validated path parameters, query parameters, and request bodies
- Strict validation on update operations to prevent accidental field changes
### Deployment Log Streaming
- Added SSE endpoint for real-time log streaming (`GET /api/deployments/:id/logs`)
- Frontend EventSource integration with cleanup function
- Automatic polling for running deployments (1-second interval)
- Proper connection cleanup on client disconnect
### Deployment Logs
- Endpoint `GET /api/deployments/:id/logs` returns the full captured stdout/stderr + current status as a single JSON payload (admin only).
- The web client polls this endpoint while a deployment is `running`. There is intentionally no SSE/WebSocket stream — the previous attempt with `fastify-sse-v2` was incompatible with Fastify 5 and was removed. If a real-time stream is needed later, implement it explicitly via `reply.raw` and update this section in the same change.
### Security Enhancements
- Added rate limiting: 100 requests per minute per IP
@ -163,7 +161,7 @@ Production deployments use `https://api.bytelyst.com/devops` for `NEXT_PUBLIC_DE
- `GET /api/deployments` - Recent deployments (with `?limit=` query param)
- `GET /api/deployments/service/:serviceId` - Deployments for specific service
- `GET /api/deployments/:id` - Single deployment
- `GET /api/deployments/:id/logs` - Stream deployment logs via SSE
- `GET /api/deployments/:id/logs` - Get captured deployment logs as JSON (web client polls this; no SSE)
- `POST /api/deployments/trigger/:serviceId` - Trigger deployment (admin only)
### Health

View File

@ -36,10 +36,8 @@ Backend has 12 modules (`services`, `deployments`, `health`, `audit`, `backup`,
- Action: add `*.test.ts` for at least `auth`, `csrf`, `deployments/orchestrator`, and `health` repository before adding more features. Mirror the style of <ref_file file="/opt/bytelyst/learning_ai_devops_tools/dashboard/backend/src/modules/services/services.test.ts" />.
- Add `pnpm test:coverage` to CI and fail under a threshold (start at 50 %, raise over time).
### 4. SSE deployment-log streaming is disabled
`backend/src/server.ts` and `backend/src/modules/deployments/routes.ts` contain `TODO: fastify-sse-v2 has compatibility issues with Fastify 5`, with the SSE plugin commented out. The README still advertises real-time log streaming and the frontend code in <ref_file file="/opt/bytelyst/learning_ai_devops_tools/dashboard/web/src/app/page.tsx" /> imports `LogViewer`, so the user-facing feature is silently broken.
- Action: pin a Fastify-5-compatible SSE library (`@fastify/eventsource`, `fastify-sse-v2 >= 5`, or a small handcrafted handler using `reply.raw`) and re-enable the route, OR remove the SSE claims from `README.md` / `ENDPOINTS.md` until it ships. Choose one — do not leave the gap.
### 4. SSE deployment-log streaming is disabled — RESOLVED (removed)
The TODO has been resolved by **removing the SSE claim**, not by shipping it: the `fastify-sse-v2` dependency is gone from `backend/package.json`, the commented-out import + plugin registration are gone from `backend/src/server.ts`, and the deployment-log endpoint is now documented as JSON-polled. The web client never used `EventSource` (`web/src/lib/api.ts` already polls `/api/deployments/:id/logs` via the normal `apiRequest` helper), so no UI change was required. README/DEPLOYMENT.md updated to match. If a real-time stream is wanted later, ship it explicitly via `reply.raw` and update the docs in the same change.
### 5. Documentation drift
- `README.md` says "Web port: 3000" but `docker-compose.yml` exposes web as `3049:3000`.

View File

@ -26,7 +26,6 @@
"@fastify/swagger-ui": "^5.2.1",
"dotenv": "^16.4.5",
"fastify": "^5.2.1",
"fastify-sse-v2": "^4.2.2",
"jose": "^6.1.2",
"zod": "^3.24.1"
},

View File

@ -44,8 +44,10 @@ export async function deploymentRoutes(fastify: FastifyInstance) {
return reply.send(deployment);
});
// Get deployment logs (admin only; SSE disabled due to Fastify 5 compatibility)
// TODO: Re-enable SSE when fastify-sse-v2 supports Fastify 5
// Get deployment logs (admin only). Returns the captured stdout/stderr +
// current status as a single JSON payload. The web client polls this for
// running deployments — there is intentionally no SSE/streaming variant
// (see server.ts for the full rationale).
fastify.get('/deployments/:id/logs', {
preHandler: async (req) => requireAdmin(req),
}, async (req, reply) => {

View File

@ -15,7 +15,6 @@ import { codeQualityRoutes } from './modules/code-quality/routes.js';
import { cosmosConfigRoutes } from './modules/cosmos-config/routes.js';
import { hermesOpsRoutes } from './modules/hermes-ops/routes.js';
import { vmRoutes } from './modules/vm/routes.js';
// import sse from 'fastify-sse-v2';
import rateLimit from '@fastify/rate-limit';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
@ -24,9 +23,12 @@ const fastify = Fastify({
logger: true,
});
// Register SSE plugin
// TODO: fastify-sse-v2 has compatibility issues with Fastify 5
// await fastify.register(sse);
// NOTE: there is no Server-Sent-Events log stream. `fastify-sse-v2 ^4` is not
// compatible with Fastify 5 and was never functional here. The deployment-log
// endpoint (`GET /api/deployments/:id/logs`) returns JSON; the web client
// polls it. If a real-time stream is wanted later, ship it explicitly via
// `reply.raw` or a Fastify-5-compatible plugin and update the docs in the
// same change.
// Register rate limiting
await fastify.register(rateLimit, {

103
dashboard/pnpm-lock.yaml generated
View File

@ -36,9 +36,6 @@ importers:
fastify:
specifier: ^5.2.1
version: 5.8.5
fastify-sse-v2:
specifier: ^4.2.2
version: 4.2.2(fastify@5.8.5)
jose:
specifier: ^6.1.2
version: 6.2.3
@ -1330,9 +1327,6 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
baseline-browser-mapping@2.10.29:
resolution: {integrity: sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==}
engines: {node: '>=6.0.0'}
@ -1356,9 +1350,6 @@ packages:
buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
@ -1586,9 +1577,6 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
fast-json-stable-stringify@2.1.0:
resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
@ -1604,17 +1592,9 @@ packages:
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
fastify-plugin@4.5.1:
resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==}
fastify-plugin@5.1.0:
resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==}
fastify-sse-v2@4.2.2:
resolution: {integrity: sha512-/XFZ7uyc/9C6ANabIs2bwymS0d3B2ZiJEcu4r/czpqYOEVSn+znKNrx0TraHPZkdhy2v0QNpIdYbgeLHBixMeA==}
peerDependencies:
fastify: '>=4'
fastify@5.8.5:
resolution: {integrity: sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==}
@ -1670,9 +1650,6 @@ packages:
resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==}
engines: {node: '>=6.9.0'}
get-iterator@1.0.2:
resolution: {integrity: sha512-v+dm9bNVfOYsY1OrhaCrmyOcYoSeVvbt+hHZ0Au+T+p1y+0Uyj9aMaGIeUTT6xdpRbWzDeYKvfOslPhggQMcsg==}
get-tsconfig@4.14.0:
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
@ -1727,9 +1704,6 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
ignore@5.3.2:
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
engines: {node: '>= 4'}
@ -1805,12 +1779,6 @@ packages:
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
engines: {node: '>=8'}
it-pushable@1.4.2:
resolution: {integrity: sha512-vVPu0CGRsTI8eCfhMknA7KIBqqGFolbRx+1mbQ6XuZ7YCz995Qj7L4XUviwClFunisDq96FdxzF5FnAbw15afg==}
it-to-stream@1.0.0:
resolution: {integrity: sha512-pLULMZMAB/+vbdvbZtebC0nWBTbG581lk6w8P7DfIIIKUfa8FbY7Oi0FxZcFPbxvISs7A9E+cMpLDBc1XhpAOA==}
jackspeak@3.4.3:
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
@ -2100,13 +2068,6 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
p-defer@3.0.0:
resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==}
engines: {node: '>=8'}
p-fifo@1.0.0:
resolution: {integrity: sha512-IjoCxXW48tqdtDFz6fqo5q1UfFVjjVZe8TC1QRflvNUJtNfCUhxOUw6MOVZhDPjqhSzc26xKdugsO17gmzd5+A==}
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
engines: {node: '>=10'}
@ -2226,10 +2187,6 @@ packages:
resolution: {integrity: sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
@ -2366,9 +2323,6 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@ -2513,9 +2467,6 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
vite-node@3.2.4:
resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==}
engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@ -3852,8 +3803,6 @@ snapshots:
balanced-match@4.0.4: {}
base64-js@1.5.1: {}
baseline-browser-mapping@2.10.29: {}
brace-expansion@1.1.15:
@ -3879,11 +3828,6 @@ snapshots:
buffer-equal-constant-time@1.0.1: {}
buffer@6.0.3:
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
@ -4114,8 +4058,6 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-fifo@1.3.2: {}
fast-json-stable-stringify@2.1.0: {}
fast-json-stringify@6.4.0:
@ -4135,17 +4077,8 @@ snapshots:
fast-uri@3.1.2: {}
fastify-plugin@4.5.1: {}
fastify-plugin@5.1.0: {}
fastify-sse-v2@4.2.2(fastify@5.8.5):
dependencies:
fastify: 5.8.5
fastify-plugin: 4.5.1
it-pushable: 1.4.2
it-to-stream: 1.0.0
fastify@5.8.5:
dependencies:
'@fastify/ajv-compiler': 4.0.5
@ -4209,8 +4142,6 @@ snapshots:
gensync@1.0.0-beta.2: {}
get-iterator@1.0.2: {}
get-tsconfig@4.14.0:
dependencies:
resolve-pkg-maps: 1.0.0
@ -4274,8 +4205,6 @@ snapshots:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {}
ignore@5.3.2: {}
ignore@7.0.5: {}
@ -4336,19 +4265,6 @@ snapshots:
html-escaper: 2.0.2
istanbul-lib-report: 3.0.1
it-pushable@1.4.2:
dependencies:
fast-fifo: 1.3.2
it-to-stream@1.0.0:
dependencies:
buffer: 6.0.3
fast-fifo: 1.3.2
get-iterator: 1.0.2
p-defer: 3.0.0
p-fifo: 1.0.0
readable-stream: 3.6.2
jackspeak@3.4.3:
dependencies:
'@isaacs/cliui': 8.0.2
@ -4630,13 +4546,6 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
p-defer@3.0.0: {}
p-fifo@1.0.0:
dependencies:
fast-fifo: 1.3.2
p-defer: 3.0.0
p-limit@3.1.0:
dependencies:
yocto-queue: 0.1.0
@ -4748,12 +4657,6 @@ snapshots:
react@19.2.6: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
real-require@0.2.0: {}
real-require@1.0.0: {}
@ -4906,10 +4809,6 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.2.0
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strip-ansi@6.0.1:
dependencies:
ansi-regex: 5.0.1
@ -5032,8 +4931,6 @@ snapshots:
dependencies:
punycode: 2.3.1
util-deprecate@1.0.2: {}
vite-node@3.2.4(@types/node@25.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.21.0)(yaml@2.8.4):
dependencies:
cac: 6.7.14

View File

@ -123,7 +123,7 @@ This is the biggest operational asymmetry and the reason half the ops-panel warn
- [x] **P0:** Fix the CI workspace path (`${{ gitea.workspace }}`) in `.gitea/workflows/ci.yml`, `DEPLOYMENT.md`, `scripts/deploy-hotcopy.sh` (currently point at non-existent `/opt/bytelyst/bytelyst-devops-tools/...`).
- [x] **P0:** Replace the no-op `lint` echo with real linting (`next lint` for web, minimal ESLint for backend); make `pnpm lint` fail on bad code.
- [x] **P1:** Add tests for `auth`, `csrf`, `deployments/orchestrator`, `health`, **and `hermes-ops`**; add `pnpm test:coverage` gate. *(35 new unit tests; v8 coverage thresholds gated on the six tested files in `backend/vitest.config.ts` (≥85% lines/funcs/stmts, ≥65% branches), wired into Gitea CI as a dedicated step. Today's actuals: ≥95% lines on every gated file. Ratchet up as more modules get tested.)*
- [ ] **P1:** Resolve the SSE TODO — either ship a Fastify-5-compatible log-stream or remove the SSE claim from docs/UI.
- [x] **P1:** Resolve the SSE TODO — either ship a Fastify-5-compatible log-stream or remove the SSE claim from docs/UI. *(Chose **remove**: dropped `fastify-sse-v2` dep, deleted commented-out plugin import + TODO from `server.ts` and `deployments/routes.ts`, rewrote the README/DEPLOYMENT.md "Log Streaming" section as "Logs (JSON-polled, no SSE)". Web client already polls `/deployments/:id/logs` via `apiRequest` — no UI change needed. If a real-time stream is wanted later, implement via `reply.raw` and update docs in the same change.)*
- [ ] **P1:** Fix doc drift (web port 3000 vs 3049; endpoint URLs; merge duplicate deployment docs).
- [ ] **P1:** Document the docker-socket + host-log/script mount privilege surface (the backend reads cross-user/host paths — blast radius must be written down; consider an allow-list wrapper over the raw socket).
- [ ] **P2:** Structured backend logging (pino → stdout); wire E2E (`hermes.spec.ts`) into CI with a started stack.