Line assurance builds on the [coding printers](/docs/printers/): every job is recorded with what it was
printed for, the printed code can be verified, and the result is shown live and reported per shift.

## Batch traceability and lot recall

A `printer` node carries an optional traceability context (templates over the record): `order`, `batch`,
`lot`, `product` and `code` (the code the job prints), plus `mask`, a list of logical field names stored as
`***`. A `jobs_from` entry may carry the same keys itself.

```json
{ "id": "p", "type": "printer", "line": "${line}", "message": "${fmt}",
  "fields": { "BATCH": "${batch}", "SERIAL": "${serial}" },
  "order": "${order}", "batch": "${batch}", "lot": "${lot}", "product": "${sku}",
  "code": "${serial}", "mask": ["CUSTOMER"] }
```

Every row of the job log (`printer_jobs.db`) then holds order, batch, lot, product, line, printer, message,
field values (masked by the privacy rules and by `mask`), the code, timestamps, the outcome, what the driver
confirmed and the verification result. Old databases are migrated in place.

- `GET /api/traceability/jobs?batch=&lot=&order=&product=&line=&printer=&code=&status=&verify=&from=&to=&format=json|csv`
- `GET /api/traceability/lots` the lots (else batches) seen.
- `GET /api/traceability/recall?lot=LOT-9[&line=&from=&to=]&format=json|csv|html`: every code printed under
  the lot, on every printer, where and when, with the failed and unverified jobs. `html` is a printable page
  (print it to PDF from the browser). Dates are `YYYY-MM-DD` in `assurance.timezone` or RFC 3339.

MCP tools: `traceability_jobs`, `list_lots`, `lot_recall`. Editor: Operations, *Traceability*.

## Verification

Give a printer a `verify:` option:

```yaml
printers:
  - id: z1
line: L1
protocol: zebra
confirm: true            # ~HS label-count confirmation
verify: { method: printer }
  - id: mi1
line: L1
protocol: markem_imaje
verify: { method: scan, window_ms: 8000 }
```

`method: scan` needs the node's `code`. After the job a scanner or camera must report that code within
`window_ms`. `method: printer` accepts only the driver's own proof of print.

What each driver can confirm (`confirmation` in the job log and printer list):

| Driver | Confirmation |
|---|---|
| Zebra | `printed` with `confirm: true` (`~HS` shows no label left on two polls), else `sent` |
| Markem-Imaje, Domino, Hitachi, Zipher, EOTSUM | `acked`: every command acknowledged (data accepted, not proof of paper) |
| ASCII | `acked` with an `ack` regex, else `sent` |
| TSC, SATO | `sent`: the status query is only a precheck |

Scans reach the engine three ways: `POST /api/assurance/scan {code, printer?, line?, scanner?}`, a `verify`
flow node at the end of a flow that receives readings (webhook, MQTT, ingest), or a configured scanner that
streams one code per line over TCP or serial:

```yaml
assurance:
  scanners:
- { id: cam1, line: L1, transport: { kind: tcp, host: 10.0.0.9, port: 23 }, strip_prefix: "]d2" }
```

A scan equal to a waiting job's code verifies it. A different code marks the oldest waiting job on that
printer or line `unverified: mismatch`. A job with no scan in time becomes `unverified: missed`. A verdict is
final and alerts once. A line alerts on the first unverified job of a streak and then every
`alert_repeat_every` (default 10) until a job verifies.

**Reject output.** Each unverified job sends a record `{event: "print_unverified", reason, line, printer_id,
job_id, code, scanned_code, order, batch, lot, product, consecutive}` into the flow source `verify:{line}`
(or `verify:*`). Put an `http` node or a PLC write behind it to stop the line or divert the product. Without
such a source nothing is sent.

## Line board

`GET /api/assurance/board[?line=]` aggregates per line: each printer's state (up / fault / down, from jobs and
a status poll every `status_poll_secs`), fault text, current message / order / batch / lot / product, last
code and its verification, counts of the running shift (else the day), and per line the verification rate,
jobs per hour, waiting read-backs and the unverified streak.

- Editor: Operations, *Line board*.
- TV on the line: open `/board` (all lines) or `/board/L1`, sign in once. Dark, large type, refreshes every
  3 s (`?refresh=` seconds), keeps the last data on screen and says so when the connection drops.
- Apps: an `assurance` data source (views `board`, `jobs`, `recall`, `lots`, `report`) and the `line_board` component.

## Shift and day report

```yaml
assurance:
  timezone: Asia/Jakarta
  shifts:
- { name: A, start: "06:00", end: "14:00" }
- { name: N, start: "22:00", end: "06:00" }   # crosses midnight
```

`GET /api/assurance/report?day=2026-10-30[&shift=A][&line=L1]&format=json|csv|html` gives per line and
printer: jobs, failed, verified, unverified, verification rate, downtime and availability. Downtime is the
time a printer was unreachable (`down`) or reporting a fault (`fault`); time while the connector was stopped
is not counted. MCP tool `line_report`; editor: Operations, *Line shift report*.

## Configuration

`assurance:` keys: `enabled`, `timezone`, `shifts`, `status_poll_secs` (30, 0 = off), `sweep_ms`,
`alert_repeat_every`, `scan_grace_ms`, `scanners`. Routes need `viewer`, except `POST /api/assurance/scan`
(`operator`, audited `assurance.scan`).