---
module: dashboard.cash-stats
title: Cash Stats
tab: Claude
order: 40
audience: dev
---

Terse agent reference for `dashboard.cash-stats`. Read view over cassette bill counts.

## Ownership
- **Frontend:** `hiveops-dashboard` → `frontend/src/components/Dashboard/CashStatus.svelte` (no backend).
- **Read API owner:** `hiveops-incident` — `JournalEventController` / `JournalEventService`.
- **Data producer:** `hiveops-journal` (upload role) — `CassetteInventoryService` writes into incident's DB.
- Dashboard SPA has **no server**; all data is incident's.

## Endpoints (METHOD path → service)
| Method | Path | Service | Auth |
|--------|------|---------|------|
| GET | `/api/journal-events/fleet/cassette-inventory` | incident | any app role, institution-scoped |
| GET | `/api/atms` | incident | any app role |
| POST | `/api/journal/admin/sync-cassette-inventory` | journal | `MSP_ADMIN`/`BCOS_ADMIN` |
| POST | `/api/journal/admin/reconcile` | journal | `MSP_ADMIN`/`BCOS_ADMIN` (run post-deploy) |

- Frontend calls these via configured API base (`window.__APP_CONFIG__.apiUrl`); paths above are the incident controller mappings.

## Tables
- `journal_events` — incident DB (`hiveiq_incident`). Filter `event_type='CASSETTE_INVENTORY'`. **Written by journal**, read by incident API.
- `atms` — incident DB. Journal resolves `atm_id` (string) → `id` (numeric) here.
- `journal_transactions` — journal DB (`hiveiq_journal`). Source STARTUP `rawLines`.

## Topics
- `journal.file-parsed` — consumed by `CassetteInventoryService` (groupId `journal-cassette-inventory`, `auto.offset.reset=latest`).
- No topic is consumed/produced by the dashboard itself.

## Roles
- Read endpoints: `USER, CUSTOMER, ADMIN, MSP_ADMIN, QDS_ADMIN, BCOS_ADMIN` (incident `/api/**`).
- Scope: `InstitutionContext.resolveScope()` → `CUSTOMER`/`MSP_ADMIN` scoped to JWT institution keys; others unrestricted.
- Journal admin: `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`.

## Gotchas
- `event_source='CASSETTE_CONFIG'` = **synthetic config-only** slot (no real count) — UI shows "(no data)". Real data = `event_source='JOURNAL'` (from journal) or agent ingest.
- Journal write **deletes then inserts** all `CASSETTE_INVENTORY` rows for an ATM per update; has a freshness guard (skips if existing `event_time` newer).
- Read query uses `MAX(id)` per `(atm_id, COALESCE(cassette_position,''))` as recency proxy — assumes monotonic ids.
- Currency hardcoded `USD`; type from STARTUP is `RECYCLER`/`MIX`/`RETRACT`. MIX/RETRACT have null denomination.
- Low threshold = **200 bills** (frontend `LOW_BILL_THRESHOLD`), empty = 0 bills, evaluated only on slots with denomination > 0.
- `CassetteInventoryService` runs **only in journal upload role** (`journal.role=upload`).
- ATMs whose `atm_id` isn't in incident `atms` get skipped (logged warn), so they show as "No Data."

## Do NOT
- Do NOT add a cassette/cash endpoint to `hiveops-dashboard` — it has no backend; use incident.
- Do NOT write `CASSETTE_INVENTORY` rows from anywhere but journal's `CassetteInventoryService` (single writer owns the delete+insert).
- Do NOT expect Kafka backlog replay — consumer is `auto.offset.reset=latest`; use `sync-cassette-inventory` to backfill.
- Do NOT assume "last reported" = now; it's the STARTUP transaction time.
- Do NOT bypass institution scoping by querying the table directly for a customer context.
- Do NOT invent per-position history — read path keeps only the latest per slot (getCassetteInventory keeps latest + previous for single-ATM only).
