---
module: recon.recon-rules
title: Recon Rules
tab: Internal
order: 20
audience: internal
---

> **Internal · ops/support/admin.** Grounded in `hiveops-recon` source (2026-07-01). Owner service: **hiveops-recon** (port 8091, NGINX `/api/recon/`, prod external 8013). The feature edits a **YAML file**, not a DB table.

## What this screen actually is

The "Recon Rules" view (`ReconRulesView.svelte`) lists the **replenishment formats** defined in `recon-rules.yml`. Each row = one format's parsing config (detection sentinel, slot strategy, deduplication, pre-block end). Clicking a row opens `ReconRulesPanel.svelte`, which shows/edits the **raw YAML** of the *entire* rules file. Detection drives how recon parses `STANDARD REPLENISHMENT` journal blocks into cassette totals.

## Roles required — everything here is admin-only

Every endpoint on `ReconRulesController` is annotated `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")` — **including the read GETs**. A user without one of those roles gets **403 on page load** (the table's `GET /admin/rules/formats` is gated), not just on save.

| Action in UI | Call | Role |
|---|---|---|
| Load format table | GET `/api/recon/admin/rules/formats` | MSP_ADMIN / BCOS_ADMIN |
| View raw YAML (panel) | GET `/api/recon/admin/rules` | MSP_ADMIN / BCOS_ADMIN |
| Save edited YAML | PUT `/api/recon/admin/rules` | MSP_ADMIN / BCOS_ADMIN |
| Reload from disk | POST `/api/recon/admin/reload-rules` | MSP_ADMIN / BCOS_ADMIN |

## First things to check when it misbehaves

1. **"No rules configured" / empty table** — `getRules()` returned null or zero formats. Means `recon-rules.yml` failed to load at startup (`ReconRulesService.loadRules()` fell back to `new ReconRules()`). Check recon logs for `Failed to load recon-rules.yml`. Confirm the classpath resource (or `RECON_RULES_PATH` file) is valid YAML.
2. **403 loading the page** — the user isn't MSP_ADMIN/BCOS_ADMIN. This is expected, not a bug.
3. **Save fails with "recon.rules.path not configured"** — see failure modes below; this is the #1 gotcha.
4. **A machine's replenishment reads wrong** — check which format matched. Detection is **order-dependent, first sentinel `contains()` match wins**; a format with no sentinel is the **fallback**. If a specific format's sentinel string is wrong/missing, records fall through to fallback and parse incorrectly.
5. **Edited a rule but nothing changed** — edits only affect **future** parsing after the cache reloads; already-recorded cycles are unaffected. Use the recon backfill/reparse admin tooling to re-run past data.

## Common failure modes + fixes

| Symptom | Cause | Fix |
|---|---|---|
| **Save returns 400 "recon.rules.path not configured — cannot save rules to classpath resource"** | `RECON_RULES_PATH` env var is unset (default is empty). `saveRulesContent()` refuses to write to the packaged classpath file and throws `IllegalStateException`. **This env var is not set anywhere in the repo deployment** — so Save is effectively disabled in prod until an ops person mounts a writable file and sets `RECON_RULES_PATH` to it. | Set `RECON_RULES_PATH` to a writable external path (bind-mounted), seed it with the current `recon-rules.yml`, restart recon. Then Edit/Save works and persists across restarts. |
| **Save returns 500** | Submitted YAML is invalid — `saveRulesContent()` validates by parsing into `ReconRules.class` before writing; a parse failure surfaces as a 500 with the Jackson error message. | Fix the YAML syntax/structure; the panel shows the error banner text. |
| **Save "succeeds" but reverts after redeploy** | Rules were written to the classpath copy inside the running container (only possible if someone edited in place) — a new image overwrites it. | Use an external `RECON_RULES_PATH` file so edits live outside the image. |
| **Change saved but parsing still old** | In-memory `cachedRules` not refreshed. Save auto-reloads; a manual out-of-band file edit does not. | Click **Reload** (POST `/reload-rules`) or restart recon. |
| **Wrong format keeps matching** | Earlier format's `detectionSentinel` is a substring that matches too broadly. | Reorder/narrow sentinels — remember first match wins, fallback (null sentinel) is last resort. |

## Don't confuse these two "rules"

- **recon-rules.yml (THIS screen)** — replenishment *format parsing* config. A YAML file. No DB table.
- **`recon_device_rules` table / `ReconDeviceRuleController`** — a *different* feature that auto-selects which devices are enrolled in recon. Unrelated to this view.
