---
module: incident.journal-events
title: Journal Events
tab: Claude
order: 40
audience: dev
---

Act-without-guessing reference. Everything below is confirmed in code.

## Ownership
- **Reads + store owner:** `hiveops-incident` (port 8080, NGINX `/api/incident/`).
- **Ingest gateway:** `hiveops-agent-proxy` (port 8093/host 8015). Agents NEVER hit incident directly.
- Frontend: `hiveops-incident/frontend/src/components/JournalEvents/JournalEvents.svelte` (view label "Event Journal").

## Endpoints (METHOD path)
| Method + path | Service | Auth |
|---|---|---|
| GET `/api/journal-events/atm/{atmId}/paginated` | incident | JWT/browser role |
| GET `/api/journal-events/atm/{atmId}` | incident | JWT/browser role |
| GET `/api/journal-events/atm/{atmId}/recent?hoursBack=24` | incident | JWT/browser role |
| GET `/api/journal-events/atm/{atmId}/card-reader` | incident | JWT/browser role |
| GET `/api/journal-events/atm/{atmId}/cassette` | incident | JWT/browser role |
| GET `/api/journal-events/atm/{atmId}/cassette-inventory` | incident | JWT/browser role |
| GET `/api/journal-events/activity?hoursBack=24` | incident | JWT/browser role (institution-scoped) |
| GET `/api/journal-events/fleet/cassette-inventory` | incident | JWT/browser role (institution-scoped) |
| POST `/api/journal-events` | incident controller / agent-proxy native | AGENT or JWT/browser role |
| POST `/api/internal/journal-events` | incident `InternalJournalController` | ROLE_INTERNAL (`X-Internal-Secret`) |

- `{atmId}` = numeric `atms.id`, NOT the `AT000xxx` agent id.
- Every `/atm/{id}/...` has an identical `/device/{id}/...` alias.
- Paginated query params: `page`, `size`, `sort=eventTime,desc`, `hoursBack`, `eventTypes` (repeated key, e.g. `eventTypes=CARD_READER_FAIL&eventTypes=...`).

## Roles
- Read `/api/journal-events/**`: `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','QDS_ADMIN','BCOS_ADMIN')`. NO `@PreAuthorize`; NOT admin-only.
- Ingest internal: `hasRole('INTERNAL')` (header `X-Internal-Secret` = env `INTERNAL_SERVICE_SECRET`).
- Agent path `POST /api/journal-events` also allows `ROLE_AGENT`.

## Tables / DB
- Table `journal_events` in DB **`hiveiq_incident`**. Entity `JournalEvent`.
- Reads also touch `atms`; ingest touches `atms`, `event_type` (known-type lookup), `incidents` (auto-link).

## Kafka
- NONE for journal events. Ingest is synchronous HTTP. No `hiveops.journal.*` topic exists.

## Ingest chain (exact)
`agent → agent-proxy JournalEventController POST /api/journal-events → IncidentInternalService.createJournalEvent → incident POST /api/internal/journal-events → JournalEventService.createEvent`

## Gotchas
- Unknown `eventType` (not in `event_type` lookup) → NOT stored; recorded as event exception; returns HTTP 202. Known → 201.
- `createEvent` is `@Retryable` on `PessimisticLockingFailureException` (incident#212 deadlock guard).
- `eventTime` parsed as `LocalDateTime` (no timezone); unparseable/blank → `now()`.
- Auto-registers ATM on `agentAtmId` via `findOrCreateAtm`; also updates last heartbeat + may flip service state (`ATM_IN_SERVICE`/`ATM_OUT_OF_SERVICE`/`SUPERVISOR_ENTER`/`SUPERVISOR_EXIT`) + trigger auto-incident.
- Category filter in UI is an allowlist of exact type strings (8 groups); types outside the groups are hidden when any category is ticked.
- Per-ATM read endpoints filter by `atmId` only — they do NOT apply `InstitutionContext` scope (only `/activity` and `/fleet/cassette-inventory` do).

## Do NOT
- Do NOT POST agent journal events straight to incident — go through agent-proxy.
- Do NOT call `/api/internal/journal-events` without `X-Internal-Secret` — 401/403.
- Do NOT pass the `AT000xxx` agent id where `{atmId}` (numeric) is expected.
- Do NOT expect a Kafka topic for journal ingest — there is none.
- Do NOT assume this view is admin-gated — any authenticated user role can read it.
- Do NOT treat a 202 on ingest as "stored" — it means the type was unknown.
