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:
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user