---
module: dashboard.cash-stats
title: Cash Stats
tab: Internal
order: 20
audience: internal
---

Ops / support view for the **Cash Stats** page on `dashboard.bcos.cloud`. It shows the latest cassette bill counts per ATM across the fleet. The page is **read-only** — there are no admin actions on the page itself; all levers are on the backend that feeds it.

## Who serves this

- **Frontend:** `hiveops-dashboard` (SPA, port 5181) — `CashStatus.svelte`. No backend of its own.
- **Data API:** `hiveops-incident` backend (port 8080) — `JournalEventController`.
- **Data producer:** `hiveops-journal` (upload role) parses ATM STARTUP journals and writes `CASSETTE_INVENTORY` rows into the **incident** DB.

The page makes exactly two calls on load and every 60 s:
- `GET /api/journal-events/fleet/cassette-inventory` (incident)
- `GET /api/atms` (incident)

## Roles required

- Both endpoints fall under incident's `/api/**` rule → **any authenticated app user**: `USER, CUSTOMER, ADMIN, MSP_ADMIN, QDS_ADMIN, BCOS_ADMIN`. (`/api/journal-events` POST also allows `AGENT`, but that's ingest, not this page.)
- **Institution scoping is automatic** (`InstitutionContext.resolveScope`): `CUSTOMER`/`MSP_ADMIN` see only their granted institution keys; `BCOS_ADMIN/ADMIN/QDS_ADMIN/USER` see the whole fleet. If a customer says "I only see some ATMs," that's correct scoping, not a bug.
- **Admin backfill/sync is `MSP_ADMIN` or `BCOS_ADMIN` only** (journal `AdminController`).

## First things to check when it misbehaves

1. **Whole page errors / red banner** → incident backend down or unreachable. `curl` the two endpoints with a real JWT; expect `200` + JSON array.
2. **Everything shows "No Data" / "No inventory reported"** → no `CASSETTE_INVENTORY` rows in incident `journal_events`. Usual cause: journal parsing stalled or the cassette-inventory Kafka consumer isn't running. Check `hiveops-journal` (upload role) is up and that files are parsing (`parse_status=PARSED`).
3. **One ATM stuck stale / never updates** → its STARTUP journals aren't parsing, or the ATM's `atm_id` string doesn't resolve to a numeric id in incident's `atms` table (`resolveNumericId` returns null → row skipped, logged as a warning).
4. **Counts look wrong for a recycler** → the journal parser reads the `[DENOMINATION]` table (or falls back to `RCY USD` lines) out of the STARTUP record; a firmware format it doesn't match yields missing/zero slots.
5. **Cassettes show but say "(no data)"** → those are **config-only** synthetic slots. The ATM has a cassette *configuration* (agent-synced or a manually applied template) but has reported no actual bill count yet. Not an error.

## Admin actions (backend, not on the page)

- **Force a fleet re-derive of cash inventory** from the latest STARTUP journal of every ATM:
  `POST /api/journal/admin/sync-cassette-inventory` (journal service, `MSP_ADMIN`/`BCOS_ADMIN`). Runs `CassetteInventoryService.backfillAll()`.
- After any journal deploy, run the standard journal **reconcile** (`POST /api/journal/admin/reconcile`) so stranded files finish parsing — otherwise new inventory never lands.

## Common failure modes → fix

| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| All "No Data" | journal upload role down / no parsed STARTUPs | Bring up `hiveiq-journal`; verify files reach `PARSED`; then `sync-cassette-inventory` |
| One ATM never updates | `atm_id` not in incident `atms`, or no STARTUP parsed | Confirm ATM exists in incident; check journal parse logs for that ATM |
| Newer count not showing | journal skips write because existing `event_time` is newer | Expected guard; a genuinely newer STARTUP will overwrite |
| Customer sees subset of ATMs | institution scoping from JWT | Working as intended |
| 401/403 on load | expired/invalid JWT, or user has none of the allowed roles | Re-auth; check role |

## Data freshness

- The "last reported" time per ATM is the `event_time` of its newest cassette event — for journal-derived data this is the **STARTUP transaction time**, not "now." A stale time means the ATM hasn't produced a fresh STARTUP journal, not that the page is broken.
- Page auto-refreshes every 60 s and on the **↻** button; there's no server-side push.
