Files
ABAP-Indexing/query/dashboard.py
T

124 lines
5.9 KiB
Python

"""관측소 대시보드 — 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("</", "<\\/")
return (TEMPLATE.read_text(encoding="utf-8")
.replace("__DATA_JSON__", payload)
.replace("__GENERATED_AT__", datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
def main() -> 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()