Fork of satmachineadmin's v2-bitspire line into its own repo. Renames
both identifiers so this extension is fully independent of the original
satmachineadmin install (which remains in service):
- extension id satmachineadmin -> spirekeeper
(router prefix, static path/static_url_for, module symbols, task
names, templates dir, config/manifest paths)
- database name satoshimachine -> spirekeeper
(Database(ext_spirekeeper), all schema-qualified table refs)
Also resets versioning to 0.1.0, sets the display name + manifest to
spirekeeper/aiolabs, and fixes the placeholder pyproject description.
Historical aiolabs/satmachineadmin#N issue references in comments are
left pointing at the original repo where those issues live.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Surfaces the new directional fee fields in the admin dashboard so
operators + the LNbits super can configure cash-in and cash-out fees
independently:
Templates (`templates/satmachineadmin/index.html`):
- Platform fee banner now shows both directional super fractions
side-by-side ("cash-in X% · cash-out Y% of each transaction's
principal"). Wording updated to "principal" not "commission" since
the math is now principal-based.
- Super-fee edit dialog: replaces the single q-input with two
(super_cash_in_fee_fraction + super_cash_out_fee_fraction); each
capped at 0.15 via max attr (visual hint; server enforces).
- Add-machine + edit-machine dialogs both gain operator_cash_in_fee_
fraction + operator_cash_out_fee_fraction inputs with the same 0.15
cap hint. Hint text mentions the "sits on top of platform fee, total
capped at 15% per direction" semantics so operators understand the
layering.
JS (`static/js/index.js`):
- superFeeDialog.data shape switches to the new directional fields.
- openSuperFeeDialog / submitSuperFee load + POST the new shape.
- _emptyMachineForm / _cleanMachineForm pass through operator
directional fields (Number-coerced, default 0).
- openEditMachineDialog / submitEditMachine include the operator fee
fields in the form data + PUT body.
- New computed `superAnyFee` drives the banner styling (sum of both
directional fractions — non-zero → blue active banner; zero → muted
grey "free instance" banner).
All Quasar UMD components use explicit close tags per the UMD-mode
parsing rule.
Migration carry-over verified in dev container: pre-m009
super_fee_fraction=0.33 backfilled to super_cash_in=0.33 +
super_cash_out=0.33 on migrate-up. Note this puts existing dev
instances above the new 15% cap; operators will see the cap
validation error on their next super-config save and must adjust to
≤0.15 per direction. Production aiolabs/server-deploy will land at
0.03 on both directions (well under cap).
164/164 tests green. Layer 1 (#38) complete; Layer 2 (#39) wire-
format publisher is the next milestone.
Refs: aiolabs/satmachineadmin#37 (parent), #38 (closes Layer 1).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UI flips to position-keyed per the v1.1 redesign:
- Column order: Bay | Denomination | Count | ATM-reported | Updated
(position first, since it's the row identity)
- Position becomes read-only: rendered as "Bay N" label
- Denomination becomes an editable q-input (with the fiat code as a
suffix on the input)
- Count remains editable
- ATM-reported column now shows "<denom> <fiat> · ×<count>" combining
state_denomination + state_count for at-a-glance reconciliation
(still v1: only the bootstrap snapshot; v2 reverse-channel makes
this live)
- Confirm-modal preview list: header is "Bay N", side shows the
denomination + count being sent
JS:
- cassettesTable.columns reordered to put position first
- markCassetteDirty pivots on position (the immutable identity) and
compares denomination + count against pristine
- submitCassettePublish builds {positions: {<pos>: {denomination,
count}}} payload instead of {denominations: ...}
No "lock icon" on denomination — the previous instinct to add one was
based on the m007 misinterpretation. v1.1 design correctly makes
denomination operator-editable.
Tests still red until commit f.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The operator-facing surface for #29 v1. Two changes:
1. Sub-tab inside the existing machine-detail modal
(`templates/satmachineadmin/index.html`):
- q-tabs strip with Settlements + Cassettes inside the machine detail
q-card-section, wrapping the existing Settlements content in a
q-tab-panel name="settlements" and adding a new q-tab-panel
name="cassettes"
- Cassettes panel renders the cassette_configs rows from
GET /api/v1/dca/machines/{id}/cassettes:
- One row per denomination (read-only label)
- Editable q-input for count + position (the only operator-
editable fields per the locked design)
- ATM-reported count column (read-only, shows the v2 reverse-
channel state_count when populated; v1 only populates on
bootstrap)
- Last-updated timestamp
- Dirty rows highlighted bg-yellow-1
- "Revert" + "Publish to ATM" buttons in the header; both disabled
until at least one row is dirty
- "Waiting for ATM bootstrap" banner when cassette_configs is empty
(the bootstrap consumer hasn't received the ATM's state event yet)
2. Confirm-on-publish modal (per coord-log `07:50Z`):
- Yellow warning banner: "This publish will overwrite the ATM's
currently-tracked counts. If the ATM has dispensed cash since your
last refill, those decrements will be lost. Publish only after a
physical refill (a known total), not to 'tweak' counts mid-day. v2
reconciliation will replace this modal with reconciled state display."
- Per-denomination preview list of what's being sent
- Cancel + Publish-to-ATM buttons
Vue 3 + Quasar UMD compliance per workspace CLAUDE.md: explicit-close
tags (no self-closing), v-model.number on the numeric inputs,
@update:model-value to trigger dirty-tracking, JSON-clone for the
pristine snapshot.
JS additions in `static/js/index.js`:
- machineDetail.cassetteEdits / .cassettesPristine / .cassettesDirty /
.cassettesLoading / .cassettesPublishing / .cassettesError state
- cassettesTable.columns (no pagination — small fleets)
- cassettePublishConfirm.show
- loadMachineCassettes — fetches + sets pristine snapshot
- markCassetteDirty — compares to pristine, toggles _dirty + the
overall cassettesDirty flag
- revertCassetteEdits — deep-clone pristine back into edits
- openCassettePublishConfirm — opens the modal
- submitCassettePublish — builds PublishCassettesPayload from edits,
POSTs to /machines/{id}/cassettes/publish, refreshes from the
response, closes modal on success, surfaces 400/503 errors in
the inline banner
reloadMachineDetail now also calls loadMachineCassettes so the
Cassettes tab is pre-populated and tab-switching is flicker-free.
viewMachine resets the cassette state (edits, pristine, dirty, error,
activeTab) on each open.
This is the final commit in the #29 v1 chain. PR #30 is ready for
review once the build + manual smoke pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Direction" column to the per-machine settlements table that
renders a coloured Quasar chip with a directional icon:
- cash-out (green-8, south_west arrow) — customer paid ATM invoice in
BTC, operator wallet received sats. Principal distributes to LPs.
- cash-in (orange-8, north_east arrow) — customer redeemed LNURL-
withdraw at the ATM, operator wallet sent sats. No DCA leg;
liquidity stays in the operator wallet.
Tooltips spell out the meaning so the operator doesn't have to
remember the canonical mapping (cash_out ↔ inbound, cash_in ↔ outbound)
on sight. Defaults to cash_out for any unknown / legacy row, which is
safe because pre-S6 rows are all cash_out and the rejection-record
path also stamps cash_out.
Closes the UI half of aiolabs/satmachineadmin#22 (S8 cash-in path);
the structural half (direction discriminator + DCA skip) shipped in
eca6e96. End-to-end test against a live LNURL-withdraw redemption is
the remaining S8 acceptance gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each machine handles exactly one currency today (operator-set on
`dca_machines.fiat_code`). The deposit's currency is fully determined
by the machine it's recorded against, so it shouldn't be operator-
chooseable in the first place.
Surfaced during 2026-05-16 E2E testing: Jordan had a "15 USD" deposit
recorded against an EUR Sintra (operator typo in the freeform currency
input). The balance summary is currency-blind (`SUM(amount)` over
mixed currencies), so on the next cash-out the system distributed
15 EUR worth of sats on the strength of that 15 USD row. Worked out
by chance; could have over-paid by ~10% if the actual EUR/USD rate
had been further off.
Fix:
- `CreateDepositData` / `UpdateDepositData` no longer carry a
`currency` field. Any client-submitted value is silently dropped
at Pydantic validation, before reaching the handler.
- `api_create_deposit` resolves the machine's `fiat_code` and
passes it to `create_deposit(..., currency=...)` as a required
keyword arg. The deposit row's `currency` column always matches
the machine going forward.
- UI: the freeform `<q-input label="Currency">` becomes a read-only
`<q-chip>` slot on the amount field, sourced from the new
`depositMachineFiatCode` computed (resolves via the selected
client's machine).
- `m005_lock_deposit_currency_to_machine_fiat_code` migration
backfills existing rows: every `dca_deposits.currency` gets
rewritten to match its joined `dca_machines.fiat_code`. Greg's
stray `15 USD` row becomes `15 EUR` (the right answer at today's
invariant).
Multi-currency-per-machine support is explicitly out of scope here;
when hardware ships that reads multiple denominations across
currencies, the relevant changes are documented in issue #26's
"Future" section (dca_machines.fiat_codes set, currency-aware
balance summary, etc.). The current fix is "lock the input side";
that future work is "unlock it but constrained to the machine's
declared set".
3 new unit tests (`tests/test_deposit_currency.py`) lock in the
model-contract guarantees. Total suite 89 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator no longer chooses the LP's wallet / DCA mode / autoforward —
those belong to the LP, written via satmachineclient. The Add LP /
Edit LP dialogs reduce to (machine, user_id, optional username,
status). The clients table loses the wallet / mode / autoforward
columns and gains an "Onboarded" column showing whether the LP has a
`dca_lp` row yet (server-side LEFT JOIN; `DcaClient.lp_onboarded`).
Deposit creation gate (the structural enforcement of "must onboard
first"):
- Picker annotates each LP option with "— pending onboarding" and
disables un-onboarded LP rows.
- Selecting an un-onboarded LP shows an inline deep-orange banner
explaining the LP needs to open satmachineclient once.
- The Record button is `:disable`d in that state. The backend
refuses with HTTP 422 anyway (see previous commit) — UI is just
the first line of feedback.
Backend wiring:
- `DcaClient` model gains `lp_onboarded: bool = False`, populated
at SELECT time via a shared `_CLIENT_SELECT` / `_CLIENT_FROM`
fragment that LEFT JOINs `dca_lp` on `user_id`. All four list/
single-row read paths use it: by-id, by-(machine,user), by-machine,
by-operator, by-user. No extra round-trip per row.
- CSV export drops the removed columns; adds `lp_onboarded`.
All 86 unit tests still pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`net` is financial-accounting ambiguous (net of what?). In the
bitSpire/DCA context this column is specifically the principal the
operator distributes to LPs (gross − commission), not a generic net
amount. Renaming locally before any bitSpire firmware locks the
wire-level name; lamassu-next#44 should adopt the same name.
Scope:
- migrations.py: m003 ALTER TABLE … RENAME COLUMN, idempotent probe
pattern matching m002. Also updates the m001 canonical schema so
fresh installs land on the new column directly.
- models.py: `CreateDcaSettlementData.principal_sats` /
`DcaSettlement.principal_sats`. Field-doc comment updated.
- bitspire.py: both happy path and fallback path return
`principal_sats=…`. Reads `extra.get("principal_sats")` from the
bitSpire payload (lamassu-next#44 should follow this rename).
- crud.py: INSERT column list + `apply_partial_dispense(
new_principal_sats=…)` keyword.
- distribution.py: every `settlement.net_sats` → `settlement.
principal_sats`; partial-dispense memo + helper signatures updated;
the leg-order docblock at the top reads "principal_sats".
- tasks.py: landed-settlement log line.
- static/js/index.js: settlements-table column `principal_sats` with
label "Principal (→ LPs)".
- templates/satmachineadmin/index.html: q-td key + binding.
All 86 unit tests still pass. No backwards-compat shim — v2-bitspire
isn't released; the rename is a clean break.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When LNbits' nostr-transport stamps `nostr_sender_pubkey` and
`nostr_event_id` onto Payment.extra (post aiolabs/lnbits PR #4), the
listener now cross-checks the signer against the resolved machine's
`machine_npub` before any distribution. Mismatch / absence / unparseable
pubkey → settlement is recorded with `status='rejected'` and the
reason in `error_message`, distribution is skipped.
Wire shape:
bitspire.SettlementAttributionError + assert_nostr_attribution()
Raises on absence, mismatch, or unparseable pubkey on either side.
Normalises both `machine.machine_npub` (operator UI accepts hex
or `npub1...`) and the stamped sender through
`lnbits.utils.nostr.normalize_public_key` so the comparison is
canonical-hex on both sides.
tasks._handle_payment
parse_settlement -> stamp nostr_event_id onto bitspire_event_id ->
try assert_nostr_attribution: on failure, insert row with
initial_status='rejected' + error_message, return without
spawning process_settlement.
crud.create_settlement_idempotent
Now takes `initial_status` (required) and `error_message`.
Normal path passes 'pending'; rejected path passes 'rejected'
with the reason. Single-statement insert — no two-step pending->
errored dance.
crud.get_stuck_settlements_for_operator
New `rejected` bucket alongside `errored` / `stuck_pending` /
`stuck_processing`. Distinct because retry is wrong for these:
the row was misrouted, not operationally failed.
models.DcaSettlement.status enum extended with 'rejected'.
Worklist response model carries the new bucket; API + UI plumbed
end-to-end.
static/js/index.js + templates/satmachineadmin/index.html
New 'rejected' worklist bucket (deep-orange, gpp_bad icon).
Force-reset button now scoped to stuck_pending / stuck_processing
only — was 'not errored' which would have shown on rejected too.
10 unit tests in tests/test_nostr_attribution.py cover hex<->hex,
hex<->bech32, case-insensitivity, every absent variant, mismatch,
and unparseable on either side. All pass.
Closes the consumer-side of aiolabs/satmachineadmin#19 (G5).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the operator-facing limitation that commission split legs could
only target the operator's own LNbits wallets. Adopting the splitpayments
pattern — a single `target` string accepts:
- LNbits wallet id (UUID-shaped) — direct internal pay
- LNbits wallet invoice key — resolved via get_wallet_for_key, then
internal pay (lets the operator split to any LNbits user who shares
their invoice key)
- Lightning address (user@domain) — resolved via LNURL-pay
- LNURL string (LNURL1...) — resolved via LNURL-pay
Schema (m001 update — fresh-install only; no operator data in production):
dca_commission_splits.wallet_id → target
Backend (distribution.py):
- New _pay_split_leg helper: routes the leg by target type. External
targets (@ or LNURL prefix) go through get_pr_from_lnurl + pay_invoice;
internal targets go through create_invoice + pay_invoice (the original
path), with get_wallet_for_key as the first resolution step so
invoice keys work as well as wallet ids.
- _pay_operator_splits delegates per-leg payment to the new helper.
- dca_payments rows still record the leg as leg_type='operator_split';
external targets land destination_ln_address (the human-readable
target), internal targets land destination_wallet_id.
- Errors are caught and surfaced via the existing failed-leg path
so /retry can re-run them.
Frontend (commission tab):
- Each leg gets a per-row q-btn-toggle: "My wallet" vs "Lightning
address / LNURL / invoice key". Wallet mode shows the q-select of
the operator's own wallets (previous behaviour); external mode
shows a free-text q-input.
- On load, targetKind is inferred from whether the stored target
matches one of the operator's wallet ids (renders as 'wallet')
or not (renders as 'external'). The kind is UI-only, not persisted.
- Leg row laid out in a bordered card so the toggle + 3-column layout
don't crowd at narrow widths.
Refs: aiolabs/satmachineadmin#9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Combines the final three P9 pieces into a single commit since each is
small and they share the JS state plumbing.
Super-fee edit (P9f — visible only to super_user):
- "Edit" affordance on the platform-fee banner, gated on
g.user.super_user (LNbits passes this through windowMixin)
- Modal: super_fee_pct (decimal 0..1) + super_fee_wallet_id (text)
- PUT /api/v1/dca/super-config (check_super_user on the backend)
- Operators see the same banner read-only — no edit button rendered
Worklist tab (P9g part 1):
- Reuses GET /api/v1/dca/settlements/stuck?threshold_minutes=N
- Three labeled buckets: errored / stuck_pending / stuck_processing,
each with row count chip
- Per-row actions: open machine detail (reuses viewMachine), retry
(for errored), force-reset (for stuck — confirmation dialog warns
only-use-if-truly-stuck)
- Threshold input (default 30 min) + manual refresh button
- "All clear" green banner when worklist is empty
- Auto-loads on `created()` so the badge count is accurate from boot
Reports tab (P9g part 2):
- Four CSV download cards: machines / clients / deposits / payments
- Clients CSV merges in the per-LP balance summary from clientBalances
so the export captures total_deposits/payments/remaining + currency
- Payments CSV lazy-loads from GET /api/v1/dca/payments since payments
aren't cached in dashboard state (could be many rows)
- _downloadCsv helper properly quotes/escapes values with embedded
commas/quotes/newlines per RFC 4180
- All exports are client-side; no new endpoint required
P9 is now complete (P9a–P9g). The v1 super-only Lamassu dashboard is
fully replaced. Operators can register machines, manage LPs + deposits,
configure commission splits, work through errored settlements, and
export their data — all against the v2 backend.
Total v2 frontend: ~1326 lines JS + ~1349 lines template, replacing
the v1's 773 + 851. Increase is from the much larger v2 surface
(machines, leg-typed payments, commission editor, worklist, settle-
balance, partial-dispense, notes, force-reset, retry).
Refs: aiolabs/satmachineadmin#9 — completes P9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator configures how the post-platform-fee commission remainder is
sliced across their wallets. Default ruleset applies fleet-wide; optional
per-machine overrides take precedence for that machine only.
Template (Commission tab content):
- Scope selector: "Default ruleset" or one option per operator machine
(override). Switching reloads the legs from the API.
- Live sum indicator (green ✓ if 100%, red ✗ otherwise). Save button
is disabled until the sum is valid.
- Editable row per leg: wallet select + label input + pct input.
Each row shows the % equivalent inline (e.g. 0.30 → 30.0%).
- Add-leg button appends an empty row.
- Preview banner: shows how an example 1000-sat operator commission
would split across the current legs, mirroring the server-side
last-leg-absorbs-rounding rule (calculations.allocate_operator_split_legs).
- "Remove override" button on per-machine scopes: deletes the override
so the default applies again (default legs untouched).
- Empty-state banner explains the consequence of no rules: operator
commission stays in the machine wallet.
JS:
- commissionScope state: null = default, else machine_id
- commissionScopeOptions computed: default + one per machine
- commissionLegs[] mirror the server's CommissionSplitLeg shape
- commissionSum / commissionSumValid: client-side invariant check
matching the SetCommissionSplitsData validator (within 0.0001)
- commissionPreview: pure JS port of allocate_operator_split_legs,
so the visualization matches what the server actually does
- saveCommissionSplits sends machine_id=null for default, else the
machine id; legs sort_order set from array index
- confirmDeleteCommissionOverride calls DELETE with ?machine_id=X to
clear just the override (no body)
- loadCommissionSplits called on created() so the tab is ready when
the operator clicks it
Routes wired:
GET /api/v1/dca/commission-splits
GET /api/v1/dca/commission-splits?machine_id=X
PUT /api/v1/dca/commission-splits
DELETE /api/v1/dca/commission-splits?machine_id=X
Refs: aiolabs/satmachineadmin#9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Operator records fiat handed in by LPs and drives the pending → confirmed
status transition that promotes deposits into LP balances.
Template (Deposits tab content + dialogs):
- Filter strip: status dropdown (all/pending/confirmed/rejected) and
LP dropdown (filtered by all the operator's LPs across machines)
- Table columns: status badge, LP (+ machine subtitle), amount, created
at, confirmed at, notes, action menu
- Action menu (only enabled on pending status — confirmed/rejected are
immutable for audit):
• Confirm — flips to status='confirmed' + refreshes LP balance
• Reject — opens reject dialog for optional reason notes
• Edit — amount/currency/notes change
• Delete
- Empty-state banners: orange if no LPs (deposits are LP-scoped), blue
if LPs exist but no deposits yet, grey if filters return nothing
- Record-deposit dialog: LP select (auto-derives machine), amount,
currency, notes
- Edit-deposit dialog: amount/currency/notes; LP+machine immutable
- Reject-deposit dialog: optional reason text persisted with the status
JS:
- loadDeposits, depositStatusColor, clientUsernameById helpers
- depositClientOptions computed: includes machine name in each option
label so operators see exactly where the deposit will land
- filteredDeposits computed: client-side filter on the loaded list
(no server-side filter param — operator's deposit volume small enough)
- submitDeposit handles both create and update paths; the create body
explicitly includes machine_id (auto-derived from the selected LP)
so the server can cross-check (client_id, machine_id) alignment
- confirmDepositStatus refreshes the LP balance after confirming,
since the confirmed deposit now affects remaining_balance display
Routes wired:
GET /api/v1/dca/deposits
POST /api/v1/dca/deposits
PUT /api/v1/dca/deposits/{id}
PUT /api/v1/dca/deposits/{id}/status
DELETE /api/v1/dca/deposits/{id}
Refs: aiolabs/satmachineadmin#9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds operator-scoped LP (liquidity provider) management. Operators
register LPs against specific machines, monitor remaining balances, and
settle small remainders via the P3e endpoint.
Template (Clients tab content + dialogs):
- Table with columns: machine, LP (username/short user_id), wallet,
DCA mode badge, remaining balance (color-coded: green if positive,
grey if zero), autoforward icon (with tooltip showing LN address),
status badge, action menu
- Empty-state banners: orange if no machines yet (LPs are
machine-scoped), blue if machines exist but no LPs registered
- Register-LP dialog: machine select + user_id + wallet_id + display
name + DCA mode (flow / fixed) + fixed-mode daily limit (conditional)
+ autoforward toggle + autoforward LN address (conditional)
- Edit-LP dialog: same minus immutable user_id/wallet_id, plus status
select (active/paused/closed)
- Settle-balance dialog (closes#4): funding wallet select + exchange
rate (operator-supplied) + optional amount_fiat (blank = full
remaining) + notes textarea. Shows the LP's current remaining
balance prominently before submission.
JS:
- loadClients pulls all operator's LPs across their fleet
- Per-LP balance summaries pre-loaded (one GET per LP — N+1, captured
in review issue #11 M3 for follow-up with a single grouped JOIN)
- openAddClientDialog / openEditClientDialog with separate cleaner
helpers (_cleanClientCreate vs _cleanClientUpdate) since the v2
API immutable-field rules differ between create and update
- openSettleBalanceDialog refreshes balance immediately before
showing the modal so the operator sees the up-to-date number
- confirmDeleteClient + DELETE wired
- machineNameById helper for displaying which machine an LP is at
- machineOptions computed for the register-LP machine select
- machinesById computed cache (avoids O(N*M) lookups in render loop)
Routes wired:
GET /api/v1/dca/clients
GET /api/v1/dca/clients/{id}/balance
POST /api/v1/dca/clients
PUT /api/v1/dca/clients/{id}
DELETE /api/v1/dca/clients/{id}
POST /api/v1/dca/clients/{id}/settle
Refs: aiolabs/satmachineadmin#9 — closes the Clients-tab gap in #4 +
exposes the autoforward setting (#8) in operator UI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the operator's primary workspace: a full-screen dialog opened from
any Fleet row that shows the machine's settlement history with action
menus for retry / partial-dispense / force-reset / note-add.
Template (templates/satmachineadmin/index.html):
- Full-screen Quasar dialog with q-bar header (machine name + fiat
chip + reload + close)
- Machine metadata strip: npub (copyable), wallet_id, location,
fallback_commission_pct
- Settlements table: status badge, time, gross / net / commission
(with super/op breakdown beneath), fiat amount, payment_hash short
- Notes blob expansion under each settlement row (pre-formatted)
- Per-row action menu (q-btn-dropdown):
• Add note — always available
• Retry — when status='errored'
• Partial dispense — when status in {pending, errored}
• Force-reset — when status in {pending, processing}
- Warning icon (⚠) on rows where used_fallback_split=true, namechecking
aiolabs/lamassu-next#44 in the tooltip
- Three sub-dialogs:
• Partial-dispense with fraction/sats toggle + notes input
• Add-note dialog (free-form, non-empty validation)
• (Retry/force-reset use Quasar.Dialog inline)
JS (static/js/index.js):
- viewMachine() opens detail and triggers reloadMachineDetail()
- GET /api/v1/dca/machines/{id}/settlements feeds the table
- confirmRetrySettlement → POST .../retry
- openPartialDispense → POST .../partial-dispense
- confirmForceReset → POST .../force-reset
- openSettlementNote → POST .../notes
- _replaceSettlement() updates the table row in-place from PUT/POST
responses so the operator sees instant feedback without a reload
- settlementStatusColor() maps statuses to Quasar badge colors
- formatSats / formatFiat / formatTime helpers; respect locale
Also: added data/ + *.sqlite3 to .gitignore so the
2026-05-14 auth-key leak can't recur from this repo (the equivalent
fix already landed in satmachineclient on the matching branch).
Refs: aiolabs/satmachineadmin#9 — closes the operator-detail-view
gap for #3 (partial dispense) + #4 (settlement) UX
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the v1 single-page super-only dashboard with the v2 operator-
scoped tabbed shell. This is the entry point for the v2 frontend — gets
the operator into a working state where they can register their first
machine and see the v2 endpoints behind a usable UI.
Template — templates/satmachineadmin/index.html (full rewrite):
- Tab strip: Fleet | Clients | Deposits | Commission | Worklist | Reports
- Header bar with operator-focused title + refresh button
- Platform-fee banner reads super-config (visible to all operators)
- Fleet tab: machines table + add/edit/delete row actions
- Worklist tab gets a count badge (red) when stuck/errored settlements
exist; populated by GET /settlements/stuck on load
- Other tabs land placeholder banners pointing at their P9b–P9g task
- Add-machine + edit-machine dialogs with full form, including
fallback_commission_pct note that namechecks lamassu-next#44
JS — static/js/index.js (full rewrite):
- Vue 3 + Quasar UMD app following the workspace CLAUDE.md conventions
- ${ } delimiters (Jinja owns {{ }})
- g.user guards (LNbits 1.4 timing — g.user can be null on initial mount)
- For typography overrides, inline :style instead of utility classes
(LNbits theme overrides Quasar's .text-* with !important)
- Pale bg-*-1 backgrounds paired with explicit dark text class for
dark-mode legibility
- LNbits.api.request for all calls; Quasar.Notify for feedback;
Quasar.copyToClipboard for npub copy
- Routes wired:
GET /api/v1/dca/super-config → banner readout
GET /api/v1/dca/machines → fleet table
POST /api/v1/dca/machines → add modal
PUT /api/v1/dca/machines/{id} → edit modal
DELETE /api/v1/dca/machines/{id} → confirm dialog
GET /api/v1/dca/settlements/stuck → worklist tab badge
Deleted the entire v1 surface: lamassu_config form, SSH tunnel settings,
single-config polling controls, quick-deposit form (deposits get their
own tab in P9d), manual transaction dialog (the partial-dispense +
retry endpoints replace it in P9b), distribution drill-down dialog.
Next: P9b (machine detail drawer — settlements list, retry button,
partial-dispense modal, notes panel), P9c (clients tab), P9d (deposits
tab), P9e (commission splits editor), P9g (worklist + CSV reports).
Refs: aiolabs/satmachineadmin#9
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add PUT /api/v1/dca/deposits/{id} endpoint to update amount, currency,
and notes on pending deposits. Add DELETE endpoint to remove deposits
not yet inserted into the machine. Both endpoints reject confirmed
deposits. Frontend now shows edit/delete buttons only for pending rows.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The finally block was resetting runningTestTransaction instead of
processingSpecificTransaction, causing the button to stay in loading
state after processing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
LNbits 1.4 changed g.user initialization (PR #3615), moving it from
windowMixin to base.html. This means g.user can be null during initial
Vue template evaluation.
- Use optional chaining g.user?.wallets || [] in template
- Add null guard before accessing this.g.user.wallets in JS
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements functionality to manually process specific Lamassu transactions by ID, bypassing dispense checks.
This allows administrators to handle transactions that may have failed due to dispense issues or were settled manually outside of the automated process.
The feature includes a new UI dialog for entering the transaction ID and an API endpoint to fetch and process the transaction, crediting wallets and distributing funds according to the DCA configuration.
Refactor GTQ storage migration: Moved the conversion logic for centavo amounts to GTQ into a new migration function, m004_convert_to_gtq_storage, ensuring proper data type changes and updates across relevant tables. This enhances clarity and maintains the integrity of the migration process.
Client registration will now be handled by the DCA client extension.
The admin extension focuses solely on:
- Reading existing clients
- Managing deposits (pending → confirmed workflow)
- Monitoring DCA activity
Test client creation code preserved in 'feature/test-client-creation' branch.
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
Update DCA API calls to use admin key: Changed references from `inkey` to `adminkey` in multiple DCA-related API requests to ensure proper access control and security compliance.
Refactor DCA Admin page endpoints: Update description, remove unused CRUD operations and API endpoints related to MyExtension, and streamline the code for improved clarity and functionality.
Remove QR Code dialog from MyExtension index.html: streamline the UI by eliminating unused dialog components, enhancing code clarity and maintainability.