feat(phase-33): final polish V3 - navigation, performance, and documentation (T-238 to T-240)
This commit is contained in:
@@ -27,8 +27,10 @@ def operational_metrics(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get all operational metrics (MTTD, MTTR, Detection Efficacy, etc.)."""
|
||||
return get_all_operational_metrics(db)
|
||||
"""Get all operational metrics (MTTD, MTTR, etc.) — cached for 5 min."""
|
||||
from app.services.score_cache import get_operational_metrics_cached
|
||||
|
||||
return get_operational_metrics_cached(db)
|
||||
|
||||
|
||||
# ── GET /metrics/operational/trend ────────────────────────────────────
|
||||
|
||||
@@ -93,8 +93,10 @@ def score_organization(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Get the overall organization security score."""
|
||||
return calculate_organization_score(db)
|
||||
"""Get the overall organization security score (cached for 5 min)."""
|
||||
from app.services.score_cache import get_organization_score_cached
|
||||
|
||||
return get_organization_score_cached(db)
|
||||
|
||||
|
||||
# ── GET /scores/history ──────────────────────────────────────────────
|
||||
@@ -170,6 +172,10 @@ def update_scoring_config(
|
||||
if payload.platform_diversity is not None:
|
||||
settings.SCORING_WEIGHT_PLATFORM_DIVERSITY = payload.platform_diversity
|
||||
|
||||
# Weights changed — bust the score cache
|
||||
from app.services.score_cache import invalidate
|
||||
invalidate()
|
||||
|
||||
return {
|
||||
"message": "Scoring config updated",
|
||||
"weights": {
|
||||
|
||||
84
backend/app/services/score_cache.py
Normal file
84
backend/app/services/score_cache.py
Normal file
@@ -0,0 +1,84 @@
|
||||
"""In-memory TTL cache for expensive scoring and metrics calculations.
|
||||
|
||||
The cache is a simple dict with timestamps. It is invalidated when tests
|
||||
are validated, scores change, or an explicit ``invalidate`` call is made.
|
||||
|
||||
Thread-safe: each worker process has its own dict, and the TTL ensures
|
||||
stale data does not persist longer than ``CACHE_TTL`` seconds.
|
||||
"""
|
||||
|
||||
import time
|
||||
from typing import Any, Optional
|
||||
|
||||
CACHE_TTL = 300 # 5 minutes
|
||||
|
||||
_cache: dict[str, dict[str, Any]] = {}
|
||||
|
||||
|
||||
def get(key: str) -> Optional[Any]:
|
||||
"""Return cached value if present and not expired, else None."""
|
||||
entry = _cache.get(key)
|
||||
if entry is None:
|
||||
return None
|
||||
if time.time() - entry["ts"] > CACHE_TTL:
|
||||
_cache.pop(key, None)
|
||||
return None
|
||||
return entry["data"]
|
||||
|
||||
|
||||
def put(key: str, data: Any) -> None:
|
||||
"""Store *data* under *key* with the current timestamp."""
|
||||
_cache[key] = {"data": data, "ts": time.time()}
|
||||
|
||||
|
||||
def invalidate(key: Optional[str] = None) -> None:
|
||||
"""Remove one key or clear the whole cache."""
|
||||
if key is None:
|
||||
_cache.clear()
|
||||
else:
|
||||
_cache.pop(key, None)
|
||||
|
||||
|
||||
# ── High-level helpers ────────────────────────────────────────────────
|
||||
|
||||
|
||||
def get_organization_score_cached(db):
|
||||
"""Cached wrapper around ``calculate_organization_score``."""
|
||||
from app.services.scoring_service import calculate_organization_score
|
||||
|
||||
cached = get("org_score")
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
result = calculate_organization_score(db)
|
||||
put("org_score", result)
|
||||
return result
|
||||
|
||||
|
||||
def get_operational_metrics_cached(db):
|
||||
"""Cached wrapper around operational metrics (MTTD, MTTR, efficacy)."""
|
||||
from app.services.operational_metrics_service import (
|
||||
calculate_mttd,
|
||||
calculate_mttr,
|
||||
calculate_detection_efficacy,
|
||||
calculate_alert_fidelity,
|
||||
calculate_coverage_velocity,
|
||||
calculate_validation_throughput,
|
||||
calculate_rejection_rate,
|
||||
)
|
||||
|
||||
cached = get("op_metrics")
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
result = {
|
||||
"mttd": calculate_mttd(db),
|
||||
"mttr": calculate_mttr(db),
|
||||
"detection_efficacy": calculate_detection_efficacy(db),
|
||||
"alert_fidelity": calculate_alert_fidelity(db),
|
||||
"coverage_velocity": calculate_coverage_velocity(db),
|
||||
"validation_throughput": calculate_validation_throughput(db),
|
||||
"rejection_rate": calculate_rejection_rate(db),
|
||||
}
|
||||
put("op_metrics", result)
|
||||
return result
|
||||
@@ -288,6 +288,12 @@ def check_dual_validation(db: Session, test: Test) -> Test:
|
||||
elif red_status == "approved" and blue_status == "approved":
|
||||
test.state = TestState.validated
|
||||
db.commit()
|
||||
# Invalidate cached scores — a validation changes org-level numbers
|
||||
try:
|
||||
from app.services.score_cache import invalidate
|
||||
invalidate()
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
notify_test_state_change(db, test, "validated")
|
||||
except Exception:
|
||||
|
||||
Reference in New Issue
Block a user