Initial commit: ABAP indexing pipeline (ingest, parser, summarize, index, query, wiki)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""관측소 대시보드 — 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()
|
||||
Reference in New Issue
Block a user