---
module: claims.dashboard
title: Claims Dashboard
tab: Internal
order: 20
audience: internal
---

Ops/support view for the HiveIQ **Claims Dashboard** — the landing view of the `claims.bcos.cloud` SPA. It renders six stat cards plus a filtered, paginated, sortable claims table. All data comes from **hiveops-claims** (`hiveiq-claims`, port 8092) via two calls: `GET /api/claims/stats` and `GET /api/claims`.

## Who can see what

- **No role guards.** `SecurityConfig` ends in `anyRequest().authenticated()` — there is **no** `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` check anywhere in this service. Any authenticated user with a valid HiveIQ JWT can load the dashboard.
- **Isolation is by `institutionKey`, not role.** Both endpoints read `principal.institutionKey()` from the JWT and scope every query to it. A user only ever sees their own institution's claims. There is no "see all institutions" admin path in the dashboard (a null `institutionKey` would return everything, but real tokens always carry one).
- No admin-only buttons on this view. The only mutating action reachable from the dashboard is **+ File New Claim** (opens a separate form → `POST /api/claims`).

## First things to check when it misbehaves

1. **Blank table / "Load failed" toast** — `GET /api/claims` failed. Check the JWT is present and valid (401 = auth filter rejected it), then check `hiveiq-claims` is up (`docker compose ps`, `curl localhost:8092/actuator/health`).
2. **Stat cards missing entirely** — `GET /api/claims/stats` failed silently. The frontend swallows stats errors (`catch {}`), so the cards just don't render while the table may still load. Curl `/api/claims/stats` directly with a real token to see the error.
3. **"Wrong institution's claims" / empty for a user who should have data** — check the `institutionKey` claim in their JWT. Wrong or missing key = wrong scope. This is a token problem, not a claims-service problem.
4. **ATM dropdown empty in File New Claim** (adjacent to dashboard) — that list comes from the `device_summary` mirror table, kept in sync by the `hiveops.device.events` Kafka consumer. If devices didn't publish, the mirror is stale. Not a dashboard-table issue.
5. **Numbers look wrong** — see "Known gotchas" below before filing a bug; several are by-design or naming quirks.

## Common failure modes + fixes

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| Table empty, no error | Filters still applied (status pills / Claim # / Terminal ID) | Click **✕ Clear all** in the Filters sidebar |
| 401 on both calls | Expired/absent JWT | Re-login; confirm `VITE_AUTH_URL` reachable |
| 403 / CORS error in console | Origin not in `CORS_ALLOWED_ORIGINS` | Ensure `claims.bcos.cloud` (or `localhost:5179` in dev) is allowed |
| Cards render but table 500s | `hiveiq_claims` DB unreachable / schema drift | Check `DB_HOST/DB_PORT/DB_NAME` env; prod uses Flyway `validate` |
| Stale/short claim list for whole institution | DB connectivity, not mirror | Mirror only affects the ATM picker, not the claims table |

## Search & filter behavior (so you can reproduce a report)

- **Status pills** are multi-select; the frontend joins them CSV and sends `?status=OPEN,WAITING_CIT,…`. Values must match the enum exactly (`OPEN`, `WAITING_CIT`, `WAITING_SP`, `WAITING_INSTITUTION`, `PENDING_PAYMENT`, `RESOLVED_PAID`, `RESOLVED_OFFAGE_FOUND`, `RESOLVED_DENIED`) — an unknown value throws `IllegalArgumentException` (500).
- **Claim #** and **Terminal ID** are separate params (`?claimNumber=`, `?terminalId=`), each a case-insensitive substring `LIKE %…%` match. There is **no** single free-text `search` param despite older docs mentioning one.
- **Sorting** is server-side via Spring `sort=field,dir`. Default `createdAt,desc`. Sortable columns: `claimNumber`, `terminalId`, `lossStart`, `shortageAmount`, `status`, `submittedAt`, `updatedAt` (and default `createdAt`). Location, Type, and the two Vendor columns are **not** sortable.
- **Pagination** default page size is 25 (backend `@PageableDefault`); the frontend overrides with the user's saved page size.
- **Amber left stripe** on a row = `hasUpdate = true` on that claim (`Claim.getHasUpdate()`), surfacing a new vendor/party update.

## Known gotchas (verify before filing a "bug")

- **"Recovered YTD" is not year-scoped.** `depositRecoveredYtd` / `withdrawRecoveredYtd` sum `amountPaid` across **all** of the institution's claims with no year filter — the "YTD" label is a misnomer, the value is all-time recovered. (See uncertainties.)
- **"Awaiting Response" counts more than truly-waiting claims.** It counts active claims in `OPEN`, `WAITING_CIT`, `WAITING_SP`, or `WAITING_INSTITUTION` — including brand-new `OPEN` claims nobody has been asked to respond to yet.
- **Exposure = shortage, not remaining owed.** `depositExposure` / `withdrawExposure` sum the full `shortageAmount` of active (non-resolved) claims; they do not subtract amounts already paid.
- **Stats are computed in-memory.** `getStats` loads every claim for the institution and streams them in Java — fine at current volumes, but it is O(all claims) per dashboard load, not a SQL aggregate.
