---
module: recon.recon-rules
title: Recon Rules
tab: Claude
order: 40
audience: dev
---

> Act-without-guessing reference. All facts confirmed in `hiveops-recon` source (2026-07-01).

## Owner
- Service: **hiveops-recon** (owns frontend + backend). Port **8091**, NGINX `/api/recon/`, prod external **8013**.
- Controller: `com.hiveops.recon.controller.ReconRulesController` (`@RequestMapping("/api/recon/admin")`).
- Service class: `ReconRulesService`. Model: `ReconRules` / `ReconRules.ReplenishmentFormatConfig`.

## Endpoints (all `@PreAuthorize hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`)
| Method | Path | Content | Purpose |
|---|---|---|---|
| GET | `/api/recon/admin/rules/formats` | JSON | Format list for the table |
| GET | `/api/recon/admin/rules` | text/plain | Raw `recon-rules.yml` text |
| PUT | `/api/recon/admin/rules` | text/plain body | Save YAML (validates, writes, reloads) |
| POST | `/api/recon/admin/reload-rules` | — | Reload file→cache, returns `{status, formats[]}` |

## Frontend → endpoint map (`frontend/src/lib/api.ts` `reconAPI`)
- `getReconFormats()` → GET `/admin/rules/formats`
- `getReconRules()` → GET `/admin/rules`
- `saveReconRules(yaml)` → PUT `/admin/rules`
- `reloadReconRules()` → POST `/admin/reload-rules`
- Components: `ReconRulesView.svelte` (table), `ReconRulesPanel.svelte` (YAML editor).

## Storage
- **No DB table.** Rules live in a YAML file: classpath `src/main/resources/recon-rules.yml`, overridable by env `RECON_RULES_PATH` → property `recon.rules.path` (default empty).
- Cached in memory: `ReconRulesService.cachedRules` (volatile), loaded `@PostConstruct`.
- Format DTO fields: `name`, `detectionSentinel`, `slotStrategy` (LETTER|DENOMINATION|POSITION), `deduplication`, `preReplenishmentBlockEnd`, `description`.

## Kafka / topics
- **None for this feature.** (Recon overall is consumer-only: `hiveops.device.events`, `journal.replenishments`, `hiveops.journal.transactions.recorded` — unrelated to rules editing.)

## Detection semantics
- Order-dependent; first `detectionSentinel` `String.contains()` match wins (`ReconRulesService.detectFormat`).
- Null-sentinel format = fallback (used only if no other matches).

## Gotchas
- GET endpoints are admin-gated too → non-admin sees **403 on page load**, not just on save.
- PUT save **throws IllegalStateException → 400** if `RECON_RULES_PATH` unset ("recon.rules.path not configured"). This env var is **not set in the repo deployment** — save is disabled in prod until an external writable file is configured.
- Invalid YAML on PUT → **500** (parse into `ReconRules.class` fails, caught as IOException).
- Save auto-reloads the cache; manual out-of-band file edits require POST `/reload-rules` or restart.
- Rule edits affect **future** parsing only; past cycles need recon backfill/reparse tooling.
- `recon-rules.yml` (format parsing) ≠ `recon_device_rules` table / `ReconDeviceRuleController` (device auto-selection). Different feature.

## Do NOT
- Do NOT assume a `recon_rules` DB table exists — it does not; it's a YAML file.
- Do NOT tell a user to PUT `/admin/rules` in prod without first confirming `RECON_RULES_PATH` points to a writable mounted file.
- Do NOT invent extra endpoints — only the four above exist on `ReconRulesController`.
- Do NOT claim any role other than MSP_ADMIN/BCOS_ADMIN can read this screen.
- Do NOT confuse this with `recon_device_rules` / device selection.
- Do NOT expect Kafka events from editing rules.
