Problem
GET /admin/stats always returns zeros. AdminStatsService.compute() (api/src/admin/admin-stats.service.ts) does not query the database at all — it returns a hardcoded snapshot:
export class AdminStatsService {
async compute(): Promise<AdminStats> {
// Placeholder zero snapshot. The query above replaces this body
// once the DB module is wired.
return {
totalUsers: 0,
totalStreams: 0,
activeStreams: 0,
eventsLast24h: 0,
generatedAt: new Date().toISOString(),
}
}
}
The comment's premise is stale: the DB module is wired, and the repository layer for every table it would count already exists (UsersRepository, StreamsDbRepository, stream_events queries in streams-db.repository.ts). The documented replacement query — four aggregate subqueries over users, streams, and stream_events — is written in the file's JSDoc but never executed. The admin dashboard (app/src/app/admin/admin-dashboard.tsx) therefore renders "0" for total users, total streams, active streams, and events last 24h, with a plausible-looking generatedAt timestamp, in every deployment.
Root cause
// api/src/admin/admin-stats.service.ts — compute()
return { totalUsers: 0, totalStreams: 0, activeStreams: 0, eventsLast24h: 0, ... }
The service injects nothing (no PG_POOL, no repository), so there is no path to real data.
Why this is architecturally hard
- The service is constructed with no dependencies, and
AdminController already wraps it in a 60s cache (CacheInterceptor plus an explicit cache.set). The fix must add a data source without breaking the caching contract — the aggregate queries are cheap and index-friendly (the JSDoc cites idx_streams_user_id and idx_stream_events_created_at), so the design question is which layer owns the query: a new admin repository, or direct PG_POOL access consistent with the codebase's repository pattern.
AdminStats (the wire shape) is consumed by the app (app/lib/api/admin-stats.ts) and the contract is stable; the change is internal. But the definition of "active streams" and "events last 24h" must be pinned (status = 'active' at query time; stream_events.created_at > NOW() - interval '24 hours') so the dashboard's labels match the query.
- This is a trust boundary: admin dashboards showing plausible zeros can mask a dead system (e.g. a failing pipeline that produces no events). The acceptance criteria should treat "non-zero when data exists" as the observable outcome, not just "the query runs".
Acceptance criteria
Service
Tests
Documentation
Out of scope
New admin metrics beyond the four existing fields, and the admin role/access story (separate issue).
Getting started
Real files in scope: api/src/admin/admin-stats.service.ts, api/src/admin/admin.controller.ts, api/src/database/database.module.ts (PG_POOL token), api/src/auth/users.repository.ts (pattern), api/src/database.integration.spec.ts (test harness), app/src/app/admin/admin-dashboard.tsx (consumer).
Verify with:
cd api && npm run typecheck && npm test
Good first files to read: api/src/admin/admin-stats.service.ts, api/src/streams/repository/streams-db.repository.ts (query style), api/src/database.integration.spec.ts.
Problem
GET /admin/statsalways returns zeros.AdminStatsService.compute()(api/src/admin/admin-stats.service.ts) does not query the database at all — it returns a hardcoded snapshot:The comment's premise is stale: the DB module is wired, and the repository layer for every table it would count already exists (
UsersRepository,StreamsDbRepository,stream_eventsqueries instreams-db.repository.ts). The documented replacement query — four aggregate subqueries overusers,streams, andstream_events— is written in the file's JSDoc but never executed. The admin dashboard (app/src/app/admin/admin-dashboard.tsx) therefore renders "0" for total users, total streams, active streams, and events last 24h, with a plausible-lookinggeneratedAttimestamp, in every deployment.Root cause
The service injects nothing (no
PG_POOL, no repository), so there is no path to real data.Why this is architecturally hard
AdminControlleralready wraps it in a 60s cache (CacheInterceptorplus an explicitcache.set). The fix must add a data source without breaking the caching contract — the aggregate queries are cheap and index-friendly (the JSDoc citesidx_streams_user_idandidx_stream_events_created_at), so the design question is which layer owns the query: a new admin repository, or directPG_POOLaccess consistent with the codebase's repository pattern.AdminStats(the wire shape) is consumed by the app (app/lib/api/admin-stats.ts) and the contract is stable; the change is internal. But the definition of "active streams" and "events last 24h" must be pinned (status = 'active' at query time;stream_events.created_at > NOW() - interval '24 hours') so the dashboard's labels match the query.Acceptance criteria
Service
compute()returns real counts:totalUsers= row count ofusers,totalStreams= row count ofstreams,activeStreams= streams withstatus = 'active',eventsLast24h=stream_eventsrows withcreated_atwithin the last 24 hours.api/src(parameterized queries,handleDbError-style error handling) and keeps the 60-second cache behaviour ofAdminControllerintact.admin-stats.service.tsare removed or rewritten to describe the implemented queries.Tests
api/src/database.integration.spec.ts) seeds users/streams/events and asserts the returned counts match, including a stream withstatus = 'inactive'that must not count towardactiveStreams.Documentation
GET /admin/statsmatches the implemented definitions (which counters, which time windows).Out of scope
New admin metrics beyond the four existing fields, and the admin role/access story (separate issue).
Getting started
Real files in scope:
api/src/admin/admin-stats.service.ts,api/src/admin/admin.controller.ts,api/src/database/database.module.ts(PG_POOLtoken),api/src/auth/users.repository.ts(pattern),api/src/database.integration.spec.ts(test harness),app/src/app/admin/admin-dashboard.tsx(consumer).Verify with:
Good first files to read:
api/src/admin/admin-stats.service.ts,api/src/streams/repository/streams-db.repository.ts(query style),api/src/database.integration.spec.ts.