---
module: reports.runs
title: Run History
tab: Internal
order: 20
audience: internal
---

Ops/support view of **Run History** — the list of report executions in hiveops-reports. Each row is one `report_runs` record. Runs are **strictly owner-scoped**: a user only ever sees runs they triggered (scheduled runs show up under the template creator). Use this when a report "won't finish," "won't download," or "never arrived."

## Access & roles

- No role gate on the runs endpoints. `SecurityConfig` ends with `anyRequest().authenticated()` — any valid JWT works; there is **no** `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")` on `/api/reports/runs/*`.
- **Ownership is the real ACL.** Every run lookup uses `findByIdAndTriggeredByUserId(id, userId)`. An MSP_ADMIN/BCOS_ADMIN **cannot** see, preview, download, delete, or distribute another user's run — they get a `404 Report run not found`, not a `403`. There is no admin override. If a customer says "I can't see the run my colleague made," that is by design.
- Runs are scoped by `triggered_by_user_id` (the numeric JWT `userId`), not by institution.

## First things to check when it misbehaves

**A run is stuck in RUNNING / never leaves PENDING**
- The executor pool is small: `@Async("reportExecutor")`, core=2 / max=5 (`AsyncConfig`). If several large reports run at once, later ones queue.
- The async job does a **live paginated HTTP fetch** to a downstream service (incident/journal/fleet). A slow/hanging downstream stalls the run. Check downstream health first.
- There is no visible per-run execution timeout in `ReportRunService.executeAsync`; a wedged downstream can hold a slot.

**A run shows FAILED**
- Hover the **⚠ Failed** marker in the UI — it shows `error_message`, which is literally the caught exception's `getMessage()`.
- Root cause is almost always downstream: the data-source fetcher (INCIDENTS/ATM_HEALTH/TRANSACTIONS/FLEET_TASKS/AGENT_VERSIONS/AGENT_MODULES) got a non-2xx from incident/journal/fleet, or the JWT it forwarded was rejected. Reports **fetches, never stores** fleet data — downstream outages surface as FAILED runs, not empty ones.
- For **scheduled** runs the token is a self-minted 5-min ADMIN JWT (`InternalTokenGenerator`, signed with `JWT_SECRET`). If `JWT_SECRET` drifts between reports and the downstream service, scheduled runs fail auth while user-triggered ones (which forward the caller's header) may still work — a classic asymmetry to watch for.

**Preview button does nothing / errors**
- Preview only appears on **COMPLETED** runs. Endpoint returns `409 CONFLICT` if not completed, `400` if there is no stored JSON result.
- Backend returns up to the first **100** rows (`resultData`); the frontend modal renders only the first **50** ("Showing first 50 of N rows"). This is display-only truncation, not data loss.

**Download fails**
- CSV only exists when the template's `outputFormat` is `CSV`. A JSON-format template has no CSV → `400 This report run has no CSV output`.
- CSVs live on disk under `REPORT_UPLOAD_DIR` (default `/app/uploads/reports`; bind-mounted `/data/hiveiq/reports/` in OpenMetal), **not** in the DB. If the container was recreated without the bind mount, files are gone → `409`/`404 CSV file not found on disk` even though the row is COMPLETED.
- Deleting a run also deletes its CSV file from disk (`ReportRunService.deleteForUser`).

**Report "completed" but recipient never got it**
- Distribution is decoupled from the run. Completion publishes a Kafka event; the in-app message is produced by hiveops-messaging downstream. A COMPLETED run with no delivery means the messaging side, not the run, failed.
- Distribution only fires if the template has `distributionConfig.recipientUserIds` (auto) or someone called the manual distribute endpoint. Check `report_distribution_log` for per-recipient `SENT` / `FAILED` rows.

## Admin-adjacent actions

- **Re-run**: there is no re-run on the run itself. A FAILED run is deleted from Run History and re-triggered from the **Templates** tab (`POST /api/reports/templates/{id}/run`).
- **Manual distribute** of a completed run: `POST /api/reports/runs/{id}/distribute` with `{"recipientUserIds":[...]}` — must be COMPLETED (else `409`).
- **Delete** removes the row and its CSV; irreversible.

## Where it lives

- Service: **hiveops-reports** (owns everything here). DB `hiveiq_reports`, table `report_runs`.
- Ports: internal `8088`, external `8011` (OpenMetal). NGINX: `api.bcos.cloud/reports/` → `hiveiq-reports:8088/api/reports/`.
- Logs: `docker compose logs hiveiq-reports` on the services VM (or Loki `localhost:3100`). Failed-run stack traces log as `Report run {id} failed`.
