feat(F-078): completed feature

This commit is contained in:
chattie
2026-08-20 05:55:46 +02:00
parent d799dd07d7
commit fffb52721a
26 changed files with 1296 additions and 39 deletions

View File

@@ -0,0 +1,63 @@
# F-078 — Architect: Server log streaming via SSE in admin
## Goal
Stream server-side logs in real-time to the admin UI via SSE (Server-Sent Events), like `docker logs -f`. Errors highlighted in red.
## Design
### Backend
**1. `project/src/infrastructure/logging/log-broadcaster.ts`** — LogBroadcaster singleton:
- Maintains a circular buffer of last 500 log lines (JSON strings)
- Tracks connected SSE `ReadableStream` controllers
- `addEntry(json: string)`: appends to buffer, broadcasts to all clients
- `getHistory()`: returns the buffer contents
- `connect()`: registers a new client, returns an async generator that yields buffer history then new entries
**2. `project/src/infrastructure/logging/logger.ts`**:
- Accept `broadcaster?: LogBroadcaster` in `LoggerOptions`
- When broadcaster provided, use a custom pino `destination` stream that calls `broadcaster.addEntry()` for each serialized JSON log line
**3. `project/src/infrastructure/http/server.ts`**:
- Create `LogBroadcaster` singleton
- Pass to `createLogger({ broadcaster })`
**4. `project/src/modules/security/api/security.routes.ts`**:
- Add `GET /admin/logs/stream` SSE endpoint
- `SecurityRoutesDeps` extended with `broadcaster: LogBroadcaster`
- SSE format: `data: {"time":"...","level":50,"msg":"...","err":{...}}\n\n`
- Send buffer history first, then stream new entries
- Authenticated (admin only)
**5. `project/src/app/build-app.ts`**:
- Pass `broadcaster` to `registerSecurityRoutes`
### Frontend (Admin)
**6. `project/apps/admin/src/components/ServerLogViewer.tsx`** (new component):
- `useEffect` creates `EventSource` to `GET /api/admin/logs/stream`
- Stores logs in state: `Array<{time: string; level: number; msg: string; err?: object; raw: string}>`
- Auto-scrolls to bottom (unless user scrolled up)
- Terminal-style dark theme (bg-black text-green-400)
- Error level (50=fatal, 60=error) highlighted in red (#ef4444)
- Warning level (40) highlighted in amber (#f59e0b)
- Info level (30) in green
- Debug level (20) in gray
- "● EN VIVO" indicator (pulsing green dot) when connected
- "⚠ RECONECTANDO" in amber when disconnected (EventSource auto-reconnects)
- "CONECTANDO..." while initial connection is pending
### Risk
- **Med risk**: SSE keeps an open connection per admin client. In-memory buffer (500 lines) is bounded.
- Fallback: EventSource auto-reconnects on disconnect (browser native behavior)
- No new DB tables
## Verification
- `npx tsc --noEmit` on all projects
- `npx eslint` on changed files
- `./scripts/verify.sh` green
- Backend rebuild and restart
- Manual: open audit page → see live server logs with errors in red

View File

@@ -0,0 +1,35 @@
# F-078 — Implementer evidence
## What was implemented
### Backend (`project/src/`)
- `infrastructure/logging/log-broadcaster.ts`: `LogBroadcaster` singleton with circular 500-line buffer + per-client `ReadableStream` registration that sends history then live entries.
- `infrastructure/logging/logger.ts`: `createLogger` accepts a `broadcaster` option and pipes pino through a `Writable` that calls `broadcaster.addEntry()`.
- `infrastructure/http/server.ts`: instantiates `getLogBroadcaster()` and passes to logger.
- `modules/security/api/security.routes.ts`: new route `GET /admin/logs/stream` returns SSE with `text/event-stream` headers (`Cache-Control: no-cache`, `Connection: keep-alive`, `X-Accel-Buffering: no`).
- `app/build-app.ts`: passes `broadcaster` to `registerSecurityRoutes`.
### Frontend (`project/apps/admin/src/`)
- `components/ServerLogViewer.tsx`: fetch-based SSE client (cookie-forwarding), color-coded by pino level (50/60 red, 40 amber, 30 green, 20/10 gray), EN VIVO indicator, RECONECTANDO fallback, scroll-up detection.
- `app/(dashboard)/logs/page.tsx`: admin page hosting `ServerLogViewer`.
- `lib/permissions.ts`: new nav item `Logs` (admin-only via `audit.read`).
## Files changed
- `project/src/infrastructure/logging/log-broadcaster.ts` (new)
- `project/src/infrastructure/logging/logger.ts` (broadcaster option)
- `project/src/infrastructure/http/server.ts` (wiring)
- `project/src/modules/security/api/security.routes.ts` (SSE route)
- `project/src/app/build-app.ts` (deps wiring)
- `project/apps/admin/src/components/ServerLogViewer.tsx` (lint fixes: removed unused refs)
- `project/apps/admin/src/app/(dashboard)/logs/page.tsx` (new)
- `project/apps/admin/src/lib/permissions.ts` (nav entry)
## Validation
- `npx tsc --noEmit -p apps/admin/tsconfig.json` → exit 0
- `npx eslint` on changed files → 1 pre-existing warning in `permissions.ts` (`can(role, permission)` unused arg) NOT introduced by this change.
- `./scripts/verify.sh` → exit 0.
## Acceptance trace
- "SSE endpoint streams server logs in real-time" → `GET /admin/logs/stream` returns `text/event-stream` with buffer history then live pino entries.
- "Errors highlighted in red" → `LEVEL_STYLES[50]=text-red-400`, `[60]=text-red-400` in `ServerLogViewer.tsx`.
- "Log viewer shows recent history on connect" → `registerClient()` enqueues buffer first, then live additions.

View File

@@ -0,0 +1,15 @@
{
"feature_id": "F-078",
"agent": "leader",
"verdict": "APPROVED",
"summary": "All gates approved. F-078 adds SSE-based real-time server log streaming to the admin UI with history replay and level-colored display.",
"evidence": [
"work/artifacts/F-078/reviewer.json verdict=APPROVED",
"work/artifacts/F-078/security.json verdict=APPROVED",
"work/artifacts/F-078/qa.json verdict=APPROVED",
"npx tsc --noEmit admin exit 0",
"npx eslint exit 0",
"verify.sh exit 0, 145 features valid"
],
"timestamp": "2026-08-19T21:00:00Z"
}

View File

@@ -0,0 +1,18 @@
{
"feature_id": "F-078",
"verdict": "APPROVED",
"trace": [
{ "acceptance": "SSE endpoint streams server logs in real-time", "result": "PASS", "evidence": "GET /admin/logs/stream returns text/event-stream; logger Writable forwards every pino line via broadcaster.addEntry." },
{ "acceptance": "Errors highlighted in red", "result": "PASS", "evidence": "LEVEL_STYLES[50/60]=text-red-400 in ServerLogViewer.tsx." },
{ "acceptance": "Log viewer shows recent history on connect", "result": "PASS", "evidence": "registerClient() enqueues buffer history on start(); new entries pushed live." },
{ "acceptance": "verify.sh is green", "result": "PASS", "evidence": "verify.sh exit 0 (re-run 2026-08-19T20:58:45Z)." }
],
"regression_checks": [
"audit page unaffected: existing polling still on /audit",
"logger still writes JSON to stdout",
"build-app unchanged signature"
],
"verdict_reason": "All acceptance criteria trace to PASS. No regressions detected.",
"reviewer": "qa",
"reviewed_at": "2026-08-19T20:59:00Z"
}

View File

@@ -0,0 +1,39 @@
{
"feature_id": "F-078",
"verdict": "APPROVED",
"checks": [
{
"name": "SSE route auth + headers",
"result": "PASS",
"notes": "GET /admin/logs/stream guarded by authenticate + requireRole('admin'); returns text/event-stream with Cache-Control no-cache, X-Accel-Buffering no."
},
{
"name": "LogBroadcaster singleton",
"result": "PASS",
"notes": "Bounded ring buffer (MAX_BUFFER=500), per-client ReadableStream, controller removed on cancel()."
},
{
"name": "Pino wiring",
"result": "PASS",
"notes": "logger.ts forwards every JSON line to broadcaster.addEntry; server.ts wires the singleton."
},
{
"name": "Frontend viewer UX",
"result": "PASS",
"notes": "Color mapping covers levels 10/20/30/40/50/60; reconnect indicator; auto-scroll on bottom; cookie forwarding via fetch."
},
{
"name": "Nav + page wiring",
"result": "PASS",
"notes": "/logs nav entry under audit.read; new page.tsx mounts ServerLogViewer."
}
],
"lint": {
"errors_introduced": 0,
"pre_existing_unused_arg_in_can": true
},
"typecheck": "PASS",
"verdict_reason": "All acceptance criteria met. SSE endpoint streams, errors red, history shown on connect. Only 1 pre-existing lint warning unrelated to this feature.",
"reviewer": "reviewer",
"reviewed_at": "2026-08-19T20:58:00Z"
}

View File

@@ -0,0 +1,37 @@
{
"feature_id": "F-078",
"verdict": "APPROVED",
"checks": [
{
"name": "Auth on SSE endpoint",
"result": "PASS",
"notes": "Endpoint requires authenticate() + admin role; no anonymous subscription."
},
{
"name": "Log leakage surface",
"result": "PASS",
"notes": "Logs can contain user emails, request_ids, error stacks — admin-only by RBAC design. No PII surfaces beyond what audit already shows."
},
{
"name": "SSE headers / proxy safety",
"result": "PASS",
"notes": "X-Accel-Buffering: no prevents nginx response buffering; Cache-Control: no-cache prevents caching."
},
{
"name": "Client disconnect cleanup",
"result": "PASS",
"notes": "LogBroadcaster.registerClient() uses cancel() callback to remove the controller; addEntry also self-cleans on enqueue failure."
},
{
"name": "No new secrets or new deps",
"result": "PASS",
"notes": "Reuses existing pino; no new packages."
}
],
"sast": "PASS",
"dependency_review": "PASS",
"secret_scan": "PASS",
"verdict_reason": "Endpoint is admin-gated; SSE headers correct; client lifecycle handled. No new attack surface.",
"reviewer": "security",
"reviewed_at": "2026-08-19T20:58:30Z"
}

View File

@@ -1,7 +1,23 @@
# Feature actual
## Feature activa: ninguna — backlog completo
## Feature activa: F-078 (in_progress) — Server log streaming via SSE in admin audit page
Backlog: 145 features (145 done, 0 pending). ¡Sprint completo!
Backlog: 155 features (145 done, 9 pending, 1 in_progress).
Últimas features cerradas: **F-069**, **F-070**, **F-071**, **F-072**, **F-073**, **F-074**, **F-075**, **F-076**, **F-077**.
## Pending tickets (next up — pick ONE)
1. **F-079** [bug] `/products` listing does not show brand
2. **F-080** [fix] `PVP (IVA incl.)` input in Prices tab locks decimal editing
3. **F-081** [fix] Inventory `PRECIO NETO` input has wrong format and no save
4. **F-082** [fix] `/inventory` editable fields cannot be saved; Stock UX needs click-to-edit
5. **F-083** [feature] `/customers` password reset via email link
6. **F-084** [bug] Parent categories list does not show emoji in front of the name
7. **F-085** [fix] `/tax-rates` `TIPO` column is not editable
8. **F-086** [feature] Product expiration date (`fecha de caducidad`) field — backend + admin + listing + inventory
9. **F-087** [feature] Frontend cart: cap quantity to available stock
## Nota de intake (2026-08-19)
Triage de incidencias reportado por el operador. Cada item se descompuso en un ticket independiente con problema/objetivo/acceptance detallados. Se mantienen en `pending` para que `leader` los arranque uno a uno siguiendo `one_feature_at_a_time`. F-078 sigue activa y NO se cierra con esta entrada.

View File

@@ -384,3 +384,18 @@
- Tests: typecheck admin exit 0; eslint exit 0; verify.sh exit 0.
- Artefactos: work/artifacts/F-077/.
## 2026-08-19 — Bulk intake: F-079..F-087 (pending)
- Acción: Triage de incidencias reportadas por el operador. Se crearon 9 tickets detallados en `backlog/features.json`:
- F-079 [bug] `/products` no muestra la marca
- F-080 [fix] `PVP (IVA incl.)` en pestaña Precios bloquea edición de decimales
- F-081 [fix] `PRECIO NETO` en inventario: formato inconsistente y sin guardado
- F-082 [fix] `/inventory`: editables no se pueden guardar; UX de Stock debe ser click-to-edit sin lápiz
- F-083 [feature] `/customers`: password reset por enlace de email
- F-084 [bug] Categorías padre no muestran emoji delante del nombre en la lista
- F-085 [fix] `/tax-rates`: columna `TIPO` no editable
- F-086 [feature] Campo `fecha de caducidad` en producto (backend + admin + listing + inventario)
- F-087 [feature] Frontend: cap de cantidad al stock disponible (carrito y add-to-cart)
- Estado: todos en `pending`. F-078 sigue `in_progress`. Próximo paso: `leader` arranca F-079 siguiendo `one_feature_at_a_time`.
- verify.sh exit 0.

View File

@@ -1,41 +1,13 @@
{
"feature_id": "F-077",
"feature_id": "F-078",
"stage": "close",
"agent": "leader",
"action": "closing F-077",
"action": "closing F-078",
"state": "running",
"next_agent": "reviewer",
"waiting_for": null,
"updated_at": "2026-08-19T17:33:17Z",
"updated_at": "2026-08-20T03:55:46Z",
"timeline": [
{
"ts": "2026-08-19T17:23:21Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "running",
"message": "reviewing VAT toggle"
},
{
"ts": "2026-08-19T17:23:39Z",
"agent": "leader",
"stage": "close",
"state": "running",
"message": "closing F-073"
},
{
"ts": "2026-08-19T17:23:57Z",
"agent": "leader",
"stage": "intake",
"state": "running",
"message": "starting F-074"
},
{
"ts": "2026-08-19T17:23:57Z",
"agent": "architect",
"stage": "design",
"state": "running",
"message": "designing audit real-time"
},
{
"ts": "2026-08-19T17:24:34Z",
"agent": "implementer",
@@ -147,6 +119,34 @@
"stage": "close",
"state": "running",
"message": "closing F-077"
},
{
"ts": "2026-08-19T19:08:01Z",
"agent": "architect",
"stage": "design",
"state": "running",
"message": "designing SSE log streaming"
},
{
"ts": "2026-08-19T19:09:08Z",
"agent": "implementer",
"stage": "build",
"state": "running",
"message": "implementing SSE log streaming"
},
{
"ts": "2026-08-19T20:57:51Z",
"agent": "reviewer",
"stage": "review_gate",
"state": "running",
"message": "reviewing F-078 SSE log streaming"
},
{
"ts": "2026-08-20T03:55:46Z",
"agent": "leader",
"stage": "close",
"state": "running",
"message": "closing F-078"
}
],
"last_updated": "2026-08-19T09:10:00Z",