Files
OpenFin/backend/routers/logs.py
OpenSquared 05a475fb04 feat: system logs page + dynamic IV watchlist with auto-add from cycle
Backend:
- DB: add system_logs table (level/source/cycle_id/ticker/message) and
  iv_watchlist table (ticker/added_by/is_active); seed builtin 18 tickers
- DBLogHandler attached at startup — all WARNING+ logs auto-persist to DB
- log_system_event() helper for structured manual events
- New router /api/logs: GET with filters (level, source, cycle_id, ticker,
  date range), GET /sources, GET /cycles for dropdowns, DELETE /clear
- iv_watchlist now read from DB instead of hardcoded constant; options_vol
  watchlist/refresh/bootstrap endpoints all use get_watchlist_tickers()
- New endpoints: POST/DELETE /options-vol/watchlist-tickers/{ticker} to
  add/remove tickers; adding triggers background 1-year bootstrap
- auto_cycle: after log_trade_entries(), auto-detect new underlying proxies
  not yet in watchlist, add them and bootstrap their IV history

Frontend:
- New page SystemLogs (/logs): log table with level/source/cycle/ticker/date
  filters, color-coded rows, expandable JSON details, auto-refresh 30s
- Options Lab: WatchlistManager section — add ticker input, chip list with
  builtin/auto/manual color coding, remove button for non-builtins
- Sidebar: Logs Système nav link (ScrollText icon)
- useApi: useSystemLogs, useLogSources, useLogCycles, useClearLogs,
  useWatchlistTickers, useAddWatchlistTicker, useRemoveWatchlistTicker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 13:07:35 +02:00

76 lines
2.3 KiB
Python

"""
System logs endpoint — exposes structured log events stored in system_logs table.
"""
from fastapi import APIRouter
from typing import Optional
router = APIRouter(prefix="/api/logs", tags=["logs"])
@router.get("")
def get_logs(
level: Optional[str] = None,
source: Optional[str] = None,
cycle_id: Optional[str] = None,
ticker: Optional[str] = None,
date_from: Optional[str] = None,
date_to: Optional[str] = None,
limit: int = 300,
):
"""
Query system logs with optional filters.
- level: INFO | WARNING | ERROR | CRITICAL
- source: logger name substring (e.g. 'auto_cycle', 'iv_engine')
- cycle_id: exact run_id UUID
- ticker: exact ticker symbol
- date_from / date_to: ISO date strings YYYY-MM-DD
- limit: max rows returned (default 300, max 1000)
"""
from services.database import get_system_logs
limit = min(limit, 1000)
logs = get_system_logs(
level=level,
source=source,
cycle_id=cycle_id,
ticker=ticker,
date_from=date_from,
date_to=date_to,
limit=limit,
)
return {"logs": logs, "count": len(logs)}
@router.get("/sources")
def get_log_sources():
"""List distinct source names present in system_logs (for filter dropdown)."""
from services.database import get_conn
conn = get_conn()
rows = conn.execute(
"SELECT DISTINCT source FROM system_logs WHERE source IS NOT NULL ORDER BY source"
).fetchall()
conn.close()
return {"sources": [r["source"] for r in rows]}
@router.get("/cycles")
def get_log_cycles(limit: int = 50):
"""List distinct cycle_ids (most recent first) for filter dropdown."""
from services.database import get_conn
conn = get_conn()
rows = conn.execute(
"""SELECT DISTINCT cycle_id, MIN(ts) as first_ts
FROM system_logs WHERE cycle_id IS NOT NULL
GROUP BY cycle_id ORDER BY first_ts DESC LIMIT ?""",
(limit,)
).fetchall()
conn.close()
return {"cycles": [{"cycle_id": r["cycle_id"], "first_ts": r["first_ts"]} for r in rows]}
@router.delete("/clear")
def clear_old_logs(older_than_days: int = 30):
"""Delete logs older than N days."""
from services.database import clear_system_logs
deleted = clear_system_logs(older_than_days)
return {"deleted": deleted, "older_than_days": older_than_days}