ADR-005 slice 2: operator alert, settle off-machine, glossary + guide #50

Merged
padreug merged 6 commits from feat/dispense-outcome-slice2 into main 2026-10-10 20:30:36 +00:00
2 changed files with 173 additions and 0 deletions
Showing only changes of commit 9eeb7f29b4 - Show all commits

docs: dispenser error glossary and operator guide

Served from the extension's static dir and linked from the dashboard
header. The glossary is keyed by raw code (78 42, 82 00, …) with the
class, what it means, and what to do inside the unit; the guide covers
bays/ops ownership, the authorize/capture lifecycle, the cash-out hold,
owed-cash resolution, and alerts.
Padreug 2026-10-10 22:27:05 +02:00

98
static/docs/errors.html Normal file
View file

@ -0,0 +1,98 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>bitSpire — Dispenser error glossary</title>
<style>
:root { color-scheme: light dark; }
body { font: 15px/1.5 system-ui, sans-serif; max-width: 52rem; margin: 2rem auto; padding: 0 1rem; }
h1 { font-size: 1.6rem; } h2 { font-size: 1.2rem; margin-top: 2.2rem; border-bottom: 1px solid #8884; padding-bottom: .3rem; }
code { font-family: ui-monospace, monospace; background: #8882; padding: .05em .35em; border-radius: 3px; }
.cls { font-weight: 600; } .terminal { color: #c62828; } .recoverable { color: #ef6c00; } .inventory { color: #2e7d32; }
dl { margin: .5rem 0 0; } dt { font-weight: 600; margin-top: .6rem; } dd { margin: .2rem 0 0 1.2rem; }
.entry { margin-top: 1.4rem; padding: .6rem .9rem; border-left: 3px solid #8884; }
.entry:target { border-left-color: #1976d2; background: #1976d20d; }
small { color: #777; }
</style>
</head>
<body>
<h1>Dispenser error glossary</h1>
<p>Every dispense failure a machine reports carries an <strong>error code</strong> (the family — which driver), a <strong>raw code</strong> (what the hardware said), and a <strong>class</strong> that decides what the machine did next:</p>
<dl>
<dt><span class="cls terminal">terminal</span></dt>
<dd>The transport path is compromised. The machine has <strong>taken cash-out out of service</strong> and will stay that way until you open it. Clear the path, then record a <em>Recount</em> (which also fixes the bay count) or press <em>Resume cash-out</em>. Re-initialising the dispenser does not move a stuck note, so a restart will not clear this.</dd>
<dt><span class="cls recoverable">recoverable</span></dt>
<dd>This bay or this note. The customer still sees a fault screen if they were short-changed, but the machine stays in service. Check the bay named in the report and the reject tray.</dd>
<dt><span class="cls inventory">inventory</span></dt>
<dd>Nothing was asked of the hardware — the request could not be met from the bays. Not a hardware fault. If this happens after payment, the customer is still owed (see the worklist).</dd>
</dl>
<p><strong>Whatever the class, a report that is not <code>dispense_confirmed</code> after a payment means a customer is owed money.</strong> The worklist shows it as <em>Cash owed</em> (nothing dispensed) or <em>Partial dispense</em> (some notes out). Nothing has been distributed; the funds are in the machine wallet. Resolve by dispensing the shortfall at the machine (the machine reports the remediation and the settlement completes) or by paying the customer by hand and recording it with <em>Settle off-machine</em>.</p>
<h2>Fujitsu F53 / F56 (error code <code>F56DispenseError</code>)</h2>
<p><small>Codes are the two bytes the BDU returns after a failed bill count. Source: Fujitsu Frontech F56-BDU Error Code List (K3KD03234–K3KD03236-0001, ed. E02) and field observations. An unknown code is treated as terminal until it has been decoded.</small></p>
<div class="entry" id="78-42">
<h3><code>78 42</code> — note stopped at the cassette exit <span class="cls terminal">terminal</span></h3>
<p>A note left the bay and stopped in the transport just past the cassette. The counters report <em>0 dispensed, 0 rejected</em> because the note completed neither path — so the bay count is also one high until you recount.</p>
<p><strong>What to do:</strong> open the unit, remove the note from the transport path, check the cassette is seated, then <em>Recount</em> the bay (this releases the cash-out hold and corrects the count). First seen: sintra, 2026-10-09 — a 20 EUR note, customer paid 40 EUR and received nothing.</p>
</div>
<div class="entry" id="82-00">
<h3><code>82 00</code> — bill length check failed (long) <span class="cls recoverable">recoverable</span></h3>
<p>The BDU measured a note longer than the accept window configured for that denomination and rejected it. Repeated on every pick from one bay, it is almost always the <em>configured window</em>, not the notes: a GTQ Tejo rejected 5 of 5 on every pick in 2026-09 because the window was ±5 mm where the identical-size USD note has ±10.</p>
<p><strong>What to do:</strong> empty the reject tray and look at the notes. Single notes → window too narrow; report it. Pairs → the separator is worn (offset double-pick) and the narrow window is doing its job.</p>
</div>
<div class="entry" id="83-00">
<h3><code>83 00</code> — bill length check failed (short) <span class="cls recoverable">recoverable</span></h3>
<p>As above, measured short. Torn or folded notes, or the wrong denomination loaded in the bay.</p>
</div>
<div class="entry" id="84-00">
<h3><code>84 00</code> — bill thickness check failed <span class="cls recoverable">recoverable</span></h3>
<p>Two notes stuck together, or a taped/damaged note. Check the reject tray.</p>
</div>
<div class="entry" id="85">
<h3><code>85 0n</code> — pick from another safe <span class="cls recoverable">recoverable</span></h3>
<p>A note arrived from a bay other than the one commanded (<code>n</code> = which). Usually a cassette not fully latched.</p>
</div>
<div class="entry" id="86-00">
<h3><code>86 00</code> — bill spacing error <span class="cls recoverable">recoverable</span></h3>
<p>Notes too close together on the transport. Often follows a worn feed roller.</p>
</div>
<div class="entry" id="b5">
<h3><code>B5 ..</code> — reject box overflow <span class="cls terminal">terminal</span></h3>
<p>The reject tray is full; nothing more can be rejected, so nothing more can be dispensed safely.</p>
<p><strong>What to do:</strong> empty the reject tray, count what is in it (those notes left a bay), <em>Recount</em>.</p>
</div>
<div class="entry" id="f56dispenseerror">
<h3>Unrecognised code <span class="cls terminal">terminal</span></h3>
<p>A code not yet in this table. The machine treats it as a jam — cash-out held — until someone decodes it. Please report the raw code with what you found inside the unit so it can be added.</p>
</div>
<div class="entry" id="dispensetimeout">
<h3><code>DispenseTimeout</code> — dispenser did not answer <span class="cls terminal">terminal</span></h3>
<p>No frame came back within the dispense timeout (serial timeout, port closed, framing error). The transport state is unknown; the bay counts are flagged unverified.</p>
</div>
<h2>Puloon LCDM (error code <code>PuloonDispenseError</code>)</h2>
<p>No decode table yet; every Puloon fault is treated as <span class="cls terminal">terminal</span>. The raw code is whatever the driver returned — report it.</p>
<h2>Software-side codes</h2>
<div class="entry" id="insufficientinventory">
<h3><code>InsufficientInventory</code> / <code>NoCassetteForDenomination</code> <span class="cls inventory">inventory</span></h3>
<p>The request could not be met from the bays as the machine believes them to be. After a payment this still leaves the customer owed; before one, the sale is simply refused. If the bays physically hold more than the machine thinks, <em>Recount</em>.</p>
</div>
<div class="entry" id="dispenseshort">
<h3><code>DispenseShort</code> <span class="cls inventory">inventory</span></h3>
<p>The dispenser returned fewer notes than requested and reported no error. The customer is owed the difference.</p>
</div>
<p><small>Reference: bitspire <code>docs/adr/005-cash-out-dispense-outcome.md</code>, Decisions 3–7.</small></p>
</body>
</html>

View file

@ -0,0 +1,75 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>spirekeeper — Operator guide</title>
<style>
:root { color-scheme: light dark; }
body { font: 15px/1.5 system-ui, sans-serif; max-width: 52rem; margin: 2rem auto; padding: 0 1rem; }
h1 { font-size: 1.6rem; } h2 { font-size: 1.2rem; margin-top: 2.2rem; border-bottom: 1px solid #8884; padding-bottom: .3rem; }
code { font-family: ui-monospace, monospace; background: #8882; padding: .05em .35em; border-radius: 3px; }
table { border-collapse: collapse; margin: .6rem 0; } td, th { border: 1px solid #8884; padding: .3rem .6rem; text-align: left; vertical-align: top; }
.warn { border-left: 3px solid #ef6c00; padding: .4rem .8rem; background: #ef6c000d; }
small { color: #777; }
</style>
</head>
<body>
<h1>spirekeeper — operator guide</h1>
<p>spirekeeper is the operator side of a bitSpire ATM: it pairs machines, publishes their fee and cassette configuration, receives every cash-out's outcome, and distributes each settlement. Everything between you and a machine travels over Nostr relays — there is no HTTP endpoint on the machine and none it needs on you.</p>
<h2>1. Setting up a machine</h2>
<ol>
<li><strong>Register</strong> it under <em>Machines</em> with a name, location and fiat currency, and pick the LNbits wallet that receives its payments.</li>
<li><strong>Pair</strong> it: <em>Pair</em> mints a one-shot <code>spire-seed</code> QR. Show it to the machine's camera (an unpaired machine boots into the pairing wizard). Pairing gives the machine its signing identity through the bunker; no key is ever typed into the machine.</li>
<li><strong>Fees</strong>: your per-machine cash-in / cash-out commission plus the platform fee are published to the machine automatically whenever either changes, and delivered again on every boot.</li>
</ol>
<h2>2. Bays and cassettes — who decides what</h2>
<p><strong>The machine owns its bay layout and its running counts.</strong> How many bays it has is hardware; which denomination sits in each and how many notes are there is something only the person at the machine knows. So:</p>
<table>
<tr><th>Fact</th><th>Where it comes from</th><th>How you change it</th></tr>
<tr><td>Number of bays</td><td>The machine's installation (<code>VITE_BITSPIRE_CASSETTES</code> in its <code>.env</code> on first boot, then its own database). spirekeeper adopts whatever the machine reports and <em>deletes</em> bays it stops reporting.</td><td>Re-provision the machine. There is no dashboard control for bay count.</td></tr>
<tr><td>Denomination per bay</td><td>You.</td><td><em>Set denomination</em> op.</td></tr>
<tr><td>Count per bay</td><td>The machine's running total: refills add, dispenses subtract.</td><td>You publish <em>operations</em>, never counts: <em>Refill +N</em>, <em>Empty</em>, <em>Recount</em>. A recount is the only absolute — it is what you do when you open the bay and count it.</td></tr>
</table>
<p class="warn">Never set a count from a form you loaded before a dispense happened — that is exactly why counts are not editable. If the number on screen is wrong, open the bay, count, and record a <em>Recount</em>.</p>
<p>Each operation carries an id the machine deduplicates on, and the machine echoes the ids it has applied back in its state report; an op shows as <em>acknowledged</em> only when the machine says so. The machine republishes its state on every change and on a heartbeat, so a dashboard that disagrees with a machine heals itself within minutes.</p>
<h2>3. What a cash-out looks like now</h2>
<p>Payment is the <em>authorization</em>; the machine's dispense report is the <em>capture</em>. A customer's payment lands as <code>awaiting_dispense</code> and <strong>nothing is distributed</strong> until the machine reports what physically came out:</p>
<table>
<tr><th>Machine reports</th><th>Settlement</th><th>What happens</th></tr>
<tr><td>Everything dispensed</td><td><code>processed</code></td><td>Distribution runs — platform fee, your splits, DCA.</td></tr>
<tr><td>Some notes out, value short</td><td><code>partial_pending</code></td><td>Held whole until you record how the shortfall was resolved.</td></tr>
<tr><td>Nothing out</td><td><code>cash_owed</code></td><td>Nothing moves. The customer is owed. You are alerted.</td></tr>
<tr><td>No report within 30 min</td><td><code>dispense_unreported</code></td><td>The machine never said. Check it.</td></tr>
</table>
<p>Those last three are the first thing on the <em>Worklist</em>. Every one means a human is owed money or a machine needs eyes.</p>
<h2>4. When the machine takes itself out of service</h2>
<p>A <em>terminal</em> dispenser fault — a jam, a motor or sensor error, a dispense that never answered — makes the machine refuse further cash-out and show the customer that it is temporarily unavailable. The machine's card in spirekeeper shows a red <strong>Cash-out is held</strong> banner with the code and the reason. It stays held until:</p>
<ul>
<li>you record a <em>Recount</em> on any bay (you opened the machine — that also fixes the count), or</li>
<li>you press <em>Resume cash-out</em> on the banner (you cleared the jam without touching a count).</li>
</ul>
<p>Restarting the machine does not clear it: re-initialising a dispenser does not move a stuck note. See the <a href="errors.html">error glossary</a> for what each code means and what you will find inside.</p>
<h2>5. Resolving owed cash</h2>
<p>Two ways, both audited, both close the machine's ledger as well as this one:</p>
<ul>
<li><strong>At the machine</strong> — dispense the shortfall through the machine's manual-dispense command against the original transaction. The machine reports the remediation and the settlement completes on its own.</li>
<li><strong>Off-machine</strong> — you paid the customer by hand. Open the settlement (worklist or table) → <em>Settle off-machine</em> → write how (<em>"handed 40 EUR to customer, 2026-10-09"</em>). The settlement distributes at the full amount and the machine marks its own row remediated.</li>
</ul>
<p>For a <em>partial</em>, <em>Record the resolution</em> opens the partial-dispense dialog pre-filled from the machine's own count of what came out: confirm it if you are writing the shortfall off (the settlement distributes the scaled amount), or use one of the two routes above if you made the customer whole.</p>
<h2>6. Alerts</h2>
<!-- # pragma: allowlist secret -->
<p>When a settlement lands in <code>cash_owed</code> or <code>partial_pending</code> you receive a Nostr direct message (NIP-17) addressed to the pubkey on your LNbits account — a note to self, readable in any client that speaks NIP-46 (the aiolabs webapp, Amber, nsec.app). You do not need your private key for this. To receive alerts on a different identity, set <em>Alerts pubkey</em> in the platform settings.</p>
<h2>7. Reconciling a machine</h2>
<p>On the machine, <code>atm-reconcile</code> re-derives each bay from its last recount plus refills minus dispenses and reports any gap. A bay that has never been recounted cannot be reconciled — its starting number is unrecorded. Recount every bay once after installation; after that, any gap is real.</p>
<p><small>Design record: bitspire <code>docs/adr/004-cassette-state-synchronization.md</code> and <code>docs/adr/005-cash-out-dispense-outcome.md</code>.</small></p>
</body>
</html>