feat(v2)(ui): operator-side LP UI matches the new dca_lp authority split

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>
This commit is contained in:
Padreug 2026-05-16 10:12:23 +02:00
commit cfad4e341c
4 changed files with 93 additions and 103 deletions

View file

@ -222,13 +222,21 @@
<q-td key="username">
<span v-text="props.row.username || shortId(props.row.user_id)"></span>
</q-td>
<q-td key="wallet_id">
<code :style="{fontSize: '0.8em'}"
v-text="shortId(props.row.wallet_id)"></code>
</q-td>
<q-td key="dca_mode">
<q-badge :color="props.row.dca_mode === 'flow' ? 'blue' : 'purple'"
:label="props.row.dca_mode"></q-badge>
<q-td key="onboarded">
<q-icon v-if="props.row.lp_onboarded"
name="check_circle" color="green" size="sm">
<q-tooltip>
LP has onboarded via satmachineclient.
Deposits and DCA distributions can proceed.
</q-tooltip>
</q-icon>
<q-icon v-else name="pending" color="deep-orange" size="sm">
<q-tooltip>
LP hasn't installed/opened satmachineclient yet.
Deposits will be refused until they register and
select a DCA wallet.
</q-tooltip>
</q-icon>
</q-td>
<q-td key="remaining_balance" class="text-right">
<span v-if="clientBalances[props.row.id]"
@ -236,18 +244,6 @@
v-text="formatFiat(clientBalances[props.row.id].remaining_balance, clientBalances[props.row.id].currency)"></span>
<span v-else :style="{opacity: 0.5}"></span>
</q-td>
<q-td key="autoforward">
<q-icon v-if="props.row.autoforward_enabled"
name="forward_to_inbox" color="primary" size="sm">
<q-tooltip>
Auto-forward enabled →
<span v-text="props.row.autoforward_ln_address || '(no address)'"></span>
</q-tooltip>
</q-icon>
<q-icon v-else name="forward" color="grey-5" size="sm">
<q-tooltip>Auto-forward disabled</q-tooltip>
</q-icon>
</q-td>
<q-td key="status">
<q-badge
:color="props.row.status === 'active' ? 'green' : 'grey'"
@ -1128,6 +1124,17 @@
class="q-mb-md" dense outlined
:rules="[v => !!v || 'Pick an LP']"></q-select>
<q-banner v-if="depositDialog.mode === 'add' && selectedDepositClient && !selectedDepositClient.lp_onboarded"
class="bg-deep-orange-1 text-grey-9 q-mb-md">
<template v-slot:avatar>
<q-icon name="pending" color="deep-orange"></q-icon>
</template>
This LP hasn't onboarded via <b>satmachineclient</b> yet, so
their DCA wallet isn't configured. Ask them to open the
satmachineclient extension once and the deposit will be
accepted next time.
</q-banner>
<q-input v-model.number="depositDialog.data.amount"
label="Amount (fiat)"
type="number" step="0.01" min="0"
@ -1148,6 +1155,7 @@
<q-btn color="primary"
:label="depositDialog.mode === 'add' ? 'Record' : 'Save'"
:loading="depositDialog.saving"
:disable="depositDialog.mode === 'add' && (!selectedDepositClient || !selectedDepositClient.lp_onboarded)"
@click="submitDeposit"></q-btn>
</q-card-actions>
</q-card>
@ -1195,9 +1203,10 @@
<q-card-section>
<p class="text-caption q-mb-md" :style="{opacity: 0.7}"
v-if="clientDialog.mode === 'add'">
LPs receive DCA distributions proportional to their remaining
balance. Each LP is scoped to a single machine; the same LP user
can register at multiple machines as separate rows.
Enrol an LP at one of your machines. Wallet, DCA mode, and
autoforward are configured by the LP themselves via the
<b>satmachineclient</b> extension — you can't set them here.
Deposits are refused until the LP has registered.
</p>
<q-select
@ -1217,38 +1226,9 @@
class="q-mb-md" dense outlined
:rules="[v => !!v || 'Required']"></q-input>
<q-input v-if="clientDialog.mode === 'add'"
v-model="clientDialog.data.wallet_id"
label="LP's wallet_id (receives DCA)"
hint="The LP's wallet where their sats land"
class="q-mb-md" dense outlined
:rules="[v => !!v || 'Required']"></q-input>
<q-input v-model="clientDialog.data.username"
label="Display name (optional)"
class="q-mb-md" dense outlined></q-input>
<q-select v-model="clientDialog.data.dca_mode"
:options="[{label: 'Flow (proportional)', value: 'flow'},
{label: 'Fixed (daily limit)', value: 'fixed'}]"
label="DCA mode"
emit-value map-options
class="q-mb-md" dense outlined></q-select>
<q-input v-if="clientDialog.data.dca_mode === 'fixed'"
v-model.number="clientDialog.data.fixed_mode_daily_limit"
label="Fixed mode daily limit (fiat)"
type="number" step="0.01"
class="q-mb-md" dense outlined></q-input>
<q-toggle v-model="clientDialog.data.autoforward_enabled"
label="Auto-forward DCA to external LN address"
class="q-mb-md"></q-toggle>
<q-input v-if="clientDialog.data.autoforward_enabled"
v-model="clientDialog.data.autoforward_ln_address"
label="LN address (e.g. user@walletofsatoshi.com)"
hint="LP-controlled; failures leave sats safely in LP's LNbits wallet"
hint="Operator-facing label only; doesn't affect distribution"
class="q-mb-md" dense outlined></q-input>
<q-select v-if="clientDialog.mode === 'edit'"