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

44
crud.py
View file

@ -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,