f21c783d8c
The 162 Architect/Claude docs were tagged 'audience: dev'. The 'dev' tier resolves to isInternal() = ROLE_MSP_ADMIN || ROLE_BCOS_ADMIN, so every MSP_ADMIN could read full internal architecture: source paths, DB tables, service ports, NGINX routes. Verified live on bcos.dev as msp_a@msp1.bcos.dev. Retag them to 'audience: bcos' (isBcos, BCOS_ADMIN only). Content-only change; the 'dev' tier mapping is deliberately left alone so the 15 Internal, 35 Overview and 66 Testing docs stay MSP-visible. Also add platform.guide-access documenting the audience tiers and the per-role visibility matrix, so the access model is queryable from the Guide instead of re-derived from source each time. Verified: MSP module count unchanged (116 across 17 apps); no module loses all its tabs. Guide is bcos.dev only, not deployed to production. Closes #16 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.4 KiB
5.4 KiB
module, title, tab, order, audience
| module | title | tab | order | audience |
|---|---|---|---|---|
| analytics.fleet-health | Fleet Health | Architect | 30 | bcos |
How it's built. Fleet Health is a synchronous fan-out aggregation — not a snapshot/Kafka read. Grounded in
FleetHealthController+FleetHealthService(hiveops-analytics) and the frontendFleetHealthTab.svelte. Cross-link [platform.data-architecture], [platform.kafka], [platform.service-ownership].
Services involved
| Service | Role in this feature |
|---|---|
| hiveops-analytics (owner, port 8089) | Serves GET /api/analytics/fleet-health; orchestrates + aggregates. Holds no state for this tab. |
| hiveops-incident | Source of ATM connectivity summary, recent activity events, and open incidents. |
| hiveops-journal (port 8087) | Source of open journal gaps. |
Cross-link [platform.service-ownership]: analytics owns the aggregation shape; incident/journal own the underlying records. Analytics reads no incident/journal DB tables directly — only their HTTP APIs.
Data flow (request-time, no Kafka)
Browser (FleetHealthTab.svelte)
└─ GET /api/analytics/fleet-health (Bearer JWT)
└─ FleetHealthService.buildFleetHealth(callerToken) [forwards the SAME JWT]
├─ incidentClient GET /api/atms/summary → {total,connected,disconnected,neverConnected}
├─ incidentClient GET /api/journal-events/activity?hoursBack=24 → [{atmId,eventType,...}]
├─ incidentClient GET /api/incidents/status/open → [ ...open incidents ]
└─ journalClient GET /api/journal/gaps?status=OPEN → [{atmId,...}]
└─ aggregate → FleetHealthResponse (JSON)
- No executor→Kafka→record-store pattern here. That pattern belongs to the analytics snapshot pipeline (
SnapshotCollectionService→analytics.snapshot-ready→analytics_snapshots), which powers other tabs (Overview/Trends), not Fleet Health. This tab bypasses the DB entirely. Cross-link [platform.kafka]. - Each downstream call is independently try/catch-wrapped; a failure logs a warning and substitutes an empty result, so the response degrades gracefully to partial/zero data.
Aggregation logic (in FleetHealthService)
- Connectivity: pulls
total/connected/disconnected/neverConnectedfrom/api/atms/summary;connectivityPct = connected*100.0/total(0 when total=0). - Event Breakdown: activity events (last 24h) grouped by category via
classifyEventType(eventType). Category map is seeded withHARDWARE, CASH, NETWORK, POWER, AUTH, SYSTEM, OTHER(all 0), then incremented.- Notable: there is no
OPERATIONScategory in the backend — the frontendcategoryColorsdefines one, butclassifyEventTypenever emits it; unmatched types fall toOTHER.
- Notable: there is no
- Top Problematic ATMs: groups the same 24h events by
atmId, counts, sorts desc,limit(5)→ProblematicAtm{atmId,eventCount}. - Open Incidents:
openIncidents.size()of/api/incidents/status/open. - ATMs with Journal Gaps: distinct
atmIdcount from/api/journal/gaps?status=OPEN.
Event-type → category mapping (source of truth: classifyEventType)
| Category | Event types |
|---|---|
| POWER | POWER_OFF |
| HARDWARE | CARD_READER_FAIL, DISPENSER_JAM, SELF_TEST_FAILED, APPLICATION_ERROR_CARD, APPLICATION_ERROR_DISPENSER, APPLICATION_ERROR_ENCRYPTION, APPLICATION_ERROR_PRINTER, APPLICATION_ERROR_PINPAD, MIXED_MEDIA_ERROR, CASH_ACCEPTOR_ERROR, ENCRYPTOR_PINPAD_ERROR |
| CASH | CASSETTE_LOW, CASSETTE_EMPTY, CASSETTE_ABNORMAL |
| NETWORK | NETWORK_DISCONNECTED, APPLICATION_ERROR_NETWORK |
| AUTH | AUTHENTICATION_FAILURE |
| SYSTEM | ATM_OUT_OF_SERVICE, APPLICATION_ERROR, APPLICATION_ERROR_JOURNAL, AGENT_LOG_ERROR, EJOURNAL_DB_WARNING, EJOURNAL_DB_CRITICAL |
| OTHER | anything unmatched / null |
DB tables read/written
- This feature: none. No reads or writes to
hiveiq_analytics. (Snapshot tablesanalytics_snapshots/ai_insightsexist for other tabs — see [technical.analytics].) - Underlying records live in incident and journal databases and are reached only via HTTP.
Key API endpoints
| Method | Path | Service | Used for |
|---|---|---|---|
| GET | /api/analytics/fleet-health |
analytics | The tab's single data call |
| GET | /api/atms/summary |
incident | Connectivity donut |
| GET | /api/journal-events/activity?hoursBack=24 |
incident | Event breakdown + top problematic |
| GET | /api/incidents/status/open |
incident | Open incidents count |
| GET | /api/journal/gaps?status=OPEN |
journal | ATMs with journal gaps |
Frontend
FleetHealthTab.svelterenders four cards (Connectivity donut, Event Breakdown bars, Top Problematic ATMs, Alerts & Gaps) fromFleetHealthData.- API base resolves from
window.__APP_CONFIG__.apiUrl(fallbackhttp://localhost:8089/api/analytics); call isanalyticsAPI.getFleetHealth()→GET {base}/fleet-health. FleetHealthData.topProblematicAtmscarries adeviceId"Phase 2 alias" alongsideatmId— part of the ongoing ATM→device rename. Cross-link [platform.service-ownership].
Auth
- JWT-authenticated; no role restriction (
SecurityConfig:.anyRequest().authenticated()). The caller's token is forwarded verbatim to incident/journal (WebClientConfigclients, no service account). Cross-link [platform.auth].