--- 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','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. - **The state flip is DUAL-WRITE (devices#121).** Incident writes its own `atm_properties`, then mirrors to **hiveops-devices** via `POST /api/internal/atms/{agentAtmId}/service-state` (`DevicesInternalService`). Devices is what every status badge reads — nginx rewrites `/incident/api/atms*` to the devices backend — so **incident's own copy is NOT what the UI shows.** Before 2026-07-09 the mirror did not exist and no device ever displayed IN SUPERVISOR. Incident keeps its local write only because `resolveOpenSupervisorTimeout` depends on it. - The mirror is **best-effort** (a devices outage logs a warning, never fails ingest) and fires on `afterCommit` — `createEvent` is `@Retryable`, so a rolled-back attempt must not mirror. - `devices.service.url` defaults to `http://hiveiq-nginx:8096` (nginx internal listener), **never** a container name — devices runs blue/green and the active colour flips on cutover. - 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. - Do NOT read `hiveiq_incident.atm_properties` to answer "is this ATM in supervisor?" — the UI reads `hiveiq_devices.atm_properties`. The two can diverge; devices is what the customer sees.