---
module: vault.dashboard
title: Dashboard
tab: Architect
order: 30
audience: dev
---

How the Vault Dashboard is built. It is a thin read view over `hiveops-vault` — no cross-service calls at request time, no executor/Kafka on the read path. See [platform.service-ownership].

## Services involved (for THIS feature)

Only **one** service participates on the request path: `hiveops-vault` (Spring Boot, port 8094; frontend Svelte SPA, port 5185). The dashboard does **not** call incident/journal/devices/mgmt at runtime. Vault owns the CIT domain end-to-end. See [platform.service-ownership].

- Frontend `DashboardView.svelte` → NGINX (`api.bcos.cloud/vault/` → `hiveiq-vault:8094/api/vault/`) → vault controllers → JPA repositories → PostgreSQL `hiveiq_vault`.

## Data flow (read path)

```
DashboardView.onMount
  ├─ statsAPI.get()     → GET /api/vault/stats          → StatsController
  │                                                        → VaultStatsService.getStats(key)
  │                                                          → CitVisitRepository aggregate queries
  │                                                          → CitImportRepository.countCompleted
  └─ visitAPI.recent()  → GET /api/vault/visits/recent  → CitVisitController.getRecentVisits
                                                           → CitVisitService.getRecent(key)
                                                             → CitVisitRepository.findTop10...
```

Both calls resolve via `Promise.all`; `institutionKey` is taken from the JWT-derived `VaultPrincipal`, never from the request body/query.

## What each stat is computed from

`VaultStatsService.getStats(institutionKey)` builds `VaultStatsResponse` from five queries (windows in **UTC**, `ZoneOffset.UTC`):

| Field | Query | Source table(s) |
|---|---|---|
| `visitsThisMonth` | `countByInstitutionAndDateRange(key, monthStart, monthEnd)` | `cit_visits` |
| `visitsPending` | `countPendingByInstitution(key)` (`status = 'PENDING'`) | `cit_visits` |
| `totalCashAddedCentsYtd` | `sumCashAddedCents(key, ytdStart)` = `SUM(added_count * denomination)` | `cit_cassette_counts` JOIN `cit_visits` |
| `totalRemovalAmountCentsYtd` | `sumRemovalAmountCents(key, ytdStart)` = `SUM(amount)` | `cit_removals` JOIN `cit_visits` |
| `totalImportsCompleted` | `countCompletedByInstitution(key)` | `cit_imports` |

Dashboard cards render only the first four; `totalImportsCompleted` is returned but unused by this view. Recent Visits = `findTop10ByInstitutionKeyOrderByVisitDateDesc` (or `findTop10ByOrderByVisitDateDesc` when key is null).

## DB tables — read/written by the dashboard

Reads only (all in `hiveiq_vault`, all scoped by `institution_key`):

| Table | Role on dashboard |
|---|---|
| `cit_visits` | Visit headers — counts, pending, recent list |
| `cit_cassette_counts` | Per-slot counts — source of Cash Added YTD |
| `cit_removals` | Non-cash removals — source of Removals YTD |
| `cit_imports` | Completed-import count (returned, not shown) |

The dashboard **writes nothing**. Writes to `cit_*` happen elsewhere (manual `POST /visits`, async CSV import). See [platform.data-architecture].

## Kafka (not on the dashboard path)

Vault **consumes** `hiveops.device.events` via `DeviceEventConsumer` (group `hiveops-vault-device-sync`, offset reset `earliest`) to maintain the `device_summary` mirror. Vault **produces no topics** (no `KafkaTemplate`/`StreamBridge`). The dashboard's stats/recent queries do **not** read `device_summary`, so the executor→Kafka→record-store mirror pattern is out of band for this feature — a stale device mirror does not affect dashboard numbers. See [platform.kafka].

## Key API endpoints (dashboard)

| Method | Path | Handler |
|---|---|---|
| GET | `/api/vault/stats` | `StatsController.getStats` |
| GET | `/api/vault/visits/recent` | `CitVisitController.getRecentVisits` |

Both require a JWT (`JwtAuthenticationFilter` → `VaultPrincipal`); no role annotation — authorization is institution-scoping only.

## Runtime / deploy

- Frontend config injected at container start (`frontend/entrypoint.sh`): `VITE_API_URL=https://api.bcos.cloud/vault`, `VITE_AUTH_URL=https://api.bcos.cloud/auth`.
- Spring CORS disabled in vault; NGINX is the CORS authority for `vault.bcos.cloud`.
- Prod DB via split env vars `DB_HOST`/`DB_PORT`/`DB_NAME=hiveiq_vault`/`DB_USERNAME`/`DB_PASSWORD` (not the `DB_URL` form); `ddl-auto: validate` + Flyway. Dev = H2 in-memory.
