From a0d9f1f9d953c72b148923203921129132537995 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 31 Mar 2026 15:54:21 -0500 Subject: [PATCH] Add live logs to admin dashboard with level filtering and error middleware --- backend/app/api/endpoints/admin.py | 53 ++++++++++++++++++++- backend/app/main.py | 21 ++++++++- frontend/src/lib/api.ts | 10 ++++ frontend/src/pages/Admin.tsx | 75 ++++++++++++++++++++++++++++-- 4 files changed, 153 insertions(+), 6 deletions(-) diff --git a/backend/app/api/endpoints/admin.py b/backend/app/api/endpoints/admin.py index 8b43ea7..1c1434b 100644 --- a/backend/app/api/endpoints/admin.py +++ b/backend/app/api/endpoints/admin.py @@ -1,6 +1,8 @@ +import logging +import io from datetime import datetime, timezone, timedelta -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select, func from sqlalchemy.ext.asyncio import AsyncSession @@ -15,6 +17,39 @@ router = APIRouter(prefix="/admin", tags=["admin"]) ADMIN_EMAILS = ["chris.ryan@deepcutsai.com"] +# In-memory log buffer for admin viewing +LOG_BUFFER_SIZE = 500 +_log_buffer: list[dict] = [] + + +class AdminLogHandler(logging.Handler): + """Captures log records into an in-memory buffer for the admin UI.""" + + def emit(self, record): + entry = { + "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(), + "level": record.levelname, + "logger": record.name, + "message": self.format(record), + } + _log_buffer.append(entry) + if len(_log_buffer) > LOG_BUFFER_SIZE: + _log_buffer.pop(0) + + +# Attach handler to root logger and uvicorn +_handler = AdminLogHandler() +_handler.setLevel(logging.INFO) +_handler.setFormatter(logging.Formatter("%(message)s")) +logging.getLogger().addHandler(_handler) +logging.getLogger("uvicorn.access").addHandler(_handler) +logging.getLogger("uvicorn.error").addHandler(_handler) +logging.getLogger("app").addHandler(_handler) + +# Create app logger for explicit logging +app_logger = logging.getLogger("app") +app_logger.setLevel(logging.INFO) + @router.get("/stats") async def get_stats( @@ -98,3 +133,19 @@ async def get_stats( }, "user_breakdown": user_breakdown, } + + +@router.get("/logs") +async def get_logs( + user: User = Depends(get_current_user), + level: str = Query("ALL", description="Filter by level: ALL, ERROR, WARNING, INFO"), + limit: int = Query(100, description="Number of log entries"), +): + if user.email not in ADMIN_EMAILS: + raise HTTPException(status_code=403, detail="Admin only") + + logs = _log_buffer[-limit:] + if level != "ALL": + logs = [l for l in logs if l["level"] == level.upper()] + + return {"logs": list(reversed(logs)), "total": len(_log_buffer)} diff --git a/backend/app/main.py b/backend/app/main.py index be92fa2..c173906 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,5 +1,9 @@ -from fastapi import FastAPI +import logging +import traceback + +from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse from app.core.config import settings from app.api.endpoints import admin, auth, bandcamp, billing, lastfm, manual_import, playlist_fix, playlists, profile, recommendations, youtube_music @@ -27,6 +31,21 @@ app.include_router(bandcamp.router, prefix="/api") app.include_router(profile.router, prefix="/api") +logger = logging.getLogger("app") + + +@app.middleware("http") +async def log_errors(request: Request, call_next): + try: + response = await call_next(request) + if response.status_code >= 400: + logger.warning(f"{request.method} {request.url.path} -> {response.status_code}") + return response + except Exception as e: + logger.error(f"{request.method} {request.url.path} -> 500: {e}\n{traceback.format_exc()}") + return JSONResponse(status_code=500, content={"detail": "Internal server error"}) + + @app.get("/api/health") async def health(): return {"status": "ok", "app": "vynl"} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 21226fd..d643dae 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -310,4 +310,14 @@ export interface AdminStats { export const getAdminStats = () => api.get('/admin/stats').then((r) => r.data) +export interface LogEntry { + timestamp: string + level: string + logger: string + message: string +} + +export const getAdminLogs = (level: string = 'ALL', limit: number = 100) => + api.get<{ logs: LogEntry[]; total: number }>('/admin/logs', { params: { level, limit } }).then((r) => r.data) + export default api diff --git a/frontend/src/pages/Admin.tsx b/frontend/src/pages/Admin.tsx index d7ea299..4b552eb 100644 --- a/frontend/src/pages/Admin.tsx +++ b/frontend/src/pages/Admin.tsx @@ -1,13 +1,16 @@ import { useEffect, useState } from 'react' -import { Shield, Users, ListMusic, Sparkles, Heart, ThumbsDown } from 'lucide-react' +import { Shield, Users, ListMusic, Sparkles, Heart, ThumbsDown, ScrollText, RefreshCw } from 'lucide-react' import { useAuth } from '../lib/auth' -import { getAdminStats, type AdminStats } from '../lib/api' +import { getAdminStats, getAdminLogs, type AdminStats, type LogEntry } from '../lib/api' const ADMIN_EMAIL = 'chris.ryan@deepcutsai.com' export default function Admin() { const { user } = useAuth() const [stats, setStats] = useState(null) + const [logs, setLogs] = useState([]) + const [logLevel, setLogLevel] = useState('ALL') + const [logTotal, setLogTotal] = useState(0) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -20,14 +23,20 @@ export default function Admin() { return } - getAdminStats() - .then(setStats) + Promise.all([ + getAdminStats().then(setStats), + getAdminLogs('ALL', 200).then((d) => { setLogs(d.logs); setLogTotal(d.total) }), + ]) .catch((err) => { setError(err.response?.status === 403 ? 'Access denied. Admin privileges required.' : 'Failed to load admin stats.') }) .finally(() => setLoading(false)) }, [isAdmin]) + const refreshLogs = () => { + getAdminLogs(logLevel, 200).then((d) => { setLogs(d.logs); setLogTotal(d.total) }) + } + if (loading) { return (
@@ -181,6 +190,64 @@ export default function Admin() {
+ {/* Logs */} +
+
+
+ +

Logs

+ ({logTotal} total) +
+
+ {['ALL', 'ERROR', 'WARNING', 'INFO'].map((lvl) => ( + + ))} + +
+
+
+ {logs.length === 0 ? ( +

No logs found

+ ) : ( + logs.map((log, i) => ( +
+ + {new Date(log.timestamp).toLocaleTimeString()} + + + {log.level} + + {log.message} +
+ )) + )} +
+
) }