---
module: incident.journal-events
title: Journal Events
tab: Architect
order: 30
audience: dev
---

How the **Event Journal** feature is built. This module is a thin read view over the journal-event store that **hiveops-incident** owns; the write side is agent ingestion routed through **hiveops-agent-proxy**. See [platform.service-ownership].

## Services involved

| Service | Role for this feature |
|---|---|
| incident-frontend (`JournalEvents.svelte`) | Vite+Svelte SPA view; per-ATM paginated table + filters |
| **hiveops-incident** | Owns reads and the `journal_events` store; `JournalEventController` (browser) + `InternalJournalController` (ingest) |
| **hiveops-agent-proxy** | Sole agent entry point; native `JournalEventController` forwards ingest to incident's internal API |
| hiveops-agent | Runs on the ATM; POSTs journal events |

Agents never call incident directly — see [platform.service-ownership].

## Data flow — read path

1. SPA selects an ATM (numeric `atms.id`) and calls `GET /api/journal-events/atm/{atmId}/paginated` with `page/size/sort/hoursBack/eventTypes[]` (axios base `/api`; `eventTypes` serialized as repeated keys, not indexed).
2. `JournalEventController.getEventsPaginated` → `JournalEventService.getEventsPaginated` builds a JPA `Specification` (`JournalEventSpecification.withFilters`) filtering on `atm.id`, `eventType IN (...)`, and `eventTime >= now-hoursBack`.
3. `journalEventRepository.findAll(spec, pageable)` → mapped to `JournalEventDTO`, returned as a Spring `Page` (`{content, page:{totalElements,totalPages,number}}`).

Aggregate reads (`/activity`, `/fleet/cassette-inventory`) additionally resolve institution scope via `InstitutionContext.resolveScope()` (null = unrestricted for `USER`/`ADMIN`/`QDS_ADMIN`/`BCOS_ADMIN`; scoped list for `CUSTOMER`/`MSP_ADMIN`). The per-ATM read endpoints do **not** apply that scope (they filter by `atmId` only).

## Data flow — write/ingest path

```
agent  --POST /api/journal-events-->  agent-proxy (ROLE_AGENT)
       JournalEventController.createEvent
          -> IncidentInternalService.createJournalEvent(body, contentType)
             --POST /api/internal/journal-events (X-Internal-Secret)-->
          incident InternalJournalController (ROLE_INTERNAL)
             -> JournalEventService.createEvent(request)
```

`JournalEventService.createEvent`:
- Resolves/auto-registers the ATM via `atmService.findOrCreateAtm(agentAtmId, deviceType)` and updates last heartbeat.
- If `eventType` is not in the `event_type` lookup (`eventTypeService.isKnown`) → recorded via `eventExceptionService.record(...)`, returns `null` → HTTP `202`. Known types → saved, HTTP `201`.
- Side effects: flips ATM service state for `ATM_IN_SERVICE`/`ATM_OUT_OF_SERVICE`/`SUPERVISOR_ENTER`/`SUPERVISOR_EXIT`; runs `incidentAutoCreationService.processRecovery` + `processEvent` (may attach a newly created incident to the event).
- Wrapped in `@Retryable(PessimisticLockingFailureException, 3x, backoff)` — deadlock guard for the morning-reboot burst (incident#212).

**No Kafka in the ingest path** for this feature — journal ingestion is synchronous HTTP. This is *not* the executor→Kafka→record-store pattern; there is no `hiveops.journal.*` topic. (Downstream, incident *does* emit `hiveops.incidents.created/updated/resolved` when auto-incident creation fires, but that is the incidents domain, not the journal event itself.) See [platform.kafka].

## DB tables

- **`journal_events`** (DB `hiveiq_incident`, entity `JournalEvent`) — written on ingest, read by this view. Columns: `atm_id` (FK→`atms`), `incident_id` (nullable FK→`incidents`), `eventType`, `eventDetails` (TEXT), card-reader (`cardReaderSlot`,`cardReaderStatus`), cassette (`cassetteType`,`cassetteFillLevel`,`cassetteBillCount`,`cassetteCurrency`,`cassettePosition`,`cassetteDenomination`), `eventTime`, `eventSource`. Indexes: `idx_journal_events_atm_time (atm_id, eventTime DESC)`, `idx_journal_events_event_type`.
- **`atms`** — read for ATM name/location in the DTO; written (auto-register + heartbeat) on ingest.
- **`event_type`** — lookup that decides known vs. exception.
- **`incidents`** — linked when auto-incident creation fires.

See [platform.data-architecture].

## Key API endpoints

| Method + path | Service | Auth |
|---|---|---|
| `GET /api/journal-events/atm/{id}/paginated` | incident | JWT/browser role |
| `GET /api/journal-events/atm/{id}` `.../recent` `.../card-reader` `.../cassette` `.../cassette-inventory` | incident | JWT/browser role |
| `GET /api/journal-events/activity` · `/fleet/cassette-inventory` | incident | JWT/browser role (institution-scoped) |
| `POST /api/journal-events` | incident (`JournalEventController`) / agent-proxy native | AGENT or JWT/browser role |
| `POST /api/internal/journal-events` | incident (`InternalJournalController`) | `ROLE_INTERNAL` via `X-Internal-Secret` |

All `/atm/{id}/...` paths have a Phase-2 `/device/{id}/...` alias mapping to the same handler.
