feat(v2): satmachineclient maintenance pass — v2 admin schema (P7)
Updates the LP-facing client extension to work against the v2 admin schema
(per-machine dca_clients, leg_type-discriminated dca_payments, settlement
audit trail) and strips the dead v1 code paths.
Maintenance-mode framing: the richer LP UI is migrating to ~/dev/webapp.
This extension now provides a minimal stable read-mostly surface so
existing LP installs don't break against the v2 admin schema.
models.py — rewrite:
+ PerMachinePosition: LP's position at a single machine
~ ClientDashboardSummary: now aggregates across all the LP's machines
and exposes per-machine breakdown in `positions` field
~ ClientTransaction: gains machine_id + machine_npub + settlement_id;
drops transaction_type / lamassu_transaction_id (v1 fields)
~ leg_type discriminator on ClientTransaction so the LP can tell DCA,
operator-initiated balance-settle, and auto-forward legs apart
+ UpdateClientAutoforward: LPs control their own autoforward setting
- ClientDashboardSummaryAPI, ClientTransactionAPI: deleted (GTQ-only
parallel "API" models that duplicated the internal ones)
- ClientPreferences, ClientRegistrationData, UpdateClientSettings:
deleted (registration + settings are operator-controlled in v2)
crud.py — rewrite:
+ _fetch_user_clients: all dca_clients rows for the LP, joined with
dca_machines for display metadata
+ _position_for_client: per-machine aggregation helper
+ get_client_dashboard_summary: aggregates positions + sums sats/fiat
+ computes cost basis on fiat-spent (not gross deposits)
+ get_client_transactions: filters to LP-visible leg_types
(dca / settlement / autoforward); optional machine_id filter
+ update_lp_autoforward: LP self-manages autoforward across all their
machine positions
- get_client_analytics (300+ lines of date-parsing pretzels): deleted;
webapp will own analytics
- register_dca_client: deleted (admin owns registration in v2)
- update_client_dca_settings: deleted (admin owns DCA mode/limits)
- Lamassu-era status='confirmed' (now 'completed'), transaction_type
column refs, lamassu_transaction_id column refs: all gone
views_api.py — rewrite to 3 endpoints:
GET /api/v1/dca-client/positions (aggregated dashboard)
GET /api/v1/dca-client/transactions (filterable history)
PUT /api/v1/dca-client/autoforward (LP self-manages forwarding)
All require_admin_key on the LP's wallet. Old registration / settings
/ analytics endpoints removed.
Files deleted:
tasks.py — empty stub ("No background tasks needed")
transaction_processor.py — empty stub ("No transaction processing")
README.md gains a status banner pointing LP audience at webapp.
.gitignore gains `data/` + sqlite db files + __pycache__ to prevent
LNbits runtime artifacts (auth keys, dev DBs) from being committed.
4 routes registered. Zero ruff errors across the extension.
Refs: aiolabs/satmachineadmin#9 — completes P7
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
98f82beb28
commit
ade4e67541
8 changed files with 289 additions and 738 deletions
231
views_api.py
231
views_api.py
|
|
@ -1,220 +1,81 @@
|
|||
# Description: Client-focused API endpoints for DCA dashboard
|
||||
# Satoshi Machine Client v2 — API.
|
||||
#
|
||||
# Read-mostly LP surface over the admin extension's v2 schema. Auth via
|
||||
# wallet admin key (LP must be an LNbits user; the admin key identifies
|
||||
# them as the wallet's owner).
|
||||
#
|
||||
# Maintenance-mode note: richer LP UI is moving to ~/dev/webapp. Keep this
|
||||
# surface stable + minimal.
|
||||
|
||||
from http import HTTPStatus
|
||||
from typing import List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from lnbits.core.models import WalletTypeInfo
|
||||
from lnbits.decorators import require_admin_key
|
||||
from starlette.exceptions import HTTPException
|
||||
|
||||
from .crud import (
|
||||
get_client_dashboard_summary,
|
||||
get_client_transactions,
|
||||
get_client_analytics,
|
||||
update_client_dca_settings,
|
||||
get_client_by_user_id,
|
||||
register_dca_client,
|
||||
update_lp_autoforward,
|
||||
)
|
||||
from .models import (
|
||||
ClientDashboardSummary,
|
||||
ClientTransaction,
|
||||
ClientAnalytics,
|
||||
UpdateClientSettings,
|
||||
ClientRegistrationData,
|
||||
UpdateClientAutoforward,
|
||||
)
|
||||
|
||||
satmachineclient_api_router = APIRouter()
|
||||
|
||||
|
||||
###################################################
|
||||
############## CLIENT REGISTRATION ###############
|
||||
###################################################
|
||||
|
||||
@satmachineclient_api_router.post("/api/v1/register", status_code=HTTPStatus.CREATED)
|
||||
async def api_register_client(
|
||||
registration_data: ClientRegistrationData,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> dict:
|
||||
"""Register a new DCA client
|
||||
|
||||
Clients can self-register using their wallet admin key.
|
||||
Creates a new client entry in the satoshimachine database.
|
||||
"""
|
||||
result = await register_dca_client(
|
||||
wallet.wallet.user,
|
||||
wallet.wallet.id,
|
||||
registration_data
|
||||
)
|
||||
|
||||
if "error" in result:
|
||||
if "already registered" in result["error"]:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.CONFLICT,
|
||||
detail=result["error"]
|
||||
)
|
||||
else:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail=result["error"]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@satmachineclient_api_router.get("/api/v1/registration-status")
|
||||
async def api_check_registration_status(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> dict:
|
||||
"""Check if user is already registered as a DCA client"""
|
||||
client = await get_client_by_user_id(wallet.wallet.user)
|
||||
|
||||
return {
|
||||
"is_registered": client is not None,
|
||||
"client_id": client["id"] if client else None,
|
||||
"dca_mode": client["dca_mode"] if client else None,
|
||||
"status": client["status"] if client else None,
|
||||
}
|
||||
|
||||
|
||||
###################################################
|
||||
############## CLIENT DASHBOARD API ###############
|
||||
###################################################
|
||||
|
||||
@satmachineclient_api_router.get("/api/v1/dashboard/summary")
|
||||
async def api_get_dashboard_summary(
|
||||
@satmachineclient_api_router.get(
|
||||
"/api/v1/dca-client/positions", response_model=ClientDashboardSummary
|
||||
)
|
||||
async def api_get_positions(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> ClientDashboardSummary:
|
||||
"""Get client dashboard summary metrics"""
|
||||
summary = await get_client_dashboard_summary(wallet.wallet.user)
|
||||
if not summary:
|
||||
"""LP's aggregated dashboard across every machine they're registered at,
|
||||
plus a per-machine breakdown in the `positions` field."""
|
||||
user_id = wallet.wallet.user
|
||||
summary = await get_client_dashboard_summary(user_id)
|
||||
if summary is None:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Client data not found"
|
||||
HTTPStatus.NOT_FOUND,
|
||||
"No DCA positions found for this wallet's owner. "
|
||||
"An operator must register you at a machine first.",
|
||||
)
|
||||
return summary
|
||||
|
||||
|
||||
@satmachineclient_api_router.get("/api/v1/dashboard/transactions")
|
||||
async def api_get_client_transactions(
|
||||
@satmachineclient_api_router.get(
|
||||
"/api/v1/dca-client/transactions", response_model=List[ClientTransaction]
|
||||
)
|
||||
async def api_get_transactions(
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
machine_id: Optional[str] = None,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
limit: int = Query(50, ge=1, le=1000),
|
||||
offset: int = Query(0, ge=0),
|
||||
transaction_type: Optional[str] = Query(None),
|
||||
start_date: Optional[datetime] = Query(None),
|
||||
end_date: Optional[datetime] = Query(None),
|
||||
) -> List[ClientTransaction]:
|
||||
"""Get client's DCA transaction history with filtering"""
|
||||
"""LP's distribution history. Returns only legs the LP can see (DCA,
|
||||
balance-settle, auto-forward). Optionally filter by ?machine_id=X."""
|
||||
user_id = wallet.wallet.user
|
||||
return await get_client_transactions(
|
||||
wallet.wallet.user,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
transaction_type=transaction_type,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
user_id, limit=limit, offset=offset, machine_id=machine_id
|
||||
)
|
||||
|
||||
|
||||
@satmachineclient_api_router.get("/api/v1/dashboard/analytics")
|
||||
async def api_get_client_analytics(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
time_range: str = Query("30d", regex="^(7d|30d|90d|1y|all)$"),
|
||||
) -> ClientAnalytics:
|
||||
"""Get client performance analytics and cost basis data"""
|
||||
try:
|
||||
analytics = await get_client_analytics(wallet.wallet.user, time_range)
|
||||
if not analytics:
|
||||
# Return empty analytics data instead of error
|
||||
return ClientAnalytics(
|
||||
user_id=wallet.wallet.user,
|
||||
cost_basis_history=[],
|
||||
accumulation_timeline=[],
|
||||
transaction_frequency={}
|
||||
)
|
||||
return analytics
|
||||
except Exception as e:
|
||||
print(f"Analytics error: {e}")
|
||||
# Return empty analytics data as fallback
|
||||
return ClientAnalytics(
|
||||
user_id=wallet.wallet.user,
|
||||
cost_basis_history=[],
|
||||
accumulation_timeline=[],
|
||||
transaction_frequency={}
|
||||
)
|
||||
|
||||
|
||||
@satmachineclient_api_router.put("/api/v1/dashboard/settings")
|
||||
async def api_update_client_settings(
|
||||
settings: UpdateClientSettings,
|
||||
@satmachineclient_api_router.put(
|
||||
"/api/v1/dca-client/autoforward", response_model=dict
|
||||
)
|
||||
async def api_update_autoforward(
|
||||
data: UpdateClientAutoforward,
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
) -> dict:
|
||||
"""Update client DCA settings (mode, limits, status)
|
||||
|
||||
Security: Users can only modify their own DCA settings.
|
||||
Validated by user_id lookup from wallet.wallet.user.
|
||||
"""
|
||||
client = await get_client_by_user_id(wallet.wallet.user)
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail="Client profile not found"
|
||||
)
|
||||
|
||||
success = await update_client_dca_settings(client.id, settings)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.BAD_REQUEST,
|
||||
detail="Failed to update settings"
|
||||
)
|
||||
|
||||
return {"message": "Settings updated successfully"}
|
||||
"""LP controls their own auto-forward setting (where DCA distributions
|
||||
get forwarded to externally, per satmachineadmin#8). Applies to all of
|
||||
the LP's dca_clients rows; operator can't override LP-controlled
|
||||
settings."""
|
||||
user_id = wallet.wallet.user
|
||||
n = await update_lp_autoforward(user_id, data)
|
||||
return {"updated_clients": n}
|
||||
|
||||
|
||||
@satmachineclient_api_router.get("/api/v1/dashboard/export/transactions")
|
||||
async def api_export_transactions(
|
||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||
format: str = Query("csv", regex="^(csv|json)$"),
|
||||
start_date: Optional[datetime] = Query(None),
|
||||
end_date: Optional[datetime] = Query(None),
|
||||
):
|
||||
"""Export client transaction history"""
|
||||
transactions = await get_client_transactions(
|
||||
wallet.wallet.user,
|
||||
limit=10000, # Large limit for export
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
if format == "csv":
|
||||
# Return CSV response
|
||||
from io import StringIO
|
||||
import csv
|
||||
|
||||
output = StringIO()
|
||||
writer = csv.writer(output)
|
||||
writer.writerow(['Date', 'Amount (Sats)', 'Amount (Fiat)', 'Exchange Rate', 'Type', 'Status'])
|
||||
|
||||
for tx in transactions:
|
||||
writer.writerow([
|
||||
tx.created_at.isoformat(),
|
||||
tx.amount_sats,
|
||||
tx.amount_fiat, # Amount already in GTQ
|
||||
tx.exchange_rate,
|
||||
tx.transaction_type,
|
||||
tx.status
|
||||
])
|
||||
|
||||
from fastapi.responses import StreamingResponse
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
iter([output.getvalue()]),
|
||||
media_type="text/csv",
|
||||
headers={"Content-Disposition": "attachment; filename=dca_transactions.csv"}
|
||||
)
|
||||
else:
|
||||
return {"transactions": transactions}
|
||||
|
||||
|
||||
# Removed local client-limits endpoint
|
||||
# Client should call admin extension's public endpoint directly
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue