---
module: reports.builder
title: Report Builder
tab: Architect
order: 30
audience: dev
---

How the Report Builder is actually wired. It is owned end-to-end by **hiveops-reports**, which stores definitions/runs and orchestrates async execution against *other* services' live APIs. See [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Components
- **Frontend** — `hiveops-reports/frontend` (`Reports.svelte`, module `reports.builder`, app `reports`). Axios client `reportsApi` base = `window.__APP_CONFIG__.apiUrl` (default `…/api/reports`).
- **Controllers** (`com.hiveops.reports.controller`) — `ReportTemplateController`, `ReportRunController`, `ReportQueryController`, `ReportSchemaController`, `DeviceSummaryController`, `UserController`.
- **Services** — `ReportTemplateService`, `ReportRunService` (executor), `ReportExportService` (CSV writer), `ReportSchedulerService`, `ReportDistributorService`, `ReportQueryService`.
- **Fetchers** (`fetcher/*`, one per data source, keyed by `supports()`), **security** (`JwtAuthenticationFilter`, `InternalTokenGenerator`), **kafka** (`DeviceEventConsumer`, `ReportCompletedEventProducer`).

## Run data flow (executor → fetch → CSV → distribute)
1. `POST /api/reports/templates/{id}/run` → `ReportRunService.triggerUserRun(...)` inserts a `report_runs` row (PENDING) carrying the caller's identity, then calls `executeAsync(runId, bearerToken)`.
2. `executeAsync` (`@Async("reportExecutor")`, pool core=2/max=5) sets status **RUNNING**, resolves the fetcher for `template.dataSource`, and calls `fetcher.fetch(effectiveFilters, bearerToken)`.
3. **Fetchers** — the run's data comes from different services depending on data source:

   | DataSource | Fetcher | Downstream (WebClient `@Qualifier`) | Path |
   |---|---|---|---|
   | INCIDENTS | `IncidentReportFetcher` | incident | `/api/incident-management` |
   | SOFTWARE_ERRORS | `SoftwareErrorReportFetcher` | incident | incident paginated, regex-grouped by error pattern |
   | AGENT_MODULES | `AgentModuleReportFetcher` | incident | `/api/fleet/modules` |
   | FLEET_TASKS | `FleetTaskReportFetcher` | fleet (`hiveiq-fleet:8097`) | `/api/fleet/tasks/paginated` |
   | TRANSACTIONS | `TransactionReportFetcher` | journal (+ incident `/api/atms`) | `/api/journal/transactions` |
   | ATM_HEALTH | `AtmHealthReportFetcher` | **local `device_summary`** | repo query, no HTTP |
   | AGENT_VERSIONS | `AgentVersionReportFetcher` | **local `device_summary`** | repo query, no HTTP |

   Downstream base URLs from `WebClientConfig`: incident `hiveops-incident-backend:8080`, journal `hiveops-journal:8087`, fleet `hiveiq-fleet:8097`, mgmt `hiveops-mgmt:8080`, messaging `hiveops-messaging:8086`. HTTP fetchers page through downstream results.
4. Rows are processed (group-by + SUM/AVG/MIN/MAX aggregations, column projection). For CSV output, `ReportExportService.writeCsv` writes `{uploadDir}/{uuid}.csv` and the path is saved to `report_runs.csv_path`; JSON output is retained for in-app preview.
5. Status → **COMPLETED** (or **FAILED** with the exception message). On success, `distributeIfConfigured` runs.

## Distribution (executor → Kafka → messaging)
- `ReportDistributorService.distribute(run, recipientUserIds, bearerToken)` builds a `ReportCompletedEvent` and calls `ReportCompletedEventProducer.publish(...)` → **Kafka `hiveops.reports.completed`** (3 partitions; key = `institutionKey` or `"global"`).
- **hiveops-messaging** consumes that topic and delivers the completed report to each recipient's in-app inbox.
- One `report_distribution_log` row is persisted per recipient (both on publish success and failure).

## Scheduling
- `ReportSchedulerService` — `@Scheduled(fixedDelay=60_000)` polls `templateRepository.findScheduledDue(now)`.
- For each due template it mints a service JWT via `InternalTokenGenerator.generateForInstitution(institutionKey)` — short-lived (300s), `role=MSP_ADMIN` + `institutionKey` claim (falls back to unscoped `role=ADMIN` when institutionKey is null), signed `Keys.hmacShaKeyFor(jwt.secret)` — then calls `triggerScheduledRun(template, serviceToken)`. Same execution path as user runs; `computeNextRun(cron)` sets the next fire time.

## Device mirror (Kafka → device_summary)
- `DeviceEventConsumer` — `@KafkaListener` on **`hiveops.device.events`**, group **`hiveops-reports-device-sync`** (offset reset `earliest`). Upserts `device_summary`; handles `DEVICE_DELETED`; null `institutionKey` defaults to `"BCOS"`. This is the sole source for ATM_HEALTH and AGENT_VERSIONS — no reconciler, so it drifts if the topic stalls. See the platform-wide device_summary mirror note in [platform.data-architecture].

## Persistence (DB `hiveiq_reports`, split `DB_*` env, Flyway V1–V10)
`report_templates` · `report_runs` · `report_queries` · `report_distribution_log` · `device_summary`. CSV artifacts live on disk (`REPORT_UPLOAD_DIR`), not in the DB.

## Auth boundary
All endpoints require an authenticated JWT (`anyRequest().authenticated()`); user runs forward the caller's `Authorization` header downstream, scheduled runs forward the self-minted service token. Only `/api/devices/atms` carries a role `@PreAuthorize`. See [platform.service-ownership].
