---
module: vault.dashboard
title: Dashboard
tab: Internal
order: 20
audience: internal
---

Ops/support view of the HiveIQ **Vault Dashboard** — the landing view of `vault.bcos.cloud`. It renders four summary stat cards plus a "Recent Visits" table of CIT (Cash-in-Transit) service calls. Owned entirely by `hiveops-vault` (port 8094, DB `hiveiq_vault`).

## What the dashboard actually calls

On mount, `DashboardView.svelte` fires **two** requests in parallel (`Promise.all`):

| UI element | Endpoint | Backing service/method |
|---|---|---|
| 4 stat cards | `GET /api/vault/stats` | `VaultStatsService.getStats(institutionKey)` |
| Recent Visits table | `GET /api/vault/visits/recent` | `CitVisitService.getRecent(institutionKey)` (top 10 by `visitDate` DESC) |

If either call throws, the whole view shows a single toast: **"Failed to load dashboard"** and the cards fall back to `0` / the table shows "No visits recorded yet." There is no per-card error state — a red toast on this page almost always means one of those two GETs returned non-2xx.

## Roles required

**None beyond a valid JWT.** `SecurityConfig` enforces only `authenticated()` on `anyRequest()`. There is **no** `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` gate anywhere in vault. Access is controlled purely by JWT validity + **institution scoping** (`VaultPrincipal.institutionKey()` decoded from the token). Any authenticated HiveIQ user with the Vault module can open this page and see their own institution's data.

## First things to check when it misbehaves

1. **"Failed to load dashboard" toast** — hit both endpoints directly with the user's JWT:
   - `GET https://api.bcos.cloud/vault/api/vault/stats`
   - `GET https://api.bcos.cloud/vault/api/vault/visits/recent`
   A 401 = expired/bad token (auth issue, not vault). A 403/CORS = NGINX routing. A 500 = vault backend — check `docker compose logs hiveiq-vault`.
2. **Cards all show 0 but the user has visits** — check whether the JWT carries an `institutionKey`. Stats queries filter `WHERE v.institutionKey = :key`; a **null** key matches nothing, so every card reads 0. (See MSP-admin gotcha below.)
3. **Cash Added / Removals YTD look wrong** — these are computed from child rows, not the visit header:
   - *Cash Added YTD* = `SUM(cit_cassette_counts.added_count * denomination)` for visits with `visit_date >= Jan 1 (UTC)`.
   - *Removals YTD* = `SUM(cit_removals.amount)` over the same window.
   A visit with a header but no cassette/removal child rows contributes 0. Values are in **cents**; the frontend formats them.
4. **Empty table / stale numbers after a CSV import** — imports are `@Async` (POST `/imports` returns 202). If a visit isn't showing, confirm the import reached `COMPLETED` via `GET /api/vault/imports/{id}` before assuming a dashboard bug.
5. **Wrong ATM's institution / device metadata off** — dashboard stats and recent visits read `cit_*` tables directly and do **not** depend on the `device_summary` mirror, so a stale device mirror does **not** corrupt these numbers. (The mirror only matters for the ATM picker/list elsewhere.)

## Common failure modes → fix

| Symptom | Likely cause | Fix |
|---|---|---|
| All requests 401 | Expired/invalid JWT | Re-login; verify `JWT_SECRET` matches the shared platform secret |
| 403 / CORS error in console | NGINX not routing `api.bcos.cloud/vault/` | Vault disables Spring CORS on purpose — CORS lives in NGINX; check the vault server block |
| 500 on `/stats` or `/visits/recent` | Backend down / DB unreachable | `docker compose logs hiveiq-vault`; check `DB_HOST`/`DB_PORT`/`DB_NAME=hiveiq_vault` connectivity |
| Cards = 0 for an MSP admin | Null `institutionKey` on token | Known asymmetry (below); not a data-loss bug |
| Times/YTD boundary look off by a few hours | All windows computed in **UTC** (`ZoneOffset.UTC`) | Expected — month/YTD boundaries are UTC, not local |

## MSP-admin gotcha (important)

For a multi-institution admin whose JWT has **no** `institutionKey`:
- **Recent Visits** returns `findTop10ByOrderByVisitDateDesc()` — i.e. visits across **all** institutions.
- **Stats cards** filter `institutionKey = null`, which matches nothing → **all four cards read 0**.

So an MSP admin sees a populated table under empty (0) cards. That inconsistency is a real code behavior, not a config problem — flag it rather than "fixing data." See the Architect tab and uncertainties.

## Where things live

- Frontend view: `hiveops-vault/frontend/src/components/Dashboard/DashboardView.svelte`
- API client: `hiveops-vault/frontend/src/lib/api.ts` (`statsAPI.get`, `visitAPI.recent`)
- Backend: `StatsController` / `VaultStatsService`, `CitVisitController#getRecentVisits` / `CitVisitService#getRecent`
- Runtime config injected by `frontend/entrypoint.sh` → `VITE_API_URL = https://api.bcos.cloud/vault`
