---
module: reports.schedule
title: Scheduling & Distribution
tab: Claude
order: 40
audience: dev
---

Act-without-guessing reference. Owner service: **hiveops-reports**. Delivery: **hiveops-messaging**. All facts below confirmed in code.

## Ownership
- Templates, schedules, queries, runs, CSVs, completion event → **hiveops-reports** (DB `hiveiq_reports`, port `8088`, external `8011`, route `api.bcos.cloud/reports/`).
- In-app delivery of completed reports → **hiveops-messaging** (consumes the Kafka event; reports does NOT POST to messaging).

## Endpoints (METHOD path) — base `/api/reports`
| Method | Path | Purpose |
|---|---|---|
| POST | `/api/reports/templates` | create template (set `scheduleEnabled`,`scheduleCron`) |
| PUT | `/api/reports/templates/{id}` | update template / schedule |
| GET | `/api/reports/templates` | list caller's templates (pageable) |
| POST | `/api/reports/templates/{id}/run` | manual async run → 202 + run id |
| GET | `/api/reports/templates/{templateId}/queries` | list saved queries |
| POST | `/api/reports/templates/{templateId}/queries` | create saved query |
| POST | `/api/reports/templates/{templateId}/queries/{queryId}/set-scheduled` | mark query as scheduled |
| DELETE | `/api/reports/templates/{templateId}/queries/scheduled` | clear scheduled query |
| GET | `/api/reports/runs` | list runs (pageable) |
| GET | `/api/reports/runs/{id}/preview` | JSON preview |
| GET | `/api/reports/runs/{id}/download` | download CSV |
| POST | `/api/reports/runs/{id}/distribute` | re-emit completion event (409 if not COMPLETED) |
| GET | `/api/devices/atms` | ATM picker from `device_summary` mirror |

## Tables (`hiveiq_reports`)
- `report_templates` — `schedule_enabled`, `schedule_cron`, `next_run_at`, `schedule_query_id`, `institution_key`
- `report_queries` — saved date-range + ATM filters
- `report_runs` — status PENDING/RUNNING/COMPLETED/FAILED, `row_count`, `triggered_by`, `triggered_by_email`
- `report_distribution_log` — per-recipient SENT/FAILED
- `device_summary` — Kafka-fed mirror (no reconciler)

## Kafka topics
| Topic | Reports | Other side |
|---|---|---|
| `hiveops.reports.completed` | produces (key `institutionKey`\|`global`) | messaging consumer group `hiveops-messaging-reports` → DIRECT message per recipient |
| `hiveops.device.events` | consumes group `hiveops-reports-device-sync` (offset `earliest`) | feeds `device_summary` |

## Roles / auth
- `/api/reports/**`: `.anyRequest().authenticated()` — **JWT only, NO role check**. Scoped per-user by `userId`.
- `/api/devices/atms`: `@PreAuthorize("hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','BCOS_ADMIN')")` — the only role gate in the module.
- Scheduled runs use `InternalTokenGenerator`: 300s JWT, `role=MSP_ADMIN`+`institutionKey` when institution set, else `role=ADMIN`; signed `Keys.hmacShaKeyFor(JWT_SECRET.getBytes(UTF_8))`.

## Gotchas
- Scheduler = single `@Scheduled(fixedDelay=60_000)` sweep (`ReportSchedulerService`), NOT per-template cron. Resolution ≈1 min.
- Cron is **Spring 6-field, evaluated in UTC**. Frontend requires exactly 6 fields.
- Invalid/blank cron → `computeNextRun` returns null → `next_run_at=NULL` → template **never** fires (silent).
- Scheduled report needs a `schedule_query_id` for date range/ATMs.
- Distribution is fire-and-forget Kafka; `report_distribution_log.status=SENT` = published, NOT delivered.
- Reports fetches downstream live per run; downstream 4xx/5xx → run FAILED, not empty.
- CSVs on disk (`REPORT_UPLOAD_DIR`, prod `/data/hiveiq/reports/`), not in DB.
- `hiveops-ai` is branded **Adoons** externally.

## Do NOT
- Do NOT assume `MSP_ADMIN`/`BCOS_ADMIN` is required to schedule — it isn't (only `/api/devices/atms` is role-gated).
- Do NOT claim reports POSTs to messaging `/api/v1/messages` — delivery is via Kafka `hiveops.reports.completed`. (The reports/messaging CLAUDE.md mention of a direct POST is stale.)
- Do NOT expect a distributed lock — running reports on >1 replica duplicates scheduled runs.
- Do NOT treat SENT distribution-log rows as proof of delivery.
- Do NOT localize cron; there is no per-user timezone (UTC only).
- Do NOT look for scheduling data outside `hiveiq_reports` — reports owns it.
