---
module: reports.templates
title: Report Templates
tab: Internal
order: 20
audience: internal
---

> **Internal — ops/support/admin.** Owner service: **hiveops-reports** (port `8088` internal / `8011` OpenMetal external). Route `api.bcos.cloud/reports/` → `hiveiq-reports:8088/api/reports/`. Frontend SPA `report.bcos.cloud` (port `5182`).

## What this module is
A **report template** is a saved report definition: data source + columns + filters + optional groupBy/aggregations + optional schedule + distribution config. Templates live in `report_templates` (Postgres `hiveiq_reports`). Templates don't hold data — running one fetches live from incident/journal/fleet at run time.

## Who can do what (confirmed in code)
- **No admin gate.** `SecurityConfig` only requires `.anyRequest().authenticated()` — any valid HiveIQ JWT can create/list/run templates. There is **no** `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` on template endpoints (unlike most other modules). The only method-level `@PreAuthorize` in the service is on `/api/devices/atms` (device picker), not on templates.
- **Per-user ownership, not per-institution.** Templates are scoped by `created_by_user_id`. A user sees/edits/deletes **only their own** templates (`findByIdAndCreatedByUserId`). "My Reports" = your templates; "Shared Reports" = templates others marked shared.
- **Role in JWT** is used only to mint downstream tokens; missing role defaults to `ROLE_USER`.

## First things to check when it misbehaves
1. **"My template disappeared / 404 on edit."** Ownership is by `created_by_user_id`. If the user logged in as a different account (different `userId`), the template won't be visible. Confirm the `sub` claim in their JWT matches `report_templates.created_by_user_id`.
2. **Stale template after edit.** `getForUser` is `@Cacheable("reportTemplates", key="#id+':'+#userId")`. Update/delete/share evict that key — but a same-object read from another key won't. If a template looks stale, bounce the pod or confirm the write actually committed.
3. **Scheduled template fires immediately after create/enable.** On create with `scheduleEnabled=true` (or on enabling it), `next_run_at` is set to `Instant.now()`, so the 60s scheduler picks it up on the next tick regardless of cron. Expected behavior — not a bug.
4. **Schedule never fires.** Scheduler is `@Scheduled(fixedDelay=60s)` `findScheduledDue(now)`. Check: (a) `schedule_enabled=true`, (b) `schedule_cron` is a valid **6-field Spring cron** (bad cron → `computeNextRun` returns null and it stops re-arming), (c) `next_run_at` is not null/in the future. Cron is evaluated in **UTC**.
5. **Scheduled run has no data / auth 401 downstream.** Scheduled runs use a **self-minted ADMIN JWT** (`InternalTokenGenerator.generateForInstitution(template.institutionKey)`, signed with `JWT_SECRET`). If `JWT_SECRET` drifted between reports and incident/journal/fleet, scheduled runs fail while user-triggered runs (which forward the caller's bearer) still work.
6. **Data source rejected on create.** `create` validates via `SchemaRegistry.find(dataSource)` → `400 Unknown data source`. Valid sources: `INCIDENTS`, `ATM_HEALTH`, `TRANSACTIONS`, `FLEET_TASKS`, `AGENT_VERSIONS`, `AGENT_MODULES`, `SOFTWARE_ERRORS`.
7. **Empty/failed run, not empty template.** Templates only define the query. A run failing = downstream (incident/journal/fleet) outage or a bad filter — check `report_runs.error_message`, not the template.

## Common failure modes + fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| 404 editing/deleting a template | Ownership mismatch (different `userId`) | Verify JWT `sub` vs `created_by_user_id` |
| Shared template can't be edited by recipient | Only owner can edit; recipients can only **run** shared templates | By design — owner edits |
| Scheduled report never delivered | Bad cron / `next_run_at` null / secret drift | Fix cron, re-enable, confirm `JWT_SECRET` parity |
| Template runs but rows empty | Downstream service outage or filter too narrow | Check `report_runs.error_message` + downstream health |
| Stale field after PUT | Template cache | Confirm commit; evictions are keyed `id:userId` |

## Direct DB (via ProxyJump to 10.10.10.188, DB `hiveiq_reports`)
```sql
SELECT id, name, data_source, schedule_enabled, schedule_cron, next_run_at,
       created_by_user_id, institution_key, shared
FROM report_templates ORDER BY updated_at DESC LIMIT 20;
```
Related tables: `report_queries` (saved date-range+ATM sets, one may be the scheduled query via `report_templates.schedule_query_id`), `report_runs`, `report_distribution_log`.

## Known cross-institution caveat
Shared templates are listed by `findBySharedTrueAndCreatedByUserIdNot(userId)` — **no institution filter**. A template shared by a user in one institution is visible to users in **all** institutions. Treat "why can institution B see institution A's report" reports as this, not a data-mirror issue. See [reports.templates] Architect tab.
