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

@ -141,18 +141,21 @@ window.app = Vue.createApp({
},
clientsTable: {
// Wallet / mode / autoforward dropped — those are LP-controlled
// via satmachineclient, not the operator's concern. `onboarded`
// surfaces the dca_lp existence flag (lp_onboarded) so operators
// can see at a glance which LPs still need to register before
// deposits can be recorded against them.
columns: [
{name: 'machine', label: 'Machine', field: 'machine_id', align: 'left'},
{name: 'username', label: 'LP', field: 'username', align: 'left'},
{name: 'wallet_id', label: 'Wallet', field: 'wallet_id', align: 'left'},
{name: 'dca_mode', label: 'Mode', field: 'dca_mode', align: 'left'},
{name: 'onboarded', label: 'Onboarded', field: 'lp_onboarded', align: 'center'},
{
name: 'remaining_balance',
label: 'Balance',
field: 'remaining_balance',
align: 'right'
},
{name: 'autoforward', label: '→', field: 'autoforward_enabled', align: 'center'},
{name: 'status', label: 'Status', field: 'status', align: 'left'},
{name: 'actions', label: '', field: 'id', align: 'right'}
],
@ -257,11 +260,24 @@ window.app = Vue.createApp({
}))
},
depositClientOptions() {
// Annotate each LP option with onboarding state so the operator
// sees at-pick time which LPs can accept deposits. We don't hide
// un-onboarded LPs — the operator might want to know they exist
// and chase them — but submission is gated below by
// `selectedDepositClient.lp_onboarded`.
return this.clients.map(c => ({
label: `${c.username || this.shortId(c.user_id)} @ ${this.machineNameById(c.machine_id)}`,
value: c.id
label:
`${c.username || this.shortId(c.user_id)} @ ` +
`${this.machineNameById(c.machine_id)}` +
(c.lp_onboarded ? '' : ' — pending onboarding'),
value: c.id,
disable: !c.lp_onboarded
}))
},
selectedDepositClient() {
const id = this.depositDialog.data.client_id
return id ? this.clients.find(c => c.id === id) : null
},
worklistBuckets() {
return [
{
@ -559,9 +575,9 @@ window.app = Vue.createApp({
})
this._downloadCsv(
'clients.csv',
['id', 'machine_id', 'machine_name', 'user_id', 'wallet_id',
'username', 'dca_mode', 'status', 'autoforward_enabled',
'autoforward_ln_address', 'total_deposits', 'total_payments',
['id', 'machine_id', 'machine_name', 'user_id',
'username', 'lp_onboarded', 'status',
'total_deposits', 'total_payments',
'remaining_balance', 'balance_currency', 'created_at'],
rows
)
@ -881,12 +897,7 @@ window.app = Vue.createApp({
id: client.id,
machine_id: client.machine_id,
user_id: client.user_id,
wallet_id: client.wallet_id,
username: client.username || '',
dca_mode: client.dca_mode,
fixed_mode_daily_limit: client.fixed_mode_daily_limit,
autoforward_enabled: !!client.autoforward_enabled,
autoforward_ln_address: client.autoforward_ln_address || '',
status: client.status
}
this.clientDialog.show = true
@ -1277,15 +1288,13 @@ window.app = Vue.createApp({
},
_emptyClientForm() {
// Operator-side LP enrolment is just (machine, user, optional
// display name). Wallet / mode / autoforward are LP-controlled
// via satmachineclient — operator can't pick or change them.
return {
machine_id: null,
user_id: '',
wallet_id: '',
username: '',
dca_mode: 'flow',
fixed_mode_daily_limit: null,
autoforward_enabled: false,
autoforward_ln_address: '',
status: 'active'
}
},
@ -1294,30 +1303,13 @@ window.app = Vue.createApp({
return {
machine_id: d.machine_id,
user_id: (d.user_id || '').trim(),
wallet_id: (d.wallet_id || '').trim(),
username: (d.username || '').trim() || null,
dca_mode: d.dca_mode || 'flow',
fixed_mode_daily_limit:
d.dca_mode === 'fixed' && d.fixed_mode_daily_limit
? Number(d.fixed_mode_daily_limit) : null,
autoforward_enabled: !!d.autoforward_enabled,
autoforward_ln_address:
d.autoforward_enabled && d.autoforward_ln_address
? d.autoforward_ln_address.trim() : null
username: (d.username || '').trim() || null
}
},
_cleanClientUpdate(d) {
return {
username: (d.username || '').trim() || null,
dca_mode: d.dca_mode,
fixed_mode_daily_limit:
d.dca_mode === 'fixed' && d.fixed_mode_daily_limit
? Number(d.fixed_mode_daily_limit) : null,
autoforward_enabled: !!d.autoforward_enabled,
autoforward_ln_address:
d.autoforward_enabled && d.autoforward_ln_address
? d.autoforward_ln_address.trim() : null,
status: d.status
}
},