diff --git a/dashboard/DEPLOYMENT.md b/dashboard/DEPLOYMENT.md
index 1ccdfbf..5cc64f9 100644
--- a/dashboard/DEPLOYMENT.md
+++ b/dashboard/DEPLOYMENT.md
@@ -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
diff --git a/dashboard/README.md b/dashboard/README.md
index c97209c..810038a 100644
--- a/dashboard/README.md
+++ b/dashboard/README.md
@@ -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
diff --git a/dashboard/REVIEW_ACTIONS.md b/dashboard/REVIEW_ACTIONS.md
index 791ba3a..7be3bd4 100644
--- a/dashboard/REVIEW_ACTIONS.md
+++ b/dashboard/REVIEW_ACTIONS.md
@@ -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 .
- 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 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`.
diff --git a/dashboard/backend/package.json b/dashboard/backend/package.json
index 52db53c..21761c8 100644
--- a/dashboard/backend/package.json
+++ b/dashboard/backend/package.json
@@ -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"
},
diff --git a/dashboard/backend/src/modules/deployments/routes.ts b/dashboard/backend/src/modules/deployments/routes.ts
index 688967a..9294d62 100644
--- a/dashboard/backend/src/modules/deployments/routes.ts
+++ b/dashboard/backend/src/modules/deployments/routes.ts
@@ -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) => {
diff --git a/dashboard/backend/src/server.ts b/dashboard/backend/src/server.ts
index 08b4e84..53cb801 100644
--- a/dashboard/backend/src/server.ts
+++ b/dashboard/backend/src/server.ts
@@ -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, {
diff --git a/dashboard/pnpm-lock.yaml b/dashboard/pnpm-lock.yaml
index 613c2d9..80b8c04 100644
--- a/dashboard/pnpm-lock.yaml
+++ b/dashboard/pnpm-lock.yaml
@@ -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
diff --git a/docs/hermes_dashboard_v2_roadmap.md b/docs/hermes_dashboard_v2_roadmap.md
index 8e53a25..f661250 100644
--- a/docs/hermes_dashboard_v2_roadmap.md
+++ b/docs/hermes_dashboard_v2_roadmap.md
@@ -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.