---
module: reports.builder
title: Report Builder
tab: Claude
order: 40
audience: dev
---

Terse act-without-guessing reference. **Owner service: `hiveops-reports`** (`hiveiq-reports`, internal `:8088`, external `:8011`). Route `api.bcos.cloud/reports/` → `hiveiq-reports:8088/api/reports/`. DB `hiveiq_reports` (split `DB_HOST/DB_PORT/DB_NAME/DB_USERNAME/DB_PASSWORD`, NOT `DB_URL`).

## Endpoints (all require JWT Bearer)
| Method | Path | Purpose |
|---|---|---|
| GET | `/api/reports/schema` | all data-source schemas |
| GET | `/api/reports/schema/{dataSource}` | one schema |
| GET | `/api/reports/templates` | caller's templates |
| POST | `/api/reports/templates` | create |
| GET | `/api/reports/templates/{id}` | get one |
| PUT | `/api/reports/templates/{id}` | update |
| DELETE | `/api/reports/templates/{id}` | delete |
| POST | `/api/reports/templates/{id}/run` | async run → returns run |
| GET | `/api/reports/templates/shared` | templates shared with caller |
| POST | `/api/reports/templates/{id}/share` | share |
| DELETE | `/api/reports/templates/{id}/share` | unshare |
| GET | `/api/reports/templates/{templateId}/queries` | list queries |
| POST | `/api/reports/templates/{templateId}/queries` | create query |
| PUT | `/api/reports/templates/{templateId}/queries/{queryId}` | update query |
| DELETE | `/api/reports/templates/{templateId}/queries/{queryId}` | delete query |
| POST | `/api/reports/templates/{templateId}/queries/{queryId}/set-scheduled` | mark scheduled query |
| DELETE | `/api/reports/templates/{templateId}/queries/scheduled` | clear scheduled query |
| GET | `/api/reports/runs` | list runs (pageable) |
| GET | `/api/reports/runs/{id}` | one run |
| GET | `/api/reports/runs/{id}/preview` | JSON preview |
| GET | `/api/reports/runs/{id}/download` | CSV download |
| DELETE | `/api/reports/runs/{id}` | delete run |
| POST | `/api/reports/runs/{id}/distribute` | manual distribute |
| GET | `/api/devices/atms` | devices from local mirror |
| GET | `/api/users/me` | current principal |

## Tables (DB `hiveiq_reports`)
- `report_templates` · `report_runs` (`status`, `csv_path`, `uuid`) · `report_queries` · `report_distribution_log` · `device_summary`

## Kafka
- **Produce** → `hiveops.reports.completed` (3 partitions; key=`institutionKey`|`"global"`) — consumed by `hiveops-messaging` for in-app delivery.
- **Consume** ← `hiveops.device.events` — group `hiveops-reports-device-sync`, offset `earliest`; upserts `device_summary`.

## Data sources → downstream (fetch happens at run time)
- INCIDENTS, SOFTWARE_ERRORS, AGENT_MODULES → **hiveops-incident** (`hiveops-incident-backend:8080`)
- FLEET_TASKS → **hiveops-fleet** (`hiveiq-fleet:8097`, `/api/fleet/tasks/paginated`)
- TRANSACTIONS → **hiveops-journal** (`/api/journal/transactions`) + incident (`/api/atms`)
- ATM_HEALTH, AGENT_VERSIONS → **local `device_summary`** (no HTTP)

## Roles / auth
- `SecurityConfig` = `anyRequest().authenticated()`. **No `MSP_ADMIN`/`BCOS_ADMIN` gate on report actions** — any valid JWT works.
- Only `/api/devices/atms` = `@PreAuthorize("hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','BCOS_ADMIN')")`.
- User runs forward caller's `Authorization` header downstream. Scheduled runs mint their own JWT (`InternalTokenGenerator`, 300s, `role=MSP_ADMIN`+`institutionKey`, or `ADMIN` if null), signed with `jwt.secret`.

## Gotchas
- Templates/runs scoped by `principal.userId()` — per-user, not per-institution; no cross-user admin listing endpoint.
- Async pool tiny (core=2/max=5); runs queue under load.
- CSV on disk at `REPORT_UPLOAD_DIR/{uuid}.csv` (default `/app/uploads/reports`, OpenMetal `/data/hiveiq/reports/`) — NOT in DB. Missing volume = COMPLETED run, broken download.
- `device_summary` has no reconciler; stale if `hiveops.device.events` stalls → stale ATM_HEALTH/AGENT_VERSIONS.
- Downstream outage surfaces as FAILED run (reason on the run), not empty rows.
- 7 data sources in code (`ReportTemplate.DataSource`) incl. `SOFTWARE_ERRORS` (seeded via V6); customer overview lists 6.

## Do NOT
- Do NOT assume `MSP_ADMIN`/`BCOS_ADMIN` is required — reports only need an authenticated JWT.
- Do NOT set `DB_URL` for this service; use the split `DB_*` vars.
- Do NOT look for report/fleet data owned here beyond templates/runs/queries/dist-log/device_summary — the actual fleet data is fetched live from incident/journal/fleet.
- Do NOT expect CSVs in Postgres — they are files on the upload volume.
- Do NOT write to `device_summary` directly — it is Kafka-fed; publish/replay `hiveops.device.events` instead.
- Do NOT revoke/rotate `JWT_SECRET` on this service alone — scheduled runs will fail downstream auth.
