From 8360d2a6fdc5e9ef2892a1086fba4e7d8c73420e Mon Sep 17 00:00:00 2001 From: Shroominic Date: Tue, 12 Aug 2025 15:30:02 -0300 Subject: [PATCH] feat: add log investigation feature and modern UI to admin dashboard - Add 'Investigate Logs' button with modal for request ID input - Implement /admin/logs/{request_id} endpoint for log viewing - Parse and display JSON log entries with formatting - Search through last 7 days of log files - Add modern minimal CSS design system - Improve typography with system font stack - Add card layouts, shadows, and rounded corners - Implement smooth transitions and hover effects - Add emoji icons for visual interest - Rename 'User's API Keys' to 'Temporary Balances' --- routstr/core/admin.py | 460 +++++++++++++++++++++++++++++------------- 1 file changed, 315 insertions(+), 145 deletions(-) diff --git a/routstr/core/admin.py b/routstr/core/admin.py index b511e449..5a547347 100644 --- a/routstr/core/admin.py +++ b/routstr/core/admin.py @@ -1,5 +1,7 @@ +import json import os from datetime import datetime, timezone +from pathlib import Path from fastapi import APIRouter, HTTPException, Request from fastapi.responses import HTMLResponse @@ -8,6 +10,9 @@ from sqlmodel import select from ..wallet import get_balance, send_token from .db import ApiKey, create_session +from .logging import get_logger + +logger = get_logger(__name__) admin_router = APIRouter(prefix="/admin", include_in_schema=False) @@ -21,26 +26,14 @@ def login_form() -> str: -
- - -
+
+

🔐 Admin Login

+
+ + +
+
""" @@ -66,19 +62,15 @@ def info(content: str) -> str: -
- {content} +
+

{content}

@@ -124,105 +116,39 @@ async def dashboard(request: Request) -> str:

Admin Dashboard

-

Current Cashu Balance

-

Your Balance: {owner_balance} sats

-

The balance is calculated by subtracting the combined user balance from the total Cashu wallet balance.

-

Total Cashu Balance: {current_balance} sats

-

User Balance: {total_user_balance} sats

+ +
+

Cashu Wallet Balance

+
+ Your Balance + {owner_balance} sats +
+
+ Total Wallet + {current_balance} sats +
+
+ User Balance + {total_user_balance} sats +
+

Your balance = Total wallet - User balance

+
+ +
+ + @@ -358,7 +332,7 @@ async def dashboard(request: Request) -> str:

Save this token! It represents your withdrawn balance.

-

User's API Keys

+

Temporary Balances

@@ -383,6 +357,202 @@ async def admin(request: Request) -> str: return admin_auth() +@admin_router.get("/logs/{request_id}", response_class=HTMLResponse) +async def view_logs(request: Request, request_id: str) -> str: + admin_cookie = request.cookies.get("admin_password") + if not admin_cookie or admin_cookie != os.getenv("ADMIN_PASSWORD"): + return admin_auth() + + logger.info(f"Investigating logs for request_id: {request_id}") + + # Search for log entries with this request_id + log_entries = [] + logs_dir = Path("logs") + + if logs_dir.exists(): + # Get all log files sorted by modification time (most recent first) + log_files = sorted( + logs_dir.glob("*.log"), key=lambda x: x.stat().st_mtime, reverse=True + ) + + for log_file in log_files[:7]: # Check last 7 days of logs + try: + with open(log_file, "r") as f: + for line in f: + if request_id in line: + try: + # Parse JSON log entry + log_data = json.loads(line.strip()) + log_entries.append(log_data) + except json.JSONDecodeError: + # If not JSON, include raw line + log_entries.append({"raw": line.strip()}) + except Exception as e: + logger.error(f"Error reading log file {log_file}: {e}") + + # Sort entries by timestamp if available + log_entries.sort(key=lambda x: x.get("asctime", ""), reverse=False) + + # Format log entries for display + formatted_logs = [] + for entry in log_entries: + if "raw" in entry: + formatted_logs.append(f'
{entry["raw"]}
') + else: + # Format JSON log entry + timestamp = entry.get("asctime", "Unknown time") + level = entry.get("levelname", "INFO") + message = entry.get("message", "") + pathname = entry.get("pathname", "") + lineno = entry.get("lineno", "") + + # Extract additional fields + extra_fields = { + k: v + for k, v in entry.items() + if k + not in [ + "asctime", + "levelname", + "message", + "pathname", + "lineno", + "name", + "version", + "request_id", + ] + } + + level_class = level.lower() + formatted_entry = f""" +
+
+ {timestamp} + [{level}] + {pathname}:{lineno} +
+
{message}
+ """ + + if extra_fields: + formatted_entry += '
' + for key, value in extra_fields.items(): + formatted_entry += f'
{key}: {json.dumps(value) if isinstance(value, (dict, list)) else value}
' + formatted_entry += "
" + + formatted_entry += "
" + formatted_logs.append(formatted_entry) + + return f""" + + + + + + ← Back to Dashboard +

Log Investigation

+
+ Request ID: {request_id} +
+
+ {"".join(formatted_logs) if formatted_logs else '
No log entries found for this Request ID
'} +
+

+ Found {len(log_entries)} log entries • Searched last 7 days of logs +

+ + + """ + + @admin_router.post("/withdraw") async def withdraw( request: Request, withdraw_request: WithdrawRequest
Hashed Key