guardia-itsm/routers/growth_dashboard.py

105 lines
3.7 KiB
Python

"""성장일지 — 기능 성장 대시보드 (라우터 수·SR 처리·자립도 추이)."""
from __future__ import annotations
import logging
from datetime import datetime, timedelta
from pathlib import Path
from fastapi import APIRouter, Depends
from sqlalchemy import select, func as sqlfunc, desc
from sqlalchemy.ext.asyncio import AsyncSession
from core.auth import get_current_user
from database import get_db
from models import User, ChangelogEntry, HealthCheckResult, SelfReport, IndependenceScore
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/growth", tags=["성장일지-대시보드"])
ITSM_ROUTER_DIR = Path("/opt/guardia/workspace/guardia-itsm/routers")
def _count_routers() -> int:
if not ITSM_ROUTER_DIR.exists():
return 0
return len([f for f in ITSM_ROUTER_DIR.glob("*.py") if not f.name.startswith("_")])
@router.get("/dashboard")
async def growth_dashboard(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""GUARDiA 성장 현황 종합 대시보드."""
router_count = _count_routers()
# 최근 건강검진 결과
hc_row = await db.execute(
select(HealthCheckResult).order_by(desc(HealthCheckResult.created_at)).limit(1)
)
last_hc = hc_row.scalar_one_or_none()
# 최근 자립도 점수
score_row = await db.execute(
select(IndependenceScore)
.where(IndependenceScore.dimension == "overall")
.order_by(desc(IndependenceScore.measured_at))
.limit(1)
)
last_score = score_row.scalar_one_or_none()
# 변경이력 30일 집계
thirty_days_ago = datetime.utcnow() - timedelta(days=30)
cl_count = await db.execute(
select(sqlfunc.count()).select_from(ChangelogEntry)
.where(ChangelogEntry.created_at >= thirty_days_ago)
)
# 주간 보고서 수
report_count = await db.execute(
select(sqlfunc.count()).select_from(SelfReport)
)
return {
"router_count": router_count,
"estimated_endpoints": router_count * 7,
"health": {
"last_check": last_hc.created_at if last_hc else None,
"status": "HEALTHY" if (last_hc and last_hc.success) else "UNKNOWN",
"passed": last_hc.passed if last_hc else 0,
"total": last_hc.total if last_hc else 69,
},
"independence": {
"score": last_score.score if last_score else 30.0,
"target": last_score.target_score if last_score else 85.0,
"dimension": last_score.dimension if last_score else "overall",
},
"changelog_30d": cl_count.scalar() or 0,
"weekly_reports": report_count.scalar() or 0,
"milestones": [
{"label": "라우터 100개", "achieved": router_count >= 100},
{"label": "자립도 50%", "achieved": (last_score.score if last_score else 0) >= 50},
{"label": "자립도 70%", "achieved": (last_score.score if last_score else 0) >= 70},
{"label": "자립도 85%", "achieved": (last_score.score if last_score else 0) >= 85},
],
"generated_at": datetime.utcnow().isoformat(),
}
@router.get("/timeline")
async def growth_timeline(
days: int = 90,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""날짜별 변경이력 타임라인."""
since = datetime.utcnow() - timedelta(days=days)
rows = await db.execute(
select(ChangelogEntry).where(ChangelogEntry.created_at >= since)
.order_by(ChangelogEntry.created_at)
.limit(200)
)
return [{
"date": e.created_at.strftime("%Y-%m-%d"),
"category": e.category,
"title": e.title,
"author": e.author,
} for e in rows.scalars().all()]