---
module: vault.dashboard
title: Dashboard
tab: Claude
order: 40
audience: dev
---

Terse act-without-guessing reference. Owner: **`hiveops-vault`** (port 8094, DB `hiveiq_vault`). Frontend view `hiveops-vault/frontend/src/components/Dashboard/DashboardView.svelte`.

## Endpoints this view calls (exactly two)

| Method | Path | Handler | Returns |
|---|---|---|---|
| GET | `/api/vault/stats` | `StatsController.getStats` | `VaultStatsResponse` |
| GET | `/api/vault/visits/recent` | `CitVisitController.getRecentVisits` | `List<VisitSummaryResponse>` (top 10, `visitDate` DESC) |

- External base (via NGINX): `https://api.bcos.cloud/vault/api/vault/...`
- Frontend `VITE_API_URL=https://api.bcos.cloud/vault` (client prefixes `/stats`, `/visits/recent`).
- Both require JWT Bearer. Result scoped to token's `institutionKey`.

## Stats response fields

- `visitsThisMonth` (UTC month), `visitsPending` (`status='PENDING'`), `totalCashAddedCentsYtd`, `totalRemovalAmountCentsYtd`, `totalImportsCompleted` (returned, NOT rendered). Money is **cents**.

## Tables (all in `hiveiq_vault`, scoped by `institution_key`)

- `cit_visits` — headers; recent list + monthly/pending counts
- `cit_cassette_counts` — Cash Added YTD = `SUM(added_count * denomination)`
- `cit_removals` — Removals YTD = `SUM(amount)`
- `cit_imports` — completed-import count
- `device_summary` — Kafka mirror; **NOT used** by dashboard stats/recent

## Kafka

- Consumes: `hiveops.device.events` (group `hiveops-vault-device-sync`) → maintains `device_summary` only.
- Produces: **none**.

## Roles / auth

- No role gate. `SecurityConfig` = `authenticated()` only. **No** `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` in vault.
- Authorization = institution scoping via `VaultPrincipal.institutionKey()` from JWT.

## Gotchas

- MSP admin (JWT `institutionKey == null`): **Recent Visits shows ALL institutions** (`findTop10ByOrderByVisitDateDesc`), but **all stat cards read 0** (`institutionKey = null` matches no rows). Populated table under zeroed cards is expected behavior.
- All date windows are **UTC** (`ZoneOffset.UTC`) — month/YTD boundaries are UTC, not local.
- YTD totals come from child rows; a visit with no cassette/removal rows contributes 0.
- Dashboard error = single toast "Failed to load dashboard"; no per-card errors.
- CSV import is `@Async` (POST `/imports` → 202); poll `GET /api/vault/imports/{id}` before blaming the dashboard for missing visits.
- Spring CORS disabled; NGINX owns CORS. Prod DB uses split `DB_HOST/DB_PORT/DB_NAME` env, not `DB_URL`.

## Do NOT

- Do NOT add role checks expecting them to already exist — none do.
- Do NOT invent write endpoints for this view — dashboard is read-only.
- Do NOT read `device_summary` to explain wrong stat numbers — stats come from `cit_*`.
- Do NOT trust MSP-admin cards as authoritative (they read 0).
- Do NOT assume local-time day boundaries — everything is UTC.
- Do NOT expect a `/visits/recent` limit param — it's a fixed top-10.
