feat: implement in-memory caching for dashboard data and background warmup task
This commit is contained in:
32
src/dashboard_cache.py
Normal file
32
src/dashboard_cache.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
In-memory cache for dashboard Overview data.
|
||||||
|
|
||||||
|
A background task periodically refreshes this cache so the dashboard
|
||||||
|
serves pre-computed data instantly instead of hitting SQLite cold.
|
||||||
|
|
||||||
|
Memory footprint is fixed — each key is overwritten on every refresh.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import threading
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
_lock = threading.Lock()
|
||||||
|
_cache: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def get_cached(key: str) -> Optional[Any]:
|
||||||
|
"""Get a value from the dashboard cache."""
|
||||||
|
with _lock:
|
||||||
|
return _cache.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
def set_cached(key: str, value: Any) -> None:
|
||||||
|
"""Set a value in the dashboard cache."""
|
||||||
|
with _lock:
|
||||||
|
_cache[key] = value
|
||||||
|
|
||||||
|
|
||||||
|
def is_warm() -> bool:
|
||||||
|
"""Check if the cache has been populated at least once."""
|
||||||
|
with _lock:
|
||||||
|
return "stats" in _cache
|
||||||
@@ -18,6 +18,7 @@ from pydantic import BaseModel
|
|||||||
|
|
||||||
from dependencies import get_db, get_client_ip
|
from dependencies import get_db, get_client_ip
|
||||||
from logger import get_app_logger
|
from logger import get_app_logger
|
||||||
|
from dashboard_cache import get_cached, is_warm
|
||||||
|
|
||||||
# Server-side session token store (valid tokens for authenticated sessions)
|
# Server-side session token store (valid tokens for authenticated sessions)
|
||||||
_auth_tokens: set = set()
|
_auth_tokens: set = set()
|
||||||
@@ -249,10 +250,16 @@ async def all_ips(
|
|||||||
sort_by: str = Query("total_requests"),
|
sort_by: str = Query("total_requests"),
|
||||||
sort_order: str = Query("desc"),
|
sort_order: str = Query("desc"),
|
||||||
):
|
):
|
||||||
db = get_db()
|
|
||||||
page = max(1, page)
|
page = max(1, page)
|
||||||
page_size = min(max(1, page_size), 10000)
|
page_size = min(max(1, page_size), 10000)
|
||||||
|
|
||||||
|
# Serve from cache on default map request (top 100 IPs)
|
||||||
|
if page == 1 and page_size == 100 and sort_by == "total_requests" and sort_order == "desc" and is_warm():
|
||||||
|
cached = get_cached("map_ips")
|
||||||
|
if cached:
|
||||||
|
return JSONResponse(content=cached, headers=_no_cache_headers())
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
try:
|
try:
|
||||||
result = db.get_all_ips_paginated(
|
result = db.get_all_ips_paginated(
|
||||||
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
page=page, page_size=page_size, sort_by=sort_by, sort_order=sort_order
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from fastapi.responses import JSONResponse
|
|||||||
from logger import get_app_logger
|
from logger import get_app_logger
|
||||||
|
|
||||||
from dependencies import get_db, get_templates
|
from dependencies import get_db, get_templates
|
||||||
|
from dashboard_cache import get_cached, is_warm
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -17,17 +18,19 @@ router = APIRouter()
|
|||||||
@router.get("")
|
@router.get("")
|
||||||
@router.get("/")
|
@router.get("/")
|
||||||
async def dashboard_page(request: Request):
|
async def dashboard_page(request: Request):
|
||||||
db = get_db()
|
|
||||||
config = request.app.state.config
|
config = request.app.state.config
|
||||||
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
dashboard_path = "/" + config.dashboard_secret_path.lstrip("/")
|
||||||
|
|
||||||
# Get initial data for server-rendered sections
|
# Serve from pre-computed cache when available, fall back to live queries
|
||||||
stats = db.get_dashboard_counts()
|
if is_warm():
|
||||||
suspicious = db.get_recent_suspicious(limit=10)
|
stats = get_cached("stats")
|
||||||
|
suspicious = get_cached("suspicious")
|
||||||
# Get credential count for the stats card
|
else:
|
||||||
cred_result = db.get_credentials_paginated(page=1, page_size=1)
|
db = get_db()
|
||||||
stats["credential_count"] = cred_result["pagination"]["total"]
|
stats = db.get_dashboard_counts()
|
||||||
|
suspicious = db.get_recent_suspicious(limit=10)
|
||||||
|
cred_result = db.get_credentials_paginated(page=1, page_size=1)
|
||||||
|
stats["credential_count"] = cred_result["pagination"]["total"]
|
||||||
|
|
||||||
templates = get_templates()
|
templates = get_templates()
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from fastapi.responses import HTMLResponse
|
|||||||
|
|
||||||
from dependencies import get_db, get_templates
|
from dependencies import get_db, get_templates
|
||||||
from routes.api import verify_auth
|
from routes.api import verify_auth
|
||||||
|
from dashboard_cache import get_cached, is_warm
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -58,10 +59,15 @@ async def htmx_top_ips(
|
|||||||
sort_by: str = Query("count"),
|
sort_by: str = Query("count"),
|
||||||
sort_order: str = Query("desc"),
|
sort_order: str = Query("desc"),
|
||||||
):
|
):
|
||||||
db = get_db()
|
# Serve from cache on default first-page request
|
||||||
result = db.get_top_ips_paginated(
|
cached = get_cached("top_ips") if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm()) else None
|
||||||
page=max(1, page), page_size=8, sort_by=sort_by, sort_order=sort_order
|
if cached:
|
||||||
)
|
result = cached
|
||||||
|
else:
|
||||||
|
db = get_db()
|
||||||
|
result = db.get_top_ips_paginated(
|
||||||
|
page=max(1, page), page_size=8, sort_by=sort_by, sort_order=sort_order
|
||||||
|
)
|
||||||
|
|
||||||
templates = get_templates()
|
templates = get_templates()
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
@@ -87,10 +93,14 @@ async def htmx_top_paths(
|
|||||||
sort_by: str = Query("count"),
|
sort_by: str = Query("count"),
|
||||||
sort_order: str = Query("desc"),
|
sort_order: str = Query("desc"),
|
||||||
):
|
):
|
||||||
db = get_db()
|
cached = get_cached("top_paths") if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm()) else None
|
||||||
result = db.get_top_paths_paginated(
|
if cached:
|
||||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
result = cached
|
||||||
)
|
else:
|
||||||
|
db = get_db()
|
||||||
|
result = db.get_top_paths_paginated(
|
||||||
|
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||||
|
)
|
||||||
|
|
||||||
templates = get_templates()
|
templates = get_templates()
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
@@ -116,10 +126,14 @@ async def htmx_top_ua(
|
|||||||
sort_by: str = Query("count"),
|
sort_by: str = Query("count"),
|
||||||
sort_order: str = Query("desc"),
|
sort_order: str = Query("desc"),
|
||||||
):
|
):
|
||||||
db = get_db()
|
cached = get_cached("top_ua") if (page == 1 and sort_by == "count" and sort_order == "desc" and is_warm()) else None
|
||||||
result = db.get_top_user_agents_paginated(
|
if cached:
|
||||||
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
result = cached
|
||||||
)
|
else:
|
||||||
|
db = get_db()
|
||||||
|
result = db.get_top_user_agents_paginated(
|
||||||
|
page=max(1, page), page_size=5, sort_by=sort_by, sort_order=sort_order
|
||||||
|
)
|
||||||
|
|
||||||
templates = get_templates()
|
templates = get_templates()
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
|
|||||||
68
src/tasks/dashboard_warmup.py
Normal file
68
src/tasks/dashboard_warmup.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# tasks/dashboard_warmup.py
|
||||||
|
|
||||||
|
"""
|
||||||
|
Pre-computes all Overview tab data and stores it in the in-memory cache.
|
||||||
|
This keeps SQLite page buffers warm and lets the dashboard respond instantly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from logger import get_app_logger
|
||||||
|
from database import get_database
|
||||||
|
from dashboard_cache import set_cached
|
||||||
|
|
||||||
|
app_logger = get_app_logger()
|
||||||
|
|
||||||
|
# ----------------------
|
||||||
|
# TASK CONFIG
|
||||||
|
# ----------------------
|
||||||
|
TASK_CONFIG = {
|
||||||
|
"name": "dashboard-warmup",
|
||||||
|
"cron": "*/1 * * * *",
|
||||||
|
"enabled": True,
|
||||||
|
"run_when_loaded": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------
|
||||||
|
# TASK LOGIC
|
||||||
|
# ----------------------
|
||||||
|
def main():
|
||||||
|
"""
|
||||||
|
Refresh the in-memory dashboard cache with current Overview data.
|
||||||
|
TasksMaster will call this function based on the cron schedule.
|
||||||
|
"""
|
||||||
|
task_name = TASK_CONFIG.get("name")
|
||||||
|
app_logger.info(f"[Background Task] {task_name} starting...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
db = get_database()
|
||||||
|
|
||||||
|
# --- Server-rendered data (stats cards + suspicious table) ---
|
||||||
|
stats = db.get_dashboard_counts()
|
||||||
|
|
||||||
|
cred_result = db.get_credentials_paginated(page=1, page_size=1)
|
||||||
|
stats["credential_count"] = cred_result["pagination"]["total"]
|
||||||
|
|
||||||
|
suspicious = db.get_recent_suspicious(limit=10)
|
||||||
|
|
||||||
|
# --- HTMX Overview tables (first page, default sort) ---
|
||||||
|
top_ips = db.get_top_ips_paginated(page=1, page_size=8)
|
||||||
|
top_ua = db.get_top_user_agents_paginated(page=1, page_size=5)
|
||||||
|
top_paths = db.get_top_paths_paginated(page=1, page_size=5)
|
||||||
|
|
||||||
|
# --- Map data (default: top 100 IPs by total_requests) ---
|
||||||
|
map_ips = db.get_all_ips_paginated(
|
||||||
|
page=1, page_size=100, sort_by="total_requests", sort_order="desc"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Store everything in the cache (overwrites previous values)
|
||||||
|
set_cached("stats", stats)
|
||||||
|
set_cached("suspicious", suspicious)
|
||||||
|
set_cached("top_ips", top_ips)
|
||||||
|
set_cached("top_ua", top_ua)
|
||||||
|
set_cached("top_paths", top_paths)
|
||||||
|
set_cached("map_ips", map_ips)
|
||||||
|
|
||||||
|
app_logger.info(f"[Background Task] {task_name} cache refreshed successfully.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
app_logger.error(f"[Background Task] {task_name} failed: {e}")
|
||||||
Reference in New Issue
Block a user