"""관측소 대시보드 — index.db 현재 상태를 담아 서버가 직접 서빙한다 (GET /dashboard). 정적 아티팩트 스냅샷과 같은 화면이며, 요청 시점마다 DB를 다시 읽어 HTML에 주입한다. 템플릿: query/dashboard.html (플레이스홀더 __DATA_JSON__, __GENERATED_AT__) """ from __future__ import annotations import json from datetime import datetime from pathlib import Path from config.settings import settings from index.db import connect, loads TEMPLATE = Path(__file__).with_name("dashboard.html") ELIGIBLE = "('FORM','METHOD','FUNCTION','MODULE','EVENT')" def _rows(con, sql: str, params: tuple = ()) -> list[dict]: return [dict(r) for r in con.execute(sql, params)] def build_data(con, running: list[str]) -> dict: counts = { t: con.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0] for t in ("program", "include", "unit", "symbol", "symbol_write", "symbol_read", "table_ref", "call_edge", "topo") } counts["table_read"] = con.execute("SELECT COUNT(*) FROM table_ref WHERE mode='read'").fetchone()[0] counts["table_write"] = con.execute("SELECT COUNT(*) FROM table_ref WHERE mode='write'").fetchone()[0] unit_counts = {r["program"]: r["n"] for r in con.execute( "SELECT program, COUNT(*) n FROM unit GROUP BY program")} chunk_counts = {r["program"]: r["n"] for r in con.execute( "SELECT program, COUNT(*) n FROM logic_chunk GROUP BY program")} counts["logic_chunk"] = sum(chunk_counts.values()) programs = [ {"name": r["name"], "devclass": r["devclass"], "title": r["title_ko"], "units": unit_counts.get(r["name"], 0), "chunks": chunk_counts.get(r["name"], 0)} for r in con.execute("SELECT name, devclass, title_ko FROM program ORDER BY name") ] return { "counts": counts, "programs": programs, "includes": _rows(con, "SELECT program, include, line_count FROM include ORDER BY program, include"), "units": _rows(con, "SELECT unit_id, program, include, unit_type, name, line_start, line_end, loc FROM unit"), "edges": _rows(con, "SELECT program, from_unit, to_unit, external_name, call_type, line FROM call_edge"), "table_refs": _rows(con, "SELECT program, unit_id, table_name, mode FROM table_ref"), "rw": _rows(con, """ SELECT program, symbol, SUM(w) w, SUM(r) r FROM ( SELECT program, symbol, COUNT(*) w, 0 r FROM symbol_write GROUP BY program, symbol UNION ALL SELECT program, symbol, 0 w, COUNT(*) r FROM symbol_read GROUP BY program, symbol ) GROUP BY program, symbol ORDER BY (SUM(w)+SUM(r)) DESC"""), "summaries": { r["unit_id"]: {k: (loads(r["summary_json"]) or {}).get(k) for k in ("purpose_ko", "chunk_count", "coverage")} for r in con.execute("SELECT unit_id, summary_json FROM unit WHERE summary_status='done'") }, "chunks": _rows(con, "SELECT chunk_id, program, unit_id, include, line_start, line_end, kind, " "purpose_ko, confidence FROM logic_chunk ORDER BY program, include, line_start"), "llm": { # 설정된 모델과 **실제로 쓰인 백엔드**를 구분해 보여준다 — file 백엔드로 돌린 결과를 # 쓰지도 않은 모델 이름으로 표기하면 관측 결과가 거짓이 된다. "model": settings.llm_model, "backend": settings.summarize_backend, "api_key_set": bool(settings.llm_base_url and settings.llm_api_key), "last_used_model": (con.execute( "SELECT model FROM llm_usage_log ORDER BY id DESC LIMIT 1").fetchone() or [None])[0], "running": running, "progress": _rows(con, f""" SELECT program, COUNT(*) eligible, SUM(CASE WHEN summary_status='done' THEN 1 ELSE 0 END) done, SUM(CASE WHEN summary_status='failed' THEN 1 ELSE 0 END) failed, COALESCE(SUM(chunk_count),0) chunks FROM unit WHERE unit_type IN {ELIGIBLE} GROUP BY program ORDER BY program"""), "failures": _rows(con, """ SELECT program, unit_id, include, unit_type, name, summary_error FROM unit WHERE summary_status='failed' ORDER BY program, include, line_start"""), "totals": dict(con.execute( "SELECT COUNT(*) runs, COALESCE(SUM(calls),0) calls, " "COALESCE(SUM(prompt_tokens),0) pt, COALESCE(SUM(completion_tokens),0) ct, " "COALESCE(SUM(cost_usd),0) cost FROM llm_usage_log").fetchone()), "log": _rows(con, "SELECT * FROM llm_usage_log ORDER BY id DESC LIMIT 50"), }, } def render_dashboard(running: list[str] | None = None) -> str: con = connect() try: data = build_data(con, running or []) finally: con.close() payload = json.dumps(data, ensure_ascii=False).replace(" None: """관측소를 단일 HTML 파일로 내보낸다 — 다른 PC 로 옮겨 브라우저로만 열어보는 용도. python -m query.dashboard [--out PATH] 데이터가 파일 안에 박히므로 파이썬·서버·index.db 없이 열린다 (뷰어 스냅샷과 같은 방식). """ import argparse ap = argparse.ArgumentParser(description="관측소 대시보드 HTML 스냅샷") ap.add_argument("--out", default=None) args = ap.parse_args() out = Path(args.out) if args.out else settings.data_raw.parent / "dashboard.html" out.parent.mkdir(parents=True, exist_ok=True) html = render_dashboard([]) out.write_text(html, encoding="utf-8") print(f"{out} ({len(html) / 1024:.0f} KB)") if __name__ == "__main__": main()