diff --git a/crud.py b/crud.py index 581b053..5a70fec 100644 --- a/crud.py +++ b/crud.py @@ -201,9 +201,23 @@ async def create_dca_client(data: CreateDcaClientData) -> DcaClient: return client +# Shared SELECT fragment: client columns plus the LP-onboarded flag +# computed via LEFT JOIN on dca_lp. Returned as `lp_onboarded` (boolean +# 0/1 in SQLite, which Pydantic coerces to bool on the DcaClient model). +_CLIENT_SELECT = """ + c.id, c.machine_id, c.user_id, c.username, c.status, + c.created_at, c.updated_at, + (lp.user_id IS NOT NULL) AS lp_onboarded +""" +_CLIENT_FROM = ( + "satoshimachine.dca_clients c " + "LEFT JOIN satoshimachine.dca_lp lp ON lp.user_id = c.user_id" +) + + async def get_dca_client(client_id: str) -> Optional[DcaClient]: return await db.fetchone( - "SELECT * FROM satoshimachine.dca_clients WHERE id = :id", + f"SELECT {_CLIENT_SELECT} FROM {_CLIENT_FROM} WHERE c.id = :id", {"id": client_id}, DcaClient, ) @@ -213,9 +227,9 @@ async def get_dca_client_for_machine_user( machine_id: str, user_id: str ) -> Optional[DcaClient]: return await db.fetchone( - """ - SELECT * FROM satoshimachine.dca_clients - WHERE machine_id = :machine_id AND user_id = :user_id + f""" + SELECT {_CLIENT_SELECT} FROM {_CLIENT_FROM} + WHERE c.machine_id = :machine_id AND c.user_id = :user_id """, {"machine_id": machine_id, "user_id": user_id}, DcaClient, @@ -224,10 +238,10 @@ async def get_dca_client_for_machine_user( async def get_dca_clients_for_machine(machine_id: str) -> List[DcaClient]: return await db.fetchall( - """ - SELECT * FROM satoshimachine.dca_clients - WHERE machine_id = :machine_id - ORDER BY created_at DESC + f""" + SELECT {_CLIENT_SELECT} FROM {_CLIENT_FROM} + WHERE c.machine_id = :machine_id + ORDER BY c.created_at DESC """, {"machine_id": machine_id}, DcaClient, @@ -237,9 +251,9 @@ async def get_dca_clients_for_machine(machine_id: str) -> List[DcaClient]: async def get_dca_clients_for_operator(operator_user_id: str) -> List[DcaClient]: """All clients across every machine this operator owns.""" return await db.fetchall( - """ - SELECT c.* - FROM satoshimachine.dca_clients c + f""" + SELECT {_CLIENT_SELECT} + FROM {_CLIENT_FROM} JOIN satoshimachine.dca_machines m ON m.id = c.machine_id WHERE m.operator_user_id = :uid ORDER BY c.created_at DESC @@ -252,10 +266,10 @@ async def get_dca_clients_for_operator(operator_user_id: str) -> List[DcaClient] async def get_dca_clients_for_user(user_id: str) -> List[DcaClient]: """LP cross-operator view — every machine this LP is registered at.""" return await db.fetchall( - """ - SELECT * FROM satoshimachine.dca_clients - WHERE user_id = :user_id - ORDER BY created_at DESC + f""" + SELECT {_CLIENT_SELECT} FROM {_CLIENT_FROM} + WHERE c.user_id = :user_id + ORDER BY c.created_at DESC """, {"user_id": user_id}, DcaClient, diff --git a/models.py b/models.py index f63b262..431e70d 100644 --- a/models.py +++ b/models.py @@ -102,6 +102,10 @@ class DcaClient(BaseModel): status: str created_at: datetime updated_at: datetime + # Computed at SELECT time via LEFT JOIN on dca_lp. Lets the operator + # UI render "pending onboarding" badges and disable deposit creation + # without a second round-trip per row. + lp_onboarded: bool = False class UpdateDcaClientData(BaseModel): diff --git a/static/js/index.js b/static/js/index.js index 966c1f7..fb2a470 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -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 } }, diff --git a/templates/satmachineadmin/index.html b/templates/satmachineadmin/index.html index 728e8c2..5972481 100644 --- a/templates/satmachineadmin/index.html +++ b/templates/satmachineadmin/index.html @@ -222,13 +222,21 @@ - - - - - + + + + LP has onboarded via satmachineclient. + Deposits and DCA distributions can proceed. + + + + + LP hasn't installed/opened satmachineclient yet. + Deposits will be refused until they register and + select a DCA wallet. + + - - - - Auto-forward enabled → - - - - - Auto-forward disabled - - + + + This LP hasn't onboarded via satmachineclient yet, so + their DCA wallet isn't configured. Ask them to open the + satmachineclient extension once and the deposit will be + accepted next time. + + @@ -1195,9 +1203,10 @@

- 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 + satmachineclient extension — you can't set them here. + Deposits are refused until the LP has registered.

- - - - - - - - - -