---
module: recon.atm-cycle-history
title: ATM Cycle History
tab: Architect
order: 30
audience: dev
---

How the ATM Cycle History feature is built. Owned end-to-end by **hiveops-recon** — see [platform.service-ownership]. Recon does its own reconciliation math and only reaches out to hiveops-journal and hiveops-incident for raw inputs.

## Services involved

| Service | Role for this feature |
|---|---|
| **hiveops-recon** (8091, `/api/recon`) | Owns cycles, cassette config, variance math, institution scoping. Serves the screen. |
| **hiveops-journal** (`services.journal.url`, default `http://hiveops-journal:8087`) | Source of transactions: WITHDRAWAL/DEPOSIT/REVERSAL for sync, STARTUP for reboot snapshots, REPLENISHMENT for prefill. Read via `journalWebClient`. |
| **hiveops-incident** (`services.incident.url`, default `http://hiveiq-incident:8081`) | Source of cassette layout for **Sync from Device**. Read via `incidentWebClient`. |

## Frontend

- `frontend/src/components/AtmCycleHistory.svelte` — left panel = cycle list, right panel = `CycleDetailPanel.svelte`; header has the ⚙ `CassetteConfigPanel.svelte` and ↻ Sync Journal buttons. Data goes through `reconAPI` in `frontend/src/lib/api.ts` (base `window.__APP_CONFIG__.apiUrl` → `/api/recon`).
- Cycle list from `getCycles`; detail from the selected row (`getCurrentCycle` seeds the open one). `CycleDetailPanel` lazily loads opening/closing cassette entries via `getReplenishment(id)` and reboot snapshots via `getStartupSnapshots(cycleId)`. Header format badge comes from `getAtmSummary().journalFormat`.

## Data flow

**Browse / open a cycle (read path)**
```
Svelte → GET /api/recon/atm/{atmId}/cycles        → reconciliation_cycles (institution-scoped)
       → GET /api/recon/cycles/{id}               → reconciliation_cycles row
       → GET /api/recon/replenishments/{id}       → replenishment_sessions + replenishment_cassette_entries
```

**↻ Sync Journal (recalc open cycle)**
```
Svelte → POST /api/recon/atm/{atmId}/sync  (admin)
recon CycleService → JournalFetchService → journalWebClient GET /api/journal/transactions
       → recompute totals on the open reconciliation_cycles row → save
```

**Daily Reboot Snapshots (live, not stored)**
```
GET /api/recon/cycles/{id}/startup-snapshots
recon → JournalFetchService.fetchStartupSnapshots(atmId, cycleStart..cycleEnd)
      → journal GET /api/journal/transactions (STARTUP)
      → AtmCassetteService.buildStartupCassetteEntries(...) → response (nothing persisted)
```

**Cassette Configuration → Sync from Device**
```
POST /api/recon/atm/{atmId}/cassettes/sync  (admin)
recon AtmCassetteService.syncFromIncident → IncidentFetchService.fetchCassetteConfig
      → incident GET /api/atms  (string atmId → numeric id map)
      → incident GET /api/atms/{id}/properties
      → upsert atm_cassette_configs
```

## Kafka — consumer-only

Recon **publishes nothing** for this feature; it reacts to upstream events. See [platform.kafka].

| Topic | Consumer | Group | Effect |
|---|---|---|---|
| `hiveops.device.events` | `DeviceEventConsumer` | `hiveops-recon-device-sync` | Maintains local `device_summary` mirror (drives institution scope; no reconciler — goes stale if upstream stops). |
| `journal.replenishments` | `ReplenishmentEventConsumer` | `recon-replenishment-group` | Ingests replenishment events that seed/close cycles. |
| `hiveops.journal.transactions.recorded` | `JournalTransactionConsumer` | `recon-transaction-group` | Reacts to newly recorded journal transactions. |

## DB tables (this feature)

`hiveiq_recon` (prod builds JDBC from `DB_HOST`/`DB_PORT`/`DB_NAME`, default `hiveiq_recon`; dev = H2). See [platform.data-architecture].

| Table | Read/Write here |
|---|---|
| `reconciliation_cycles` | R/W — cycle list, detail, variance, open/close |
| `replenishment_sessions` | R — opening/closing session for a cycle |
| `replenishment_cassette_entries` | R — per-cassette opening/closing detail |
| `atm_cassette_configs` | R/W — cassette layout (Sync from Device / manual save) |
| `atm_cash_positions` | R — holds `journalFormat` (parse driver, header badge) |
| `device_summary` | R — institution scoping (Kafka-fed mirror) |

## Notes for changes

- Reads are gated only by institution scope, not `@PreAuthorize`; all mutating cycle/cassette/sync endpoints require `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`.
- Startup snapshots are computed on demand — no snapshot table; changing the journal STARTUP format changes what appears with no migration.
- Cassette Sync from Device couples recon to incident's numeric ATM id map — a rename of incident's `/api/atms` contract breaks it.

Cross-links: [platform.data-architecture], [platform.kafka], [platform.service-ownership], [technical.recon].
