diff --git a/backend/app.py b/backend/app.py index f70c2f2..d908ee2 100644 --- a/backend/app.py +++ b/backend/app.py @@ -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"]) diff --git a/frontend/index.html b/frontend/index.html index f43cdba..2a2d92c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -83,9 +83,7 @@ body{font-family:var(--fs);background:var(--bg);color:var(--t1);height:100vh;ove .pc{padding:1.5rem;max-width:1400px;flex:1;overflow-y:auto;min-height:0} /* ── Cards ── */ -.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.35rem;margin-bottom:.85rem; - box-shadow:var(--sh1);transition:box-shadow .3s var(--trans)} -.cd:hover{box-shadow:var(--sh2)} +.cd{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1.35rem;margin-bottom:.85rem;box-shadow:var(--sh1)} .ct{font-weight:700;font-size:.92rem;margin-bottom:.85rem;display:flex;align-items:center;gap:.45rem;color:var(--t1);letter-spacing:-.01em} .cdesc{font-size:.78rem;color:var(--t3);margin-top:-.45rem;margin-bottom:.85rem;line-height:1.55} @@ -120,6 +118,25 @@ select{cursor:pointer;appearance:none;background-image:url("data:image/svg+xml,% .b-admin{background:var(--ppl);color:var(--pp)}.b-user{background:var(--bll);color:var(--bl)}.b-viewer{background:var(--bg3);color:var(--t3)} .b-on{background:var(--gnl);color:var(--gn)}.b-off{background:var(--bg3);color:var(--t4)} +/* ── KPI Cards ── */ +.kpi-row{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-bottom:.85rem} +.kpi{background:var(--bg1);border:1px solid var(--bd);border-radius:var(--rl);padding:1rem 1.15rem;box-shadow:var(--sh1);transition:all .3s var(--trans);position:relative;overflow:hidden} +.kpi:hover{box-shadow:var(--sh2);transform:translateY(-2px)} +.kpi::after{content:'';position:absolute;bottom:0;left:0;right:0;height:3px;border-radius:0 0 var(--rl) var(--rl)} +.kpi.kpi-score::after{background:var(--ac)}.kpi.kpi-pass::after{background:var(--gn)}.kpi.kpi-fail::after{background:var(--rd)}.kpi.kpi-warn::after{background:var(--yl)}.kpi.kpi-info::after{background:var(--bl)} +.kpi-label{font-size:.68rem;font-weight:600;color:var(--t3);text-transform:uppercase;letter-spacing:.06em;display:flex;align-items:center;gap:.35rem;margin-bottom:.45rem} +.kpi-val{font-size:1.65rem;font-weight:800;letter-spacing:-.03em;line-height:1;color:var(--t1);font-variant-numeric:tabular-nums} +.kpi-val.v-ok{color:var(--gn)}.kpi-val.v-err{color:var(--rd)}.kpi-val.v-warn{color:var(--yl)}.kpi-val.v-info{color:var(--bl)}.kpi-val.v-ac{color:var(--ac)} +.kpi-sub{font-size:.66rem;color:var(--t4);margin-top:.3rem;font-family:var(--fm);font-variant-numeric:tabular-nums} +.kpi-bar{height:6px;background:var(--bg3);border-radius:3px;margin-top:.55rem;overflow:hidden} +.kpi-bar-fill{height:100%;border-radius:3px;transition:width .6s ease} +.kpi-sections{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:.5rem;margin-top:.55rem} +.kpi-sec{background:var(--bg2);border:1px solid var(--bd);border-radius:10px;padding:.6rem .75rem;transition:all .2s} +.kpi-sec:hover{border-color:var(--bdf);box-shadow:var(--sh1)} +.kpi-sec-name{font-size:.68rem;font-weight:700;color:var(--t2);margin-bottom:.3rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.kpi-sec-row{display:flex;gap:.5rem;font-size:.64rem;font-family:var(--fm)} +.kpi-sec-row .s-ok{color:var(--gn)}.kpi-sec-row .s-fail{color:var(--rd)}.kpi-sec-row .s-rev{color:var(--yl)} + /* ── Tables ── */ table{width:100%;border-collapse:separate;border-spacing:0;font-size:.78rem} th{text-align:left;padding:.65rem .75rem;font-size:.66rem;text-transform:uppercase;letter-spacing:.08em;color:var(--t4); @@ -212,7 +229,6 @@ tbody tr:last-child td{border-bottom:none} .lc .st{color:var(--t4);font-size:.76rem;margin-top:.15rem;margin-bottom:1.35rem;font-weight:400} /* ── Alerts ── */ -.al{padding:.65rem .85rem;border-radius:10px;font-size:.78rem;margin-bottom:.75rem;font-weight:500} .al-e{background:var(--rdl);color:var(--rd);border:1px solid rgba(220,38,38,.1)} .al-s{background:var(--gnl);color:var(--gn);border:1px solid rgba(22,163,74,.1)} .al-i{background:var(--bll);color:var(--bl);border:1px solid rgba(37,99,235,.1)} @@ -258,13 +274,91 @@ tbody tr:last-child td{border-bottom:none} .exp-node-children{padding-left:.85rem;border-left:1px solid var(--bg4);margin-left:.65rem} /* ── Animations ── */ -@keyframes fi{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}} -.fi{animation:fi .35s var(--trans)} +@keyframes fi{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:translateY(0)}} +@keyframes fiScale{from{opacity:0;transform:scale(.96) translateY(6px)}to{opacity:1;transform:scale(1) translateY(0)}} +@keyframes slideIn{from{opacity:0;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}} +@keyframes slideUp{from{opacity:0;transform:translateY(12px)}to{opacity:1;transform:translateY(0)}} +@keyframes alertIn{from{opacity:0;transform:translateY(-6px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}} +@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}} @keyframes pu{0%,100%{opacity:1}50%{opacity:.4}} -.ld{animation:pu 1.2s ease infinite;color:var(--t4);font-size:.82rem;text-align:center;padding:1.5rem} @keyframes spin{to{transform:rotate(360deg)}} +@keyframes pulseGlow{0%,100%{box-shadow:0 0 0 0 rgba(199,70,52,.15)}50%{box-shadow:0 0 0 6px rgba(199,70,52,0)}} +.fi{animation:fi .35s var(--trans) both} +.ld{animation:pu 1.2s ease infinite;color:var(--t4);font-size:.82rem;text-align:center;padding:1.5rem} .spinner{display:inline-block;width:16px;height:16px;border:2px solid var(--bd);border-top-color:var(--ac);border-radius:50%;animation:spin .6s linear infinite} +/* Page content fade-in */ +.pc>.cd,.pc>.kpi-row,.pc>.exp-wrap,.pc>.ch-c,.pc>div{animation:fi .4s var(--trans) both} +.pc>.cd:nth-child(2),.pc>div:nth-child(2){animation-delay:.05s} +.pc>.cd:nth-child(3),.pc>div:nth-child(3){animation-delay:.1s} +.pc>.cd:nth-child(4),.pc>div:nth-child(4){animation-delay:.15s} + +/* Alert slide-in */ +.al{padding:.65rem .85rem;border-radius:10px;font-size:.78rem;margin-bottom:.75rem;font-weight:500;animation:alertIn .3s var(--trans) both} + +/* KPI card stagger */ +.kpi{animation:fiScale .4s var(--trans) both} +.kpi:nth-child(2){animation-delay:.06s} +.kpi:nth-child(3){animation-delay:.12s} +.kpi:nth-child(4){animation-delay:.18s} + +/* Enhanced card hover */ +.cd{transition:box-shadow .3s var(--trans),transform .3s var(--trans),border-color .3s var(--trans)} +.cd:hover{box-shadow:var(--sh2);transform:translateY(-1px);border-color:var(--bdf)} + +/* Button press effect */ +.btn{transition:all .2s var(--trans);position:relative;overflow:hidden} +.btn:active:not(:disabled){transform:scale(.97)} +.bp:active:not(:disabled){transform:scale(.97);box-shadow:0 1px 4px rgba(199,70,52,.2)} + +/* Skeleton loading */ +.skel{background:linear-gradient(90deg,var(--bg3) 25%,var(--bg4) 50%,var(--bg3) 75%);background-size:200% 100%;animation:shimmer 1.5s ease infinite;border-radius:8px} + +/* Badge hover */ +.badge{transition:transform .15s var(--trans),box-shadow .15s var(--trans)} +.badge:hover{transform:scale(1.05)} + +/* Table row slide-in */ +tbody tr{animation:fi .3s var(--trans) both} +tbody tr:nth-child(2){animation-delay:.03s} +tbody tr:nth-child(3){animation-delay:.06s} +tbody tr:nth-child(4){animation-delay:.09s} +tbody tr:nth-child(5){animation-delay:.12s} + +/* Explorer card stagger */ +.exp-c{animation:fiScale .3s var(--trans) both} +.exp-c:nth-child(2){animation-delay:.04s} +.exp-c:nth-child(3){animation-delay:.08s} +.exp-c:nth-child(4){animation-delay:.12s} +.exp-c:nth-child(5){animation-delay:.16s} +.exp-c:nth-child(6){animation-delay:.2s} + +/* Nav item hover glow */ +.ni:hover{background:var(--bg2);color:var(--t2);transform:translateX(2px)} +.ni.on{animation:slideIn .3s var(--trans) both} + +/* Smooth theme transition */ +html{transition:background .4s ease,color .3s ease} +html *{transition-property:background-color,border-color,color,box-shadow,opacity;transition-duration:.25s;transition-timing-function:var(--trans)} +html input,html select,html textarea,html button,html .btn,html .cd,html .kpi,html .exp-c,html .ni,html .badge,html .lo-btn,html .ch-hp,html .ch-side{transition-duration:.25s} +html .spinner,html [class*="kpi-bar-fill"],html canvas{transition:none!important} + +/* Chat bubble animation */ +.ch-msg{animation:fi .3s var(--trans) both} + +/* Dropdown animation */ +.mdrop-dd,.mdrop-list{animation:fiScale .2s var(--trans) both} + +/* Empty state subtle bounce */ +.emp .eic{animation:fi .5s var(--trans) both} + +/* Active tab glow */ +.exp-cat.active,.exp-tab.active{transition:all .2s var(--trans)} + +/* Theme toggle spin */ +.lo-btn svg{transition:transform .4s var(--trans)} +.lo-btn:hover svg{transform:rotate(25deg)} + /* ── Scrollbar ── */ ::-webkit-scrollbar{width:6px} ::-webkit-scrollbar-track{background:transparent} @@ -359,6 +453,7 @@ tbody tr:last-child td{border-bottom:none} .tf-bottom{min-height:0} } +
@@ -367,6 +462,61 @@ const V='1.1'; // Theme: restore from localStorage, default to light (function(){const t=localStorage.getItem('theme')||'light';document.documentElement.className=t})(); function toggleTheme(){const h=document.documentElement;const isDark=h.classList.contains('dark');h.className=isDark?'light':'dark';localStorage.setItem('theme',isDark?'light':'dark');R()} +// SVG Icon system (Lucide-based, 16x16, currentColor) +const _i=(d,s=16)=>``; +const IC={ + chat:_i('Inicie uma conversa com o agente.
Selecione um modelo para começar.
Inicie uma conversa com o agente.
Selecione um modelo para começar.
$1').replace(/\n/g,'