@@ -53,7 +53,11 @@ interface TypeChipCount {
5353 tone ?: "accent" | "err" ;
5454}
5555
56- export type MessageTone = "ok" | "warn" | "err" | "pend" ;
56+ // `held` = manually deferred via POST /defer/:id. Distinct from `pend`
57+ // (received, worker hasn't processed yet — animated, terminal-bound) so the
58+ // chip doesn't show a misleading "processing" spinner on a status that won't
59+ // move on its own. Operator must POST /mark-for-retry/:id to resume.
60+ export type MessageTone = "ok" | "warn" | "err" | "pend" | "held" ;
5761
5862// The 4 hard-error statuses the design's "errors" pseudo-chip aggregates.
5963// Matches CLAUDE.md's IncomingHL7v2Message status vocabulary.
@@ -216,8 +220,9 @@ export function statusToTone(p: ParsedIncomingMessage): MessageTone {
216220 case "conversion_error" :
217221 case "sending_error" :
218222 return "err" ;
219- case "received" :
220223 case "deferred" :
224+ return "held" ;
225+ case "received" :
221226 return "pend" ;
222227 default :
223228 return assertNever ( p ) ;
@@ -235,6 +240,7 @@ export function statusStringToTone(s: string | undefined): MessageTone {
235240 if ( ! s ) { return "pend" ; }
236241 if ( s === "processed" || s === "warning" ) { return "ok" ; }
237242 if ( s === "code_mapping_error" ) { return "warn" ; }
243+ if ( s === "deferred" ) { return "held" ; }
238244 if ( s . endsWith ( "_error" ) ) { return "err" ; }
239245 return "pend" ;
240246}
@@ -243,6 +249,10 @@ function toneDot(tone: MessageTone): string {
243249 if ( tone === "ok" ) { return "dot ok" ; }
244250 if ( tone === "warn" ) { return "dot warn" ; }
245251 if ( tone === "err" ) { return "dot err" ; }
252+ // `held` (manually deferred): static neutral dot — terminal state, no
253+ // background work in progress. Distinct from `pend` so the row doesn't
254+ // pulse like an in-flight message.
255+ if ( tone === "held" ) { return "dot" ; }
246256 // `pend` (received / queued-for-worker): animated pulse so the user
247257 // sees the message is actively being worked on, not stuck. The
248258 // in-process worker polls every ~5s so the flip to "processed" is
@@ -254,6 +264,8 @@ function toneChip(tone: MessageTone): string {
254264 if ( tone === "ok" ) { return `<span class="chip chip-ok">processed</span>` ; }
255265 if ( tone === "warn" ) { return `<span class="chip chip-warn">needs mapping</span>` ; }
256266 if ( tone === "err" ) { return `<span class="chip chip-err">error</span>` ; }
267+ // Held = neutral static chip. Terminal — no spinner, no auto-refresh.
268+ if ( tone === "held" ) { return `<span class="chip">deferred</span>` ; }
257269 // Active-state chip: inline spinner + "processing" text so the user
258270 // sees activity rather than a static "pending" label. Per-row polling
259271 // (see renderRowStatusCell) swaps this into its settled form when the
0 commit comments