---
module: reports.runs
title: Run History
tab: Claude
order: 40
audience: dev
---

Terse agent reference for **reports.runs**. Act only on what's below.

## Owner
- Service: **hiveops-reports** (owns run lifecycle, CSV, runs API). No other service owns runs.
- DB: `hiveiq_reports` · table `report_runs`.
- Ports: internal `8088`, external `8011`. NGINX `api.bcos.cloud/reports/` → `hiveiq-reports:8088/api/reports/`.

## Endpoints (`ReportRunController`, base `/api/reports/runs`)
| METHOD | Path | Guard / precondition | Returns |
|---|---|---|---|
| GET | `/api/reports/runs` | authenticated; owner-scoped, pageable (`?page=&size=`) | Page of runs (caller's only) |
| GET | `/api/reports/runs/{id}` | owner-scoped | run or 404 |
| GET | `/api/reports/runs/{id}/preview` | must be COMPLETED (409); result_data present (400) | ≤100 rows JSON |
| GET | `/api/reports/runs/{id}/download` | COMPLETED (409); csv_path set (400); file on disk (404) | CSV stream |
| DELETE | `/api/reports/runs/{id}` | owner-scoped | 204; deletes CSV + row |
| POST | `/api/reports/runs/{id}/distribute` | COMPLETED (409); body `{"recipientUserIds":[...]}` | 204 |
| POST | `/api/reports/templates/{id}/run` | trigger a NEW run (not a runs endpoint) | run id |

## Roles
- No `@PreAuthorize` / `hasAnyRole` on runs. `SecurityConfig` = `anyRequest().authenticated()`.
- Access boundary = **ownership**: `findByIdAndTriggeredByUserId`. Non-owner (even MSP_ADMIN/BCOS_ADMIN) → **404**, never 403.
- Scheduled runs owned by `template.createdByUserId`.

## Tables (`hiveiq_reports`)
- `report_runs` — primary. Key cols: `uuid`, `status`, `triggered_by`, `triggered_by_user_id`, `triggered_by_email`, `query_id`, `filter_overrides`(jsonb), `row_count`, `result_data`(jsonb), `csv_path`, `error_message`, `started_at`, `completed_at`, `created_at`.
- `report_templates` — FK parent (name, columns, outputFormat, distributionConfig).
- `report_distribution_log` — per-recipient delivery (channel=IN_APP, status SENT|FAILED).
- Status enum: `PENDING → RUNNING → COMPLETED | FAILED`. TriggerSource: `USER | SCHEDULED`.

## Kafka
| Direction | Topic | Class |
|---|---|---|
| Produced | `hiveops.reports.completed` | `ReportCompletedEventProducer` (const `ReportsKafkaConfig.TOPIC_REPORTS_COMPLETED`, 3 partitions, key=institutionKey\|"global") |
| Consumed (reports svc, not runs-specific) | `hiveops.device.events` | `DeviceEventConsumer` → `device_summary` |
- `hiveops-messaging` consumes `hiveops.reports.completed` and creates the in-app message.

## Execution facts
- `executeAsync` on `@Async("reportExecutor")` pool: core=2, max=5. Scheduler `@Scheduled(fixedDelay=60s)`.
- User run forwards caller's `Authorization` header downstream. Scheduled run mints 5-min ADMIN JWT via `InternalTokenGenerator` (HMAC `JWT_SECRET`).
- Preview: backend caps 100 rows; frontend renders only first 50.
- CSV path: `REPORT_UPLOAD_DIR` (default `/app/uploads/reports`; OpenMetal bind `/data/hiveiq/reports/`). Not in DB.
- FAILED `error_message` = caught exception `getMessage()`; downstream (incident/journal/fleet) failure is the usual cause.

## Gotchas
- Admin CANNOT view another user's run — 404, no override path.
- COMPLETED ≠ delivered: distribution is a separate Kafka hop; check `report_distribution_log`.
- CSV can 404 while row is COMPLETED (container recreated w/o bind mount).
- JSON-format template → no CSV → download 400.
- `hiveops-reports/CLAUDE.md` claims distribution POSTs to `messaging /api/v1/messages` — **stale**; real path is Kafka `hiveops.reports.completed`.
- DB conn uses split `DB_HOST/DB_PORT/DB_NAME`, not `DB_URL`.

## Do NOT
- Do NOT assume role checks — none exist on runs; ownership is the gate.
- Do NOT add an admin "view all runs" path expecting it to exist.
- Do NOT treat COMPLETED as "recipient received it."
- Do NOT expect re-run on a run; re-trigger via `POST /api/reports/templates/{id}/run`.
- Do NOT look for run data in another service — hiveops-reports owns it.
- Do NOT invent a `/api/v1/messages` distribution call; distribution is Kafka-only.
