ADR-005 slice 2: operator alert, settle off-machine, glossary + guide #50
2 changed files with 114 additions and 2 deletions
feat(ui): settle off-machine, glossary links, alerts pubkey
Settle action on the cash_owed/partial_pending worklist buckets and the settlements table; the worklist's error cell links the raw code to the glossary entry; the platform settings dialog gains the alerts pubkey.
commit
cdb1fe71cc
|
|
@ -108,7 +108,8 @@ window.app = Vue.createApp({
|
||||||
super_cash_in_fee_fraction: 0,
|
super_cash_in_fee_fraction: 0,
|
||||||
super_cash_out_fee_fraction: 0,
|
super_cash_out_fee_fraction: 0,
|
||||||
super_fee_wallet_id: '',
|
super_fee_wallet_id: '',
|
||||||
max_cash_in_sats: null
|
max_cash_in_sats: null,
|
||||||
|
alerts_pubkey: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -261,6 +262,13 @@ window.app = Vue.createApp({
|
||||||
dispensed_sats: null,
|
dispensed_sats: null,
|
||||||
notes: ''
|
notes: ''
|
||||||
},
|
},
|
||||||
|
// ADR-005 §6 — settle an owed-cash settlement off-machine
|
||||||
|
settleDialog: {
|
||||||
|
show: false,
|
||||||
|
settlement: null,
|
||||||
|
note: '',
|
||||||
|
saving: false
|
||||||
|
},
|
||||||
noteDialog: {
|
noteDialog: {
|
||||||
show: false,
|
show: false,
|
||||||
saving: false,
|
saving: false,
|
||||||
|
|
@ -659,7 +667,8 @@ window.app = Vue.createApp({
|
||||||
super_cash_out_fee_fraction:
|
super_cash_out_fee_fraction:
|
||||||
this.superConfig?.super_cash_out_fee_fraction ?? 0,
|
this.superConfig?.super_cash_out_fee_fraction ?? 0,
|
||||||
super_fee_wallet_id: this.superConfig?.super_fee_wallet_id || '',
|
super_fee_wallet_id: this.superConfig?.super_fee_wallet_id || '',
|
||||||
max_cash_in_sats: this.superConfig?.max_cash_in_sats ?? null
|
max_cash_in_sats: this.superConfig?.max_cash_in_sats ?? null,
|
||||||
|
alerts_pubkey: this.superConfig?.alerts_pubkey || ''
|
||||||
}
|
}
|
||||||
this.superFeeDialog.show = true
|
this.superFeeDialog.show = true
|
||||||
},
|
},
|
||||||
|
|
@ -1263,6 +1272,45 @@ window.app = Vue.createApp({
|
||||||
this.partialDispenseDialog.show = true
|
this.partialDispenseDialog.show = true
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ADR-005 §6 — the operator paid the customer by hand; close both ledgers.
|
||||||
|
openSettleCashOwed(settlement) {
|
||||||
|
this.settleDialog.settlement = settlement
|
||||||
|
this.settleDialog.note = ''
|
||||||
|
this.settleDialog.show = true
|
||||||
|
},
|
||||||
|
|
||||||
|
async submitSettleCashOwed() {
|
||||||
|
const d = this.settleDialog
|
||||||
|
d.saving = true
|
||||||
|
try {
|
||||||
|
const {data} = await LNbits.api.request(
|
||||||
|
'POST',
|
||||||
|
`${SETTLEMENTS_PATH}/${d.settlement.id}/settle-cash-owed`,
|
||||||
|
null,
|
||||||
|
{note: d.note}
|
||||||
|
)
|
||||||
|
this._replaceSettlement(data)
|
||||||
|
d.show = false
|
||||||
|
Quasar.Notify.create({
|
||||||
|
type: 'positive',
|
||||||
|
message:
|
||||||
|
'Settled — distributing the full amount; the machine will mark its row remediated'
|
||||||
|
})
|
||||||
|
if (this.worklist.totalCount) await this.loadWorklist()
|
||||||
|
} catch (e) {
|
||||||
|
this._notifyError(e, 'Settle failed')
|
||||||
|
} finally {
|
||||||
|
d.saving = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Deep link into the error glossary for a settlement's dispenser error.
|
||||||
|
errorGlossaryHref(settlement) {
|
||||||
|
const code = settlement.dispense_raw_code || settlement.dispense_error_code || ''
|
||||||
|
const anchor = code.toLowerCase().replace(/[^a-z0-9]+/g, '-')
|
||||||
|
return `/spirekeeper/static/docs/errors.html${anchor ? '#' + anchor : ''}`
|
||||||
|
},
|
||||||
|
|
||||||
// ADR-005 §5 — release a machine's cash-out hold after a terminal
|
// ADR-005 §5 — release a machine's cash-out hold after a terminal
|
||||||
// dispenser fault, when the jam was cleared without a recount.
|
// dispenser fault, when the jam was cleared without a recount.
|
||||||
confirmResumeCashOut(machine) {
|
confirmResumeCashOut(machine) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
<h4 class="q-my-none">Satoshi Machine — Operator</h4>
|
<h4 class="q-my-none">Satoshi Machine — Operator</h4>
|
||||||
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
<p class="text-caption q-my-none" :style="{opacity: 0.7}">
|
||||||
Manage your bitSpire fleet, liquidity providers, and commission distribution.
|
Manage your bitSpire fleet, liquidity providers, and commission distribution.
|
||||||
|
<a href="/spirekeeper/static/docs/operator-guide.html" target="_blank" rel="noopener"
|
||||||
|
:style="{marginLeft: '8px'}">Operator guide</a> ·
|
||||||
|
<a href="/spirekeeper/static/docs/errors.html" target="_blank" rel="noopener">Error glossary</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
|
|
@ -654,6 +657,13 @@
|
||||||
<q-td key="error_message">
|
<q-td key="error_message">
|
||||||
<span :style="{fontSize: '0.85em', opacity: 0.8}"
|
<span :style="{fontSize: '0.85em', opacity: 0.8}"
|
||||||
v-text="props.row.error_message || '—'"></span>
|
v-text="props.row.error_message || '—'"></span>
|
||||||
|
<a v-if="props.row.dispense_error_code"
|
||||||
|
:href="errorGlossaryHref(props.row)" target="_blank" rel="noopener"
|
||||||
|
:style="{fontSize: '0.8em', marginLeft: '6px', whiteSpace: 'nowrap'}">
|
||||||
|
<code v-text="props.row.dispense_raw_code || props.row.dispense_error_code"></code>
|
||||||
|
<q-icon name="help_outline" size="xs"></q-icon>
|
||||||
|
<q-tooltip>What this code means and what to do</q-tooltip>
|
||||||
|
</a>
|
||||||
</q-td>
|
</q-td>
|
||||||
<q-td key="actions" auto-width>
|
<q-td key="actions" auto-width>
|
||||||
<q-btn flat dense size="sm" icon="open_in_new"
|
<q-btn flat dense size="sm" icon="open_in_new"
|
||||||
|
|
@ -661,6 +671,12 @@
|
||||||
@click="viewMachineFromWorklist(props.row)">
|
@click="viewMachineFromWorklist(props.row)">
|
||||||
<q-tooltip>Open machine detail</q-tooltip>
|
<q-tooltip>Open machine detail</q-tooltip>
|
||||||
</q-btn>
|
</q-btn>
|
||||||
|
<q-btn v-if="bucket.key === 'cash_owed' || bucket.key === 'partial_pending'"
|
||||||
|
flat dense size="sm" icon="handshake"
|
||||||
|
color="positive"
|
||||||
|
@click="openSettleCashOwed(props.row)">
|
||||||
|
<q-tooltip>Settle off-machine — you paid the customer by hand</q-tooltip>
|
||||||
|
</q-btn>
|
||||||
<q-btn v-if="bucket.key === 'partial_pending'"
|
<q-btn v-if="bucket.key === 'partial_pending'"
|
||||||
flat dense size="sm" icon="call_split"
|
flat dense size="sm" icon="call_split"
|
||||||
color="deep-orange"
|
color="deep-orange"
|
||||||
|
|
@ -1119,6 +1135,15 @@
|
||||||
</q-item-section>
|
</q-item-section>
|
||||||
<q-item-section>Retry distribution</q-item-section>
|
<q-item-section>Retry distribution</q-item-section>
|
||||||
</q-item>
|
</q-item>
|
||||||
|
<q-item
|
||||||
|
v-if="['cash_owed','partial_pending'].includes(props.row.status)"
|
||||||
|
clickable v-close-popup
|
||||||
|
@click="openSettleCashOwed(props.row)">
|
||||||
|
<q-item-section avatar>
|
||||||
|
<q-icon name="handshake" color="positive"></q-icon>
|
||||||
|
</q-item-section>
|
||||||
|
<q-item-section>Settle off-machine…</q-item-section>
|
||||||
|
</q-item>
|
||||||
<q-item
|
<q-item
|
||||||
v-if="['pending','errored'].includes(props.row.status)"
|
v-if="['pending','errored'].includes(props.row.status)"
|
||||||
clickable v-close-popup
|
clickable v-close-popup
|
||||||
|
|
@ -1454,6 +1479,41 @@
|
||||||
<!-- =============================================================== -->
|
<!-- =============================================================== -->
|
||||||
<!-- ADD-NOTE DIALOG -->
|
<!-- ADD-NOTE DIALOG -->
|
||||||
<!-- =============================================================== -->
|
<!-- =============================================================== -->
|
||||||
|
<!-- ADR-005 §6: settle an owed-cash settlement off-machine -->
|
||||||
|
<q-dialog v-model="settleDialog.show" persistent>
|
||||||
|
<q-card :style="{width: '460px', maxWidth: '95vw'}">
|
||||||
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
<div class="text-h6">Settle off-machine</div>
|
||||||
|
<q-space></q-space>
|
||||||
|
<q-btn icon="close" flat round dense v-close-popup></q-btn>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-section v-if="settleDialog.settlement">
|
||||||
|
<p class="q-mb-sm">
|
||||||
|
The customer paid
|
||||||
|
<strong v-text="formatFiat(settleDialog.settlement.fiat_amount, settleDialog.settlement.fiat_code)"></strong>
|
||||||
|
and the machine reports
|
||||||
|
<strong v-text="settleDialog.settlement.status === 'cash_owed' ? 'nothing dispensed' : 'a partial dispense'"></strong>.
|
||||||
|
</p>
|
||||||
|
<p class="text-caption q-mb-md" :style="{opacity: 0.8}">
|
||||||
|
Use this when you have made the customer whole <em>outside</em> the machine.
|
||||||
|
The settlement distributes at the full amount and the machine marks its own
|
||||||
|
record remediated. Nothing is dispensed. If you dispensed the shortfall at the
|
||||||
|
machine instead, do nothing — it reports that itself.
|
||||||
|
</p>
|
||||||
|
<q-input v-model="settleDialog.note" type="textarea" autogrow outlined dense
|
||||||
|
label="How was the customer paid? (required)"
|
||||||
|
hint="e.g. handed 40 EUR to the customer in person, 2026-10-09"
|
||||||
|
maxlength="500" counter></q-input>
|
||||||
|
</q-card-section>
|
||||||
|
<q-card-actions align="right">
|
||||||
|
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||||||
|
<q-btn color="positive" label="Settle at full amount"
|
||||||
|
:disable="!settleDialog.note.trim()" :loading="settleDialog.saving"
|
||||||
|
@click="submitSettleCashOwed"></q-btn>
|
||||||
|
</q-card-actions>
|
||||||
|
</q-card>
|
||||||
|
</q-dialog>
|
||||||
|
|
||||||
<q-dialog v-model="noteDialog.show" persistent>
|
<q-dialog v-model="noteDialog.show" persistent>
|
||||||
<q-card :style="{width: '420px', maxWidth: '95vw'}">
|
<q-card :style="{width: '420px', maxWidth: '95vw'}">
|
||||||
<q-card-section class="row items-center q-pb-none">
|
<q-card-section class="row items-center q-pb-none">
|
||||||
|
|
@ -1518,6 +1578,10 @@
|
||||||
hint="Server-side ceiling on a single cash-in's principal. The ATM attests the amount; this bounds a compromised/buggy machine to one capped tx."
|
hint="Server-side ceiling on a single cash-in's principal. The ATM attests the amount; this bounds a compromised/buggy machine to one capped tx."
|
||||||
type="number" step="1" min="0"
|
type="number" step="1" min="0"
|
||||||
class="q-mb-md" dense outlined></q-input>
|
class="q-mb-md" dense outlined></q-input>
|
||||||
|
<q-input v-model="superFeeDialog.data.alerts_pubkey"
|
||||||
|
label="Alerts pubkey (hex or npub — blank = each operator's own account key)"
|
||||||
|
hint="Where owed-cash alerts are delivered as Nostr DMs (NIP-17). Blank sends each operator a note to self on their LNbits account identity."
|
||||||
|
class="q-mb-md" dense outlined></q-input>
|
||||||
</q-card-section>
|
</q-card-section>
|
||||||
<q-card-actions align="right">
|
<q-card-actions align="right">
|
||||||
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
<q-btn flat label="Cancel" v-close-popup></q-btn>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue