/
ncit
/
coderagsystem
Обзор
Документация
Войти
/
ncit
/
coderagsystem
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
codex/rag-codegraph-patterns
src/rag/web/index.html
811 строк
31 KB
ncit
Add persistent RAG jobs and Qdrant benchmarking
06 июн 2026, 00:20
06 июн 2026, 00:20
03503e2
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>RAG · Dashboard</title> <link rel="preconnect" href="https://fonts.googleapis.com" /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" /> <style> /* --- design tokens lifted from ragsystem.pen --- */ :root { --bg: #0B0F14; --panel: #10161D; --panel2: #141B23; --border: #1E2731; --border-strong: #2A3441; --fg: #C9D1D9; --fg-bright: #E6EDF3; --dim: #6B7785; --muted: #4A5462; --accent: #5EE6D0; /* mint — primary */ --accent-dim: #2F8E80; --violet: #B084EB; /* embedder / strategy */ --blue: #7FB6F0; /* generator */ --ok: #7FD18B; /* uptime / success */ --warn: #E0B466; --err: #E5687A; --mono: "JetBrains Mono", ui-monospace, "Geist Mono", "SF Mono", Menlo, monospace; --sans: "Inter", system-ui, sans-serif; --r-sm: 4px; --r-md: 6px; --r-lg: 8px; } * { box-sizing: border-box; margin: 0; padding: 0; } html, body { height: 100%; } body { background: #05080b; color: var(--fg); font-family: var(--mono); font-size: 13px; -webkit-font-smoothing: antialiased; display: flex; align-items: center; justify-content: center; padding: 24px; } /* --- terminal window --- */ #app { width: 100%; max-width: 1200px; height: 100%; max-height: 820px; background: var(--bg); border: 1px solid var(--border-strong); border-radius: 10px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 24px 80px rgba(0,0,0,.55); } .title { display: flex; align-items: center; gap: 12px; padding: 14px 18px 10px; } .title .caret { color: var(--accent); font-size: 14px; } .title h1 { font-family: var(--sans); font-size: 18px; font-weight: 600; color: var(--fg-bright); } .title .screen-of { color: var(--dim); font-size: 12px; } /* --- chrome bar --- */ .chrome { display: flex; align-items: center; gap: 10px; height: 28px; padding: 0 14px; background: var(--panel2); border-bottom: 1px solid var(--border); } .lights { letter-spacing: 2px; color: var(--muted); } .chrome .cwd { color: var(--dim); font-size: 12px; } .chrome .dims { margin-left: auto; color: var(--muted); font-size: 11px; } /* --- status strip --- */ .status { display: flex; align-items: center; gap: 16px; height: 24px; padding: 0 14px; background: var(--panel2); border-bottom: 1px solid var(--border); font-size: 11px; color: var(--dim); } .status .seg { display: flex; align-items: center; gap: 6px; white-space: nowrap; } .status .seg .val { color: var(--fg); } .status .dot { font-size: 11px; } .status .clock { margin-left: auto; color: var(--muted); } /* --- body: sidebar + main --- */ .body { flex: 1; display: flex; min-height: 0; } .sidebar { width: 200px; flex-shrink: 0; background: var(--panel); border-right: 1px solid var(--border); padding: 12px; display: flex; flex-direction: column; gap: 4px; overflow: hidden; } .sb-label { color: var(--muted); font-size: 10px; letter-spacing: 1px; margin: 4px 0; } .nav-item { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 8px; border-radius: var(--r-sm); color: var(--fg); cursor: pointer; user-select: none; } .nav-item .icon { color: var(--accent); width: 14px; text-align: center; } .nav-item .num { margin-left: auto; color: var(--muted); font-size: 11px; } .nav-item:hover { background: #11181f; } .nav-item.active { background: var(--panel2); } .nav-item.active .label { color: var(--fg-bright); } .nav-item.disabled { opacity: .45; cursor: default; } .nav-item.disabled:hover { background: transparent; } .sb-div { height: 1px; background: var(--border); margin: 6px 0; } .repo-item { display: flex; align-items: center; gap: 8px; height: 22px; padding: 0 8px; } .repo-item .dot { color: var(--accent); font-size: 11px; } .repo-item .label { font-size: 12px; } /* --- main content --- */ .main { flex: 1; background: var(--bg); padding: 12px 14px; display: flex; flex-direction: column; gap: 10px; min-width: 0; overflow: hidden; } /* a routed screen fills the main content area */ .screen { flex: 1; display: flex; flex-direction: column; gap: 10px; min-height: 0; min-width: 0; } /* KPI cards */ .kpis { display: flex; gap: 10px; height: 110px; } .kpi { flex: 1; min-width: 0; background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md); padding: 12px 14px; display: flex; flex-direction: column; gap: 6px; } .kpi .k-label { color: var(--muted); font-size: 10px; letter-spacing: 1px; } .kpi .k-value { font-size: 20px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .kpi .k-sub { color: var(--dim); font-size: 11px; } .kpi.index .k-value { color: var(--accent); } .kpi.embed .k-value { color: var(--violet); } .kpi.gen .k-value { color: var(--blue); } .kpi.up .k-value { color: var(--ok); } /* generic panel */ .panel { background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-md); display: flex; flex-direction: column; min-height: 0; overflow: hidden; } .panel-head { display: flex; align-items: center; gap: 8px; height: 26px; padding: 0 12px; background: var(--panel2); border-bottom: 1px solid var(--border); color: var(--dim); font-size: 11px; flex-shrink: 0; } .panel-head .ph-title::before { content: "┌─ "; color: var(--dim); } .panel-head .ph-right { margin-left: auto; color: var(--muted); } .panel-body { padding: 10px 12px; overflow-y: auto; flex: 1; min-height: 0; } /* home layout */ .home-split { flex: 1; display: flex; gap: 10px; min-height: 0; } .home-left { flex: 1; min-width: 0; display: flex; } .home-right { width: 280px; flex-shrink: 0; display: flex; } /* sparkline */ .spark { display: flex; align-items: flex-end; gap: 2px; height: 56px; margin-bottom: 4px; } .spark .bar { flex: 1; background: var(--accent); border-radius: 1px 1px 0 0; min-height: 2px; opacity: .9; } .spark-axis { display: flex; justify-content: space-between; color: var(--muted); font-size: 10px; margin-bottom: 8px; } .hr { height: 1px; background: var(--border); margin: 8px 0; } .sub-label { color: var(--muted); font-size: 10px; letter-spacing: 1px; margin-bottom: 6px; } /* query rows */ .qrow { display: flex; align-items: center; gap: 10px; padding: 3px 0; } .qrow .qtime { color: var(--muted); font-size: 11px; flex-shrink: 0; width: 38px; } .qrow .qtext { color: var(--fg); font-size: 11px; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .qrow .qmeta { display: flex; gap: 10px; flex-shrink: 0; } .qrow .qmeta .strat { color: var(--violet); font-size: 10px; } .qrow .qmeta .score { color: var(--accent); font-size: 11px; } /* list rows (plugins / collections) */ .lrow { display: flex; align-items: center; gap: 8px; padding: 3px 0; font-size: 11px; } .lrow .dot { font-size: 10px; } .lrow .dot.on { color: var(--ok); } .lrow .dot.off { color: var(--muted); } .lrow .arrow { color: var(--accent-dim); font-size: 10px; } .lrow .name { color: var(--fg); } .lrow.off .name { color: var(--muted); } .lrow .count { margin-left: auto; color: var(--dim); } /* events / logs */ .ev { display: flex; gap: 12px; padding: 1px 0; font-size: 11px; line-height: 1.5; } .ev .ev-ts { color: var(--muted); flex-shrink: 0; } .ev .ev-lvl { flex-shrink: 0; width: 42px; } .ev .ev-lvl.info { color: var(--blue); } .ev .ev-lvl.warn { color: var(--warn); } .ev .ev-lvl.err { color: var(--err); } .ev .ev-msg { color: var(--fg); white-space: pre-wrap; word-break: break-word; } .ev .ev-msg .k { color: var(--dim); } /* heatmap */ .heat { display: flex; align-items: flex-end; gap: 2px; height: 60px; } .heat .hbar { flex: 1; border-radius: 1px 1px 0 0; min-height: 2px; } /* search screen */ .search-split { flex: 1; display: flex; gap: 10px; min-height: 0; } .results { width: 360px; flex-shrink: 0; } .result-item { padding: 8px 10px; border-radius: var(--r-sm); cursor: pointer; border: 1px solid transparent; } .result-item:hover { background: #11181f; } .result-item.active { background: var(--panel2); border-color: var(--border); } .result-item .ri-path { color: var(--accent); font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .result-item .ri-meta { display: flex; gap: 10px; margin-top: 3px; } .result-item .ri-meta .name { color: var(--fg); font-size: 11px; } .result-item .ri-meta .lines { color: var(--muted); font-size: 10px; margin-left: auto; } .result-item .ri-meta .score { color: var(--accent); font-size: 11px; } .preview { flex: 1; min-width: 0; } .preview pre { font-family: var(--mono); font-size: 12px; line-height: 1.55; color: var(--fg); white-space: pre; tab-size: 4; } .preview .pv-head { color: var(--accent); } /* bottom command bar */ .cmd { display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 14px; background: var(--panel2); border-top: 1px solid var(--border); } .cmd .colon { color: var(--accent); font-size: 14px; } .cmd .cmd-input { flex: 1; background: transparent; border: none; outline: none; color: var(--fg); font-family: var(--mono); font-size: 13px; } .cmd .cmd-input::placeholder { color: var(--muted); } .cmd .hints { display: flex; gap: 12px; margin-left: auto; } .cmd .hints span { color: var(--muted); font-size: 11px; } .empty { color: var(--muted); font-size: 11px; padding: 8px 0; } .blink { animation: blink 1.1s step-end infinite; } @keyframes blink { 50% { opacity: 0; } } ::-webkit-scrollbar { width: 8px; height: 8px; } ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: 4px; } ::-webkit-scrollbar-track { background: transparent; } .hidden { display: none !important; } </style> </head> <body> <div id="app"> <!-- title --> <div class="title"> <span class="caret">▶</span> <h1 id="title-text">Home / Dashboard</h1> <span class="screen-of" id="title-screen">· screen 1 of 3</span> </div> <!-- chrome bar --> <div class="chrome"> <span class="lights">● ● ●</span> <span class="cwd" id="cwd">rag · ~/production/ragsystem</span> <span class="dims" id="dims">web · v2.0</span> </div> <!-- status strip --> <div class="status"> <span class="seg"><span class="dot" id="st-daemon-dot" style="color:var(--ok)">●</span> <span class="val" id="st-daemon">daemon</span></span> <span class="seg" style="color:var(--dim)" id="st-port">:7890</span> <span class="seg"><span class="dot" style="color:var(--accent)">●</span> <span class="val" id="st-repo">—</span></span> <span class="seg">chunks <span class="val" id="st-chunks">—</span></span> <span class="seg"><span class="dot" style="color:var(--violet)">●</span> <span class="val" id="st-gen">—</span></span> <span class="seg">warm <span class="val" id="st-warm">—</span></span> <span class="seg">qpm <span class="val" id="st-qpm">—</span></span> <span class="seg">up <span class="val" id="st-up">—</span></span> <span class="clock" id="st-clock">--:--</span> </div> <!-- body --> <div class="body"> <!-- sidebar --> <nav class="sidebar"> <div class="sb-label">NAVIGATION</div> <div id="nav"></div> <div class="sb-div"></div> <div class="sb-label">REPOS</div> <div id="repos"></div> </nav> <!-- main --> <main class="main"> <!-- HOME --> <section id="screen-home" class="screen"> <div class="kpis"> <div class="kpi index"><div class="k-label">INDEX SIZE</div><div class="k-value" id="kpi-index">—</div><div class="k-sub" id="kpi-index-sub">chunks · files</div></div> <div class="kpi embed"><div class="k-label">EMBEDDER</div><div class="k-value" id="kpi-embed">—</div><div class="k-sub" id="kpi-embed-sub">Ollama</div></div> <div class="kpi gen"><div class="k-label">GENERATOR</div><div class="k-value" id="kpi-gen">—</div><div class="k-sub" id="kpi-gen-sub">ctx</div></div> <div class="kpi up"><div class="k-label">UPTIME</div><div class="k-value" id="kpi-up">—</div><div class="k-sub" id="kpi-up-sub">restarts</div></div> </div> <div class="home-split"> <div class="home-left"> <div class="panel" style="flex:1;"> <div class="panel-head"> <span class="ph-title">QPM (24h)</span> <span class="ph-right" id="qpm-summary">avg — · peak —</span> </div> <div class="panel-body"> <div class="spark" id="spark"></div> <div class="spark-axis"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>now</span></div> <div class="hr"></div> <div class="sub-label">RECENT QUERIES</div> <div id="queries"><div class="empty">no queries yet</div></div> </div> </div> </div> <div class="home-right" style="flex-direction:column; gap:10px;"> <div class="panel" style="flex:1;"> <div class="panel-head"><span class="ph-title">PLUGINS</span></div> <div class="panel-body" id="plugins"><div class="empty">none</div></div> </div> <div class="panel" style="flex:1;"> <div class="panel-head"><span class="ph-title">COLLECTIONS</span></div> <div class="panel-body" id="collections"><div class="empty">none</div></div> </div> </div> </div> </section> <!-- SEARCH --> <section id="screen-search" class="screen hidden"> <div class="panel" style="flex-shrink:0;"> <div class="panel-head"> <span class="ph-title">SEARCH</span> <span class="ph-right" id="search-status">enter a query below</span> </div> </div> <div class="search-split"> <div class="panel results"> <div class="panel-head"><span class="ph-title">RESULTS</span><span class="ph-right" id="results-count">0</span></div> <div class="panel-body" id="results"><div class="empty">run a search with the bar below</div></div> </div> <div class="panel preview"> <div class="panel-head"><span class="ph-title" id="preview-title">PREVIEW</span><span class="ph-right" id="preview-meta"></span></div> <div class="panel-body"><pre id="preview"><span class="empty">select a result</span></pre></div> </div> </div> </section> <!-- LOGS --> <section id="screen-logs" class="screen hidden"> <div class="panel" style="flex:1;"> <div class="panel-head"> <span class="ph-title">Logs (tail -f)</span> <span class="ph-right" id="logs-count">0 events</span> </div> <div class="panel-body" id="events"><div class="empty">waiting for events…</div></div> </div> <div class="panel" style="flex-shrink:0;"> <div class="panel-head"><span class="ph-title">EVENT VOLUME (24h)</span><span class="ph-right" id="heat-summary"></span></div> <div class="panel-body"><div class="heat" id="heat"></div></div> </div> </section> </main> </div> <!-- bottom command bar --> <div class="cmd"> <span class="colon">:</span> <input class="cmd-input" id="cmd-input" placeholder="search … (↵ to run)" autocomplete="off" spellcheck="false" /> <div class="hints"> <span>↵ run</span> <span id="cmd-mode">home</span> <span>1·2·3 screens</span> </div> </div> </div> <script> "use strict"; // --------------------------------------------------------------------------- // RAG dashboard v2 — same-origin browser client. The token is injected by the // daemon at serve time (server.py replaces the __RAG_TOKEN__ placeholder). // --------------------------------------------------------------------------- const TOKEN = "__RAG_TOKEN__"; const HDRS = { Authorization: "Bearer " + TOKEN }; const $ = (id) => document.getElementById(id); async function api(path, opts) { const o = opts || {}; o.headers = Object.assign({}, HDRS, o.headers || {}); const r = await fetch(path, o); if (!r.ok) throw new Error(path + " → " + r.status); return r.json(); } // --- screen routing --------------------------------------------------------- const SCREENS = [ { key: "home", title: "Home / Dashboard", icon: "⌂", n: 1 }, { key: "search", title: "Search", icon: ":", n: 2 }, { key: "logs", title: "Logs (tail -f)", icon: "≋", n: 3 }, ]; // nav also shows non-functional placeholders to echo the design's 8-item menu const NAV_EXTRA = ["Ask", "Filters", "Overview", "Index", "Help"]; let current = "home"; function buildNav() { const nav = $("nav"); nav.innerHTML = ""; SCREENS.forEach((s) => { const el = document.createElement("div"); el.className = "nav-item" + (s.key === current ? " active" : ""); el.innerHTML = `<span class="icon">${s.icon}</span><span class="label">${s.title.split(" ")[0]}</span><span class="num">${s.n}</span>`; el.onclick = () => go(s.key); nav.appendChild(el); }); NAV_EXTRA.forEach((label, i) => { const el = document.createElement("div"); el.className = "nav-item disabled"; el.innerHTML = `<span class="icon" style="color:var(--muted)">·</span><span class="label">${label}</span><span class="num">${SCREENS.length + i + 1}</span>`; el.title = "Not available in the web dashboard"; nav.appendChild(el); }); } function go(key) { current = key; SCREENS.forEach((s) => $("screen-" + s.key).classList.toggle("hidden", s.key !== key)); const s = SCREENS.find((x) => x.key === key); $("title-text").textContent = s.title; $("title-screen").textContent = `· screen ${s.n} of ${SCREENS.length}`; $("cmd-mode").textContent = key; const inp = $("cmd-input"); inp.placeholder = key === "search" ? "search query … (↵ to run)" : key === "logs" ? "filter events … (↵ to apply)" : "type a query, ↵ jumps to Search"; buildNav(); } // --- helpers ---------------------------------------------------------------- function hhmm(ts) { let d; if (ts == null) return "--:--"; if (typeof ts === "number") d = new Date(ts * 1000); else d = new Date(String(ts).replace(" ", "T")); if (isNaN(d)) return "--:--"; return String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0"); } function tsEpoch(ts) { if (ts == null) return null; if (typeof ts === "number") return ts; const d = new Date(String(ts).replace(" ", "T")); return isNaN(d) ? null : d.getTime() / 1000; } function num(n) { return (n == null ? 0 : n).toLocaleString("en-US").replace(/,/g, " "); } function fmtUptime(sec) { sec = Math.floor(sec || 0); const d = Math.floor(sec / 86400), h = Math.floor((sec % 86400) / 3600), m = Math.floor((sec % 3600) / 60); if (d) return `${d}d ${h}h`; if (h) return `${h}h ${m}m`; return `${m}m`; } function fmtCtx(n) { // gen_ctx_size may be raw tokens (8192) or already in k (8). Treat small // values as already-k so we never render "0k". if (n == null) return "?"; if (n < 1024) return `${n}k`; return `${Math.round(n / 1024)}k`; } // --- pollers ---------------------------------------------------------------- async function pollStatus() { try { const s = await api("/status"); $("st-daemon-dot").style.color = "var(--ok)"; $("st-daemon").textContent = "daemon"; const liveCollections = (s.collections || []).filter((c) => (c.points_count || 0) > 0); const chunks = liveCollections.reduce((a, c) => a + (c.points_count || 0), 0); $("st-chunks").textContent = num(chunks); $("st-gen").textContent = s.gen_model || s.embedder_model || "—"; $("st-warm").textContent = s.embedder_warm_ms != null ? Math.round(s.embedder_warm_ms) + "ms" : "—"; $("st-up").textContent = fmtUptime(s.uptime_seconds); // KPI cards — total live collections so named repo indexes are visible. $("kpi-index").textContent = num(chunks); $("kpi-index-sub").textContent = `chunks · ${num(s.files_indexed)} files`; $("kpi-embed").textContent = s.embedder_model || "—"; $("kpi-embed-sub").textContent = `${s.embedder_provider || "ollama"} · ${s.embedder_warm_ms != null ? Math.round(s.embedder_warm_ms) + "ms warm" : "warm"}`; $("kpi-gen").textContent = s.gen_model || "—"; $("kpi-gen-sub").textContent = s.gen_ctx_size ? `ctx ${fmtCtx(s.gen_ctx_size)} · streaming` : "streaming"; $("kpi-up").textContent = fmtUptime(s.uptime_seconds); $("kpi-up-sub").textContent = s.restart_count ? `${s.restart_count} restarts` : "no restarts"; } catch (e) { $("st-daemon-dot").style.color = "var(--err)"; $("st-daemon").textContent = "offline"; } } async function pollHealthRepo() { try { const c = await api("/collections"); const repos = $("repos"); repos.innerHTML = ""; const live = (c.collections || []).filter((x) => (x.points_count || 0) > 0); const repoNames = live.map((x) => x.repo || x.name); $("st-repo").textContent = repoNames.length ? repoNames.join(", ") : "empty"; if (!live.length) { const r = document.createElement("div"); r.className = "repo-item"; r.innerHTML = `<span class="dot" style="color:var(--muted)">●</span><span class="label">empty</span>`; repos.appendChild(r); } else { live.forEach((x) => { const r = document.createElement("div"); r.className = "repo-item"; r.innerHTML = `<span class="dot" style="color:var(--accent)">●</span><span class="label">${esc(x.repo || x.name)}</span>`; repos.appendChild(r); }); } // collections panel const col = $("collections"); col.innerHTML = ""; if (!(c.collections || []).length) { col.innerHTML = '<div class="empty">none</div>'; return; } c.collections.forEach((x) => { const el = document.createElement("div"); el.className = "lrow"; const label = x.repo ? `${x.repo} · ${x.name}` : x.name; el.innerHTML = `<span class="arrow">▸</span><span class="name">${esc(label)}</span><span class="count">${num(x.points_count || 0)}</span>`; col.appendChild(el); }); } catch (e) { /* keep last */ } } async function pollQueries() { try { const d = await api("/queries/recent?limit=200"); const rows = d.queries || []; renderSparkline(rows); renderQueries(rows.slice(0, 12)); } catch (e) { /* keep last */ } try { const st = await api("/queries/stats?window=200"); $("st-qpm").textContent = Math.round(st.qpm || 0); } catch (e) { /* */ } } function renderSparkline(rows) { // 48 bins over the last 24h const BINS = 48, now = Date.now() / 1000, span = 24 * 3600; const bins = new Array(BINS).fill(0); rows.forEach((r) => { const t = tsEpoch(r.timestamp); if (t == null) return; const age = now - t; if (age < 0 || age > span) return; const idx = BINS - 1 - Math.floor((age / span) * BINS); if (idx >= 0 && idx < BINS) bins[idx]++; }); const max = Math.max(1, ...bins); const peak = Math.max(0, ...bins); const avg = (bins.reduce((a, b) => a + b, 0) / BINS); $("qpm-summary").textContent = `avg ${avg.toFixed(1)} · peak ${peak}`; const sp = $("spark"); sp.innerHTML = ""; bins.forEach((v) => { const b = document.createElement("div"); b.className = "bar"; b.style.height = Math.max(2, (v / max) * 56) + "px"; b.style.opacity = v ? "0.9" : "0.25"; sp.appendChild(b); }); } function renderQueries(rows) { const q = $("queries"); if (!rows.length) { q.innerHTML = '<div class="empty">no queries yet</div>'; return; } q.innerHTML = ""; rows.forEach((r) => { const el = document.createElement("div"); el.className = "qrow"; const score = r.results_count != null ? `${r.results_count} hits` : ""; const lat = r.latency_ms != null ? `${Math.round(r.latency_ms)}ms` : ""; el.innerHTML = `<span class="qtime">${hhmm(r.timestamp)}</span>` + `<span class="qtext">${esc(r.query || "")}</span>` + `<span class="qmeta"><span class="strat">${esc(score)}</span><span class="score">${lat}</span></span>`; q.appendChild(el); }); } async function pollPlugins() { try { const d = await api("/plugins"); const p = $("plugins"); const list = d.plugins || []; if (!list.length) { p.innerHTML = '<div class="empty">no plugins</div>'; return; } p.innerHTML = ""; list.forEach((pl) => { const el = document.createElement("div"); el.className = "lrow"; el.innerHTML = `<span class="dot on">●</span><span class="name">${esc(pl.name)}</span><span class="count">${pl.patterns || 0}p</span>`; p.appendChild(el); }); } catch (e) { /* */ } } // --- logs screen ------------------------------------------------------------ let lastEventTs = 0; function levelOf(ev) { const s = ev.status; if (typeof s === "number" && s >= 500) return "err"; if (typeof s === "number" && s >= 400) return "warn"; if (/error|fail/i.test(ev.event || "")) return "err"; if (/warn/i.test(ev.event || "")) return "warn"; return "info"; } function fmtEvent(ev) { const parts = []; for (const k of Object.keys(ev)) { if (k === "ts" || k === "event") continue; parts.push(`<span class="k">${esc(k)}=</span>${esc(String(ev[k]))}`); } return `<b>${esc(ev.event || "event")}</b> ${parts.join(" ")}`; } async function pollEvents() { try { const d = await api("/events/recent?limit=400"); const evs = d.events || []; renderEvents(evs); renderHeat(evs); } catch (e) { /* */ } } function renderEvents(evs) { const box = $("events"); $("logs-count").textContent = `${evs.length} events`; if (!evs.length) { box.innerHTML = '<div class="empty">waiting for events…</div>'; return; } const wasBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 40; box.innerHTML = ""; evs.slice(-300).forEach((ev) => { const el = document.createElement("div"); el.className = "ev"; const lvl = levelOf(ev); el.innerHTML = `<span class="ev-ts">${hhmm(ev.ts)}</span>` + `<span class="ev-lvl ${lvl}">${lvl.toUpperCase()}</span>` + `<span class="ev-msg">${fmtEvent(ev)}</span>`; box.appendChild(el); }); if (wasBottom) box.scrollTop = box.scrollHeight; } function renderHeat(evs) { const BINS = 80, now = Date.now() / 1000, span = 24 * 3600; const bins = new Array(BINS).fill(0); evs.forEach((ev) => { const age = now - (ev.ts || 0); if (age < 0 || age > span) return; const idx = BINS - 1 - Math.floor((age / span) * BINS); if (idx >= 0 && idx < BINS) bins[idx]++; }); const max = Math.max(1, ...bins); const h = $("heat"); h.innerHTML = ""; bins.forEach((v) => { const b = document.createElement("div"); b.className = "hbar"; const ratio = v / max; b.style.height = Math.max(2, ratio * 60) + "px"; // mint → blue ramp by intensity const t = ratio; b.style.background = v ? `rgba(94,230,208,${0.35 + 0.65 * t})` : "rgba(74,84,98,0.25)"; h.appendChild(b); }); $("heat-summary").textContent = `peak ${Math.max(0, ...bins)}/bin`; } // --- search ----------------------------------------------------------------- let searchResults = []; async function runSearch(query) { if (!query.trim()) return; go("search"); $("search-status").textContent = "searching …"; $("results").innerHTML = '<div class="empty">searching…</div>'; $("preview").innerHTML = '<span class="empty">select a result</span>'; try { const d = await api("/search", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ query, top_k: 20 }), }); searchResults = d.results || []; const strat = d.plan ? d.plan.strategy : "—"; $("search-status").textContent = `${d.total} hits · ${strat} · ${Math.round(d.latency_ms)}ms`; $("results-count").textContent = d.total; renderResults(searchResults); if (searchResults.length) selectResult(0); else $("results").innerHTML = '<div class="empty">no results</div>'; } catch (e) { $("search-status").textContent = "error: " + e.message; $("results").innerHTML = '<div class="empty">search failed</div>'; } } function renderResults(rows) { const box = $("results"); box.innerHTML = ""; rows.forEach((r, i) => { const el = document.createElement("div"); el.className = "result-item" + (i === 0 ? " active" : ""); el.dataset.idx = i; el.innerHTML = `<div class="ri-path">${esc(r.file_path)}</div>` + `<div class="ri-meta"><span class="name">${esc(r.name || r.chunk_type)}</span>` + `<span class="lines">${esc(r.lines || "")}</span>` + `<span class="score">${(r.score || 0).toFixed(2)}</span></div>`; el.onclick = () => selectResult(i); box.appendChild(el); }); } function selectResult(i) { const r = searchResults[i]; if (!r) return; document.querySelectorAll(".result-item").forEach((e) => e.classList.toggle("active", +e.dataset.idx === i)); $("preview-title").textContent = r.file_path; $("preview-meta").textContent = `${r.language || ""} · ${r.lines || ""} · ${(r.score || 0).toFixed(2)}`; $("preview").textContent = r.code || "(no code)"; } function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); } // --- command bar + keys ----------------------------------------------------- $("cmd-input").addEventListener("keydown", (e) => { if (e.key === "Enter") { const v = e.target.value; if (current === "logs") { /* future: filter */ return; } if (v.trim()) { runSearch(v); } } }); document.addEventListener("keydown", (e) => { if (document.activeElement === $("cmd-input")) { if (e.key === "Escape") $("cmd-input").blur(); return; } if (e.key === "1") go("home"); else if (e.key === "2") go("search"); else if (e.key === "3") go("logs"); else if (e.key === "/" || e.key === ":") { e.preventDefault(); $("cmd-input").focus(); } }); // --- clock ------------------------------------------------------------------ function tick() { const d = new Date(); $("st-clock").textContent = String(d.getHours()).padStart(2, "0") + ":" + String(d.getMinutes()).padStart(2, "0") + ":" + String(d.getSeconds()).padStart(2, "0"); } // --- boot ------------------------------------------------------------------- buildNav(); go("home"); tick(); setInterval(tick, 1000); pollStatus(); setInterval(pollStatus, 5000); pollHealthRepo(); setInterval(pollHealthRepo, 10000); pollQueries(); setInterval(pollQueries, 3000); pollPlugins(); setInterval(pollPlugins, 30000); pollEvents(); setInterval(pollEvents, 2000); </script> </body> </html>