---
module: claims.dashboard
title: Claims Dashboard
tab: Architect
order: 30
audience: dev
---

How the **Claims Dashboard** is built. It is a thin read view over one service — **hiveops-claims** owns all the data it shows. See [platform.service-ownership].

## Components involved

| Component | Role for this view |
|-----------|-------------------|
| `hiveiq-claims-frontend` (Svelte SPA, port 5179, `claims.bcos.cloud`) | `Dashboard.svelte` renders stat cards + table; polls every 60s |
| `hiveiq-claims` backend (Spring Boot, port 8092) | `ClaimController` → `ClaimService` → JPA repositories |
| PostgreSQL `hiveiq_claims` | Source of truth for claims + payments |
| Kafka `hiveops.device.events` | Feeds the `device_summary` mirror used by the *File New Claim* ATM picker (not the dashboard table itself) |

The dashboard does **not** call any other microservice. Unlike the `device_summary` mirror consumers, its own table/stat data is fully local to hiveops-claims.

## Data flow

```
Dashboard.svelte  ──GET /api/claims/stats──▶ ClaimController.getStats
                  └─GET /api/claims?…──────▶ ClaimController.listClaims
                         │
                         ▼
                   ClaimService (@Transactional readOnly)
                         │
                         ▼
                   ClaimRepository (JPA) ──▶ hiveiq_claims.claims (+ claim_payments)
```

- `JwtAuthenticationFilter` populates a `ClaimsPrincipal` record (`userId, email, role, institutionKey`) from the Bearer token. Both controller methods read `principal.institutionKey()` and pass it down as the scope — data isolation is enforced in the service/repo layer, not by role annotations.
- **List** (`GET /api/claims`): `ClaimService.listClaims` picks one of two `@Query` repository methods:
  - no status filter → `findFilteredNoStatus(institutionKey, claimNumber, terminalId, pageable)`
  - status(es) present → parses the CSV into `List<Claim.ClaimStatus>` and calls `findFilteredWithStatuses(...)`.
  Both apply case-insensitive `LIKE %…%` on `claimNumber` and `terminalId`, return a `Page<Claim>`, and map to `ClaimSummaryResponse` (includes CIT/SP vendor uuid+name, `hasUpdate`, `submittedAt`, `updatedAt`).
- **Stats** (`GET /api/claims/stats`): `ClaimService.getStats` loads `findByInstitutionKey(institutionKey)` (all claims) and computes in Java: active count (any non-`RESOLVED_*`), deposit/withdraw exposure (sum of `shortageAmount` over active), awaiting (`OPEN`/`WAITING_*`), deposit/withdraw recovered (sum of `claim_payments.amount_paid`), and `recoveryPercent = totalRecovered / totalShortage * 100` (`HALF_UP`). Returned as `ClaimStatsResponse`.

## DB tables

| Table | Read | Written | Notes |
|-------|:----:|:-------:|-------|
| `claims` | ✓ | — | Dashboard is read-only; writes happen via *File New Claim* (`POST /api/claims`) |
| `claim_payments` | ✓ | — | `amount_paid` drives the Recovered cards; `amount_owed` set at claim creation |
| `vendors` | ✓ | — | Joined for CIT/SP vendor names in the table |
| `device_summary` | ✓ | — | Only for the ATM picker (`GET /api/claims/atms`), not the dashboard grid |

No table is written by the dashboard itself. See [platform.data-architecture].

## Kafka

The dashboard read path produces and consumes **nothing**. The service as a whole consumes `hiveops.device.events` (`DeviceEventConsumer`, group `hiveops-claims-device-sync`) to keep `device_summary` in sync — this is the standard mirror pattern shared by incident/fleet/reports/recon/claims and only surfaces in the claim-creation ATM dropdown, not this view. There is no executor→Kafka→record-store write pattern here; claim/stat state is committed synchronously to Postgres. See [platform.kafka].

## Key API endpoints (this view)

| METHOD | Path | Returns |
|--------|------|---------|
| GET | `/api/claims/stats` | `ClaimStatsResponse` |
| GET | `/api/claims?status=&claimNumber=&terminalId=&page=&size=&sort=` | `Page<ClaimSummaryResponse>` |

Externally routed as `api.bcos.cloud/claims/…` → `hiveiq-claims:8092/api/claims/…`.

## Refresh model

Client-side polling only: `Dashboard.svelte` runs a 1s ticker; every 60s (when Auto-refresh is on) it fires `loadStats()` + `loadClaims()` in parallel. Toggle state persists in `localStorage['claims_autoRefresh']`. No websockets, no SSE, no server push.
