12 — API Mapping (Dashboard Module)
- E1 — Dashboard overview (implemented)
- E2 — Attendance trend
(planned) - E3 — Finance trend
(planned) - E4 — List widgets
(planned) - E5 — Update widget
(planned) - E6 — Realtime (not REST)
- Rate limits (client-relevant, 00-shared/07 §4)
- Client contract summary
Exact endpoints per screen. Wire contract per 00-shared/07: base
/api/v1, Bearer JWT, success{success:true,message:"OK",data,meta?, timestamp,requestId}, error envelope with codes. Only shapes in code are used; blueprint-only endpoints marked(planned). Sources:dashboard.controller.ts,dashboard.service.ts,Dashboard.md,permissions.constants.ts,role.schema.ts,redis-cache.service.ts,ws-bridge.service.ts.
E1 — Dashboard overview (implemented)
| Endpoint | GET /api/v1/dashboard/overview (dashboard.controller.ts:13-17) |
| Guard | JwtAuthGuard only (dashboard.controller.ts:8) — RBAC dashboard.read is NOT enforced on this route today (permission exists at permissions.constants.ts:37; enforcement (planned), cf. IMPLEMENTATION_PLAN.md Phase 5 permissions audit) |
| Request | none; tenant from JWT only (never from body — 00-shared/07 §1) |
| Response | 200 envelope, data (dashboard.service.ts:50-70): {students:{total}, teachers:{total}, staff:{total}, attendance:{total, summary:{status:count}, rate}, finance:{pendingInvoices, totalDueAmount}} |
| Aggregates | students = count status:'active' (:31); teachers = count (:32); staff = count (:33); attendance = today's records {date: {$gte: startOfDay, $lt: tomorrow}} (:34), summary grouped by status (:40-43), rate = round(present/total×100), 0 when none (:57-64); finance = invoices with status in ['issued','partial','overdue'] (:35-37), totalDueAmount = Σ(totalAmount − paidAmount) (:45-48) |
| Errors | 401 UNAUTHENTICATED (missing/expired JWT); 429 RATE_LIMITED (api tier 100/min); 5xx INTERNAL (service has no custom errors) |
| Side effects | None today — no cache read/write, no event emission. Service live-scans repositories per request (dashboard.service.ts:24-38); blueprint target is Redis-served with TTL 60 s (Dashboard.md:41,49) — (planned) |
| Client | Dashboard Home first load; skeletons → values (06_Screen_Specifications.md §1.3) |
Cached response contract (planned) (blueprint target)
| Cache key | sl:{tenantId}:dashboard:overview — tenant namespacing automatic (redis-cache.service.ts:16-19); fallback platform (:17) |
| TTL | 60 s (Dashboard.md:41) |
| Invalidation | on AttendanceMarked, ResultPublished, PaymentRecorded, InvoiceGenerated (Dashboard.md:34) — handler (planned); plus scheduled rebuild worker (Dashboard.md:50) |
| Client mirror | client cache sl:{tenant}:dashboard:overview TTL 60 s (volatile, 00-shared/06 §3.3); force-refresh bypasses it (10_Interaction_Specification.md §2) |
| Staleness | client renders last-good + StaleBanner beyond 60 s; ≥10 min → offline banner (06_Screen_Specifications.md §1.3) |
E2 — Attendance trend (planned)
| Endpoint | GET /api/v1/dashboard/attendance (Dashboard.md:24) — no controller route in code |
| Request | `?period=today |
| Response | rate/summary series for period; today shape from dashboard.service.ts:54-65 |
| Cache | sl:{tenantId}:dashboard:attendance:{period} TTL 60 s (planned) |
| Client | SS2 attendance detail; PeriodSelector disabled until this ships (08_Form_Specifications.md §2) |
E3 — Finance trend (planned)
| Endpoint | GET /api/v1/dashboard/finance (Dashboard.md:25) |
| Request | ?period= (planned); accountant role access (planned) (role lacks dashboard.read — role.schema.ts:47) |
| Response | dues + pendingInvoices series; today shape dashboard.service.ts:66-69 |
| Cache | sl:{tenantId}:dashboard:finance:{period} TTL 60 s (planned) |
| Client | SS3 finance detail |
E4 — List widgets (planned)
| Endpoint | GET /api/v1/dashboard/widgets (Dashboard.md:26) |
| Guard | dashboard.read (permissions.constants.ts:37); reads user's widget layout |
| Response | ordered widget configs from dashboard_widgets (Dashboard.md:7) |
| Client | /dashboard/customize list + Home layout resolution (planned) |
E5 — Update widget (planned)
| Endpoint | PATCH /api/v1/dashboard/widgets/:id (Dashboard.md:27) |
| Guard | dashboard.widget.manage (permissions.constants.ts:38) — the only dashboard write permission; org_admin default (role.schema.ts:23) |
| Request | visibility / order / role chips (planned) |
| Errors | 404 RESOURCE_NOT_FOUND; 403 (no permission); 409 (concurrent edit) |
| Client | per-widget save with per-item spinner + conflict snackbar (10_Interaction_Specification.md §7) |
E6 — Realtime (not REST)
| Channel | WS /ws, JWT auth (ws.gateway.ts:20-23,35-44); client auto-joined to tenant:{tenantId} (ws.gateway.ts:50) |
| Events | AttendanceMarked, ResultPublished, PaymentRecorded, InvoiceGenerated (Dashboard.md:34) — broadcast by WsBridge for every domain event (ws-bridge.service.ts:16-22), payload {eventType, occurredAt, payload} (:17-21) |
| Client rule | filter to the 4 event types; 2 s debounce; force-refresh (10_Interaction_Specification.md §4) |
| Server push of fresh KPIs | (forward-looking) — bridge emits event metadata only |
Rate limits (client-relevant, 00-shared/07 §4)
| Tier | Limit | Notes |
|---|---|---|
| api | 100/min | overview/attendance/finance polling must stay within budget: 60 s poll = 1 req/min per screen, plus pull + WS-triggered refetches — acceptable |
| admin | 500/min | (planned) if widgets PATCH ships under admin tier |
Client contract summary
| Concern | Rule |
|---|---|
| Auth | Bearer JWT; 401 → single-flight refresh → replay; fail → session expiry (00-shared/06 §3.6) |
| Optimistic | none today (read-only); (planned) customize toggles optimistic with rollback (08_Form_Specifications.md §1) |
| Idempotency | GETs safe; PATCH widgets (planned) idempotent per widget |
| Offline | last-good payload + banners; no writes (09_User_Behaviour.md B8) |
| Formatting | client formats, never recalculates (09_User_Behaviour.md B9): rate pre-rounded :57-64, dues pre-summed :45-48 |
| Error mapping | 00-shared/06 §5: 401 reauth, 403 hide, 404 empty, 429 backoff poll, 5xx keep-last-good + snackbar |
| Role gating | dashboard.read today = org_admin only (role.schema.ts:23); screens hidden without permission; role views (planned) (IMPLEMENTATION_PLAN.md:232) |