feat: UI modernization phases 1B-5, KPI dashboard, charts, and ADB fix

- Phase 1B: replace all emojis with inline SVG icons (Lucide-based, 45+ icons)
- Phase 2: KPI cards for compliance reports (score, passed, failed, total)
- Phase 3: Chart.js integration (donut distribution + horizontal bar by section)
- Phase 4: SVG gauge/speedometer for compliance score with gradient arc
- Phase 5: polish animations (fade-in stagger, smooth theme transition, hover effects, skeleton loading, alert slide-in, button press, dropdown animation)
- New endpoint GET /api/reports/{rid}/summary for KPI data extraction
- Fix ADB wallet connection (DPY-6005) by always passing wallet_password
- Remove CIS_EMBEDDINGS default from ADB config
- Custom brain icon (Lucide) for GenAI section
This commit is contained in:
nogueiraguh
2026-03-10 00:08:28 -03:00
parent f205f819ab
commit 4fc458cdcb
2 changed files with 396 additions and 136 deletions

View File

@@ -231,7 +231,7 @@ def init_db():
password_enc TEXT NOT NULL,
wallet_dir TEXT,
wallet_password_enc TEXT,
table_name TEXT DEFAULT 'CIS_EMBEDDINGS',
table_name TEXT DEFAULT '',
use_mtls INTEGER DEFAULT 1,
is_active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
@@ -2099,8 +2099,7 @@ def _get_adb_connection(cfg: dict):
if cfg["use_mtls"] and cfg.get("wallet_dir"):
params["config_dir"] = cfg["wallet_dir"]
params["wallet_location"] = cfg["wallet_dir"]
if cfg.get("wallet_password_enc"):
params["wallet_password"] = _dec(cfg["wallet_password_enc"])
params["wallet_password"] = _dec(cfg["wallet_password_enc"]) if cfg.get("wallet_password_enc") else ""
return oracledb.connect(**params)
def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str) -> list:
@@ -2129,7 +2128,7 @@ def _embed_text(text: str, genai_cfg: dict, embedding_model_id: str) -> list:
def _vector_search(cfg: dict, query_embedding: list, top_k: int = 5, table_name: str = None) -> list:
"""Search ADB vector store using cosine similarity. Returns top-K documents."""
import array
table_name = table_name or cfg.get("table_name", "CIS_EMBEDDINGS")
table_name = table_name or cfg.get("table_name", "")
conn = _get_adb_connection(cfg)
try:
cur = conn.cursor()
@@ -2423,7 +2422,7 @@ async def parse_wallet(wallet: UploadFile = File(...), u=Depends(require("admin"
async def save_adb(
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
password: str = Form(...), wallet_password: str = Form(""),
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
table_name: str = Form(""), use_mtls: str = Form("true"),
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-v4.0"),
wallet: Optional[UploadFile] = File(None),
u=Depends(require("admin","user"))
@@ -2513,7 +2512,7 @@ async def update_adb(
vid: str,
config_name: str = Form(...), dsn: str = Form(...), username: str = Form(...),
password: str = Form(""), wallet_password: str = Form(""),
table_name: str = Form("CIS_EMBEDDINGS"), use_mtls: str = Form("true"),
table_name: str = Form(""), use_mtls: str = Form("true"),
genai_config_id: str = Form(""), embedding_model_id: str = Form("cohere.embed-v4.0"),
wallet: Optional[UploadFile] = File(None),
u=Depends(require("admin","user"))
@@ -2625,7 +2624,7 @@ def _ingest_documents_task(cfg: dict, genai_cfg: dict, documents: list, user_id:
"""Background task: embed and insert documents into ADB via OCI GenAI."""
import array
emb_model = cfg.get("embedding_model_id", "cohere.embed-v4.0")
table_name = table_name or cfg.get("table_name", "CIS_EMBEDDINGS")
table_name = table_name or cfg.get("table_name", "")
conn = _get_adb_connection(cfg)
try:
cur = conn.cursor()
@@ -2789,7 +2788,8 @@ async def list_embeddings(vid: str, table_name: str = Query(""), limit: int = Qu
try:
conn = _get_adb_connection(dict(cfg))
cur = conn.cursor()
table_name = table_name.strip() or cfg["table_name"] or "CIS_EMBEDDINGS"
table_name = table_name.strip() or cfg["table_name"]
if not table_name: raise HTTPException(400, "Nenhuma tabela selecionada")
cur.execute(f"SELECT COUNT(*) FROM {table_name}")
total = cur.fetchone()[0]
cur.execute(f"""
@@ -2814,7 +2814,8 @@ async def delete_embedding(vid: str, doc_id: str, table_name: str = Query(""), u
try:
conn = _get_adb_connection(dict(cfg))
cur = conn.cursor()
table_name = table_name.strip() or cfg["table_name"] or "CIS_EMBEDDINGS"
table_name = table_name.strip() or cfg["table_name"]
if not table_name: raise HTTPException(400, "Nenhuma tabela selecionada")
cur.execute(f"DELETE FROM {table_name} WHERE ID = :1", [doc_id])
conn.commit()
cur.close(); conn.close()
@@ -2972,6 +2973,55 @@ async def get_report(rid, u=Depends(current_user)):
if u["role"]!="admin" and r["user_id"]!=u["id"]: raise HTTPException(403)
return dict(r)
@app.get("/api/reports/{rid}/summary")
async def report_summary(rid: str, u=Depends(current_user)):
with db() as c:
r = c.execute("SELECT user_id,json_path,tenancy_name,level,created_at FROM reports WHERE id=? AND status='completed'", (rid,)).fetchone()
if not r: raise HTTPException(404)
if u["role"] != "admin" and r["user_id"] != u["id"]: raise HTTPException(403)
jp = r["json_path"]
if not jp or not Path(jp).exists():
raise HTTPException(400, "Report JSON not found")
data = json.loads(Path(jp).read_text())
total = passed = failed = 0
sections = {}
total_findings = 0
total_compliant = 0
# Format: list of checks with Compliant=Yes/No, Section, Findings, Compliant Items, Total
checks = data if isinstance(data, list) else data.get("findings", [])
if isinstance(checks, dict):
checks = list(checks.values())
for check in checks:
total += 1
compliant = str(check.get("Compliant", check.get("status", ""))).strip().lower()
is_pass = compliant in ("yes", "pass")
if is_pass:
passed += 1
else:
failed += 1
fi = str(check.get("Findings", "0")).strip()
ci = str(check.get("Compliant Items", "0")).strip()
total_findings += int(fi) if fi.isdigit() else 0
total_compliant += int(ci) if ci.isdigit() else 0
sec = check.get("Section", check.get("section", "Other"))
s = sections.setdefault(sec, {"passed": 0, "failed": 0, "total": 0})
s["total"] += 1
if is_pass:
s["passed"] += 1
else:
s["failed"] += 1
score = round((passed / total * 100), 1) if total else 0
return {
"tenancy": data.get("tenancy", r["tenancy_name"]) if isinstance(data, dict) else r["tenancy_name"],
"level": r["level"] or 2,
"regions": data.get("regions", []) if isinstance(data, dict) else [],
"generated_at": r["created_at"],
"total": total, "passed": passed, "failed": failed,
"total_findings": total_findings, "total_compliant": total_compliant,
"score": score,
"sections": sections
}
@app.get("/api/reports/{rid}/files")
async def list_report_files(rid: str, u=Depends(current_user)):
with db() as c:
@@ -3105,7 +3155,7 @@ async def _chat_background(mid: str, sid: str, msg: ChatMsg, user: dict, genai_c
query_embedding = _embed_text(msg.message, dict(emb_genai), emb_model)
tables = _get_tables_for_config(adb_cfg["id"], active_only=True)
if not tables:
tables = [{"table_name": adb_cfg.get("table_name", "CIS_EMBEDDINGS")}]
tables = [{"table_name": adb_cfg.get("table_name", "")}]
for tbl in tables:
try:
documents = _vector_search(adb_cfg, query_embedding, top_k=5, table_name=tbl["table_name"])