merge upstream: system status, health monitoring, connectivity caching

Merges xmrcheckout/xmrcheckout main (7 commits since 655d037):
- Add Monero system status and reconciler heartbeat
- Surface Monero health status in UI (dashboard + invoice pages)
- Harden reconciler status heartbeat startup
- Avoid false Monero outage states in UI
- Cache Monero connectivity in system status
- Next.js security patch (15.5.9 → 15.5.14)
- Bump Monero Docker defaults to 0.18.4.6

Resolved conflicts in reconciler.py (kept our advisory lock + upstream
heartbeat), routes.py (kept both imports), README.md (kept both sections).
This commit is contained in:
jwinterm
2026-03-28 12:20:32 -04:00
15 changed files with 780 additions and 76 deletions
+53 -2
View File
@@ -22,12 +22,13 @@ from .config import (
FOUNDER_PAYMENT_ADDRESS,
FOUNDER_VIEW_KEY,
INVOICE_DEFAULT_EXPIRY_HOURS,
INVOICE_RECONCILE_INTERVAL_SECONDS,
OPEN_REGISTRATION,
QR_STORAGE_DIR,
)
from .db import get_db
from .formatting import format_xmr_amount
from .models import Invoice, ProfileHistory, User, Webhook, WebhookDelivery
from .models import Invoice, ProfileHistory, SystemStatus, User, Webhook, WebhookDelivery
from monero.address import Address, IntegratedAddress, SubAddress
from .rates import get_xmr_rate
from .subaddress_allocator import MAX_SUBADDRESS_INDEX, create_subaddress_for_user
@@ -43,6 +44,7 @@ from .schemas import (
LoginResponse,
ProfileResponse,
ProfileUpdate,
SystemStatusResponse,
WebhookCreate,
WebhookDeliveryResponse,
WebhookResponse,
@@ -59,7 +61,6 @@ from .security import (
)
from .webhooks import build_webhook_payload, dispatch_webhooks
from .qr_codes import ensure_invoice_qr_png, invoice_qr_url, resolve_qr_settings
router = APIRouter()
WEBHOOK_EVENTS = (
@@ -78,12 +79,57 @@ def _require_donations_enabled() -> None:
)
MAX_QR_LOGO_DATA_URL_LENGTH = 120_000
MONERO_CONNECTIVITY_STATUS_NAME = "monero_connectivity"
def _get_user_for_api_key(db: Session, api_key: str) -> User | None:
return db.query(User).filter(User.api_key_hash == hash_api_key(api_key)).first()
def _status_response_from_rows(
monero_status: SystemStatus | None,
reconciler_status: SystemStatus | None,
) -> SystemStatusResponse:
wallet_rpc = (
str(monero_status.wallet_rpc)
if monero_status and monero_status.wallet_rpc
else "unreachable"
)
daemon = (
str(monero_status.daemon)
if monero_status and monero_status.daemon
else "unknown"
)
daemon_height = monero_status.daemon_height if monero_status else None
return SystemStatusResponse(
wallet_rpc=wallet_rpc,
daemon=daemon,
daemon_height=daemon_height,
invoice_reconcile_interval_seconds=INVOICE_RECONCILE_INTERVAL_SECONDS,
last_reconcile_started_at=(
reconciler_status.last_reconcile_started_at if reconciler_status else None
),
last_reconcile_completed_at=(
reconciler_status.last_reconcile_completed_at if reconciler_status else None
),
last_reconcile_error=(
reconciler_status.last_reconcile_error if reconciler_status else None
),
)
def _load_public_system_status(db: Session) -> SystemStatusResponse:
reconciler_status = (
db.query(SystemStatus).filter(SystemStatus.name == "reconciler").first()
)
monero_status = (
db.query(SystemStatus)
.filter(SystemStatus.name == MONERO_CONNECTIVITY_STATUS_NAME)
.first()
)
return _status_response_from_rows(monero_status, reconciler_status)
def _get_founder_user(db: Session) -> User:
if not FOUNDER_PAYMENT_ADDRESS or not FOUNDER_VIEW_KEY:
raise HTTPException(
@@ -541,6 +587,11 @@ def get_donation_status(
return _public_invoice_status_response(db, invoice, request)
@router.get("/api/core/public/system/status", response_model=SystemStatusResponse)
def get_public_system_status(db: Session = Depends(get_db)):
return _load_public_system_status(db)
@router.post("/api/core/webhooks", response_model=WebhookResponse)
def register_webhook(
payload: WebhookCreate,