nova-ayurvedic-ai / admin.html
jarnil's picture
Upload folder using huggingface_hub
15f5690 verified
Raw
History Blame Contribute Delete
32.3 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nova AI | Admin Command Center</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=Inter:wght@300;400;600;700&family=Orbitron:wght@400;700&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style>
:root {
--primary: #6366f1;
--primary-glow: rgba(99, 102, 241, 0.5);
--bg: #0f172a;
--card-bg: rgba(30, 41, 59, 0.7);
--card-border: rgba(255, 255, 255, 0.1);
--text-main: #f8fafc;
--text-muted: #94a3b8;
--accent: #10b981;
--danger: #ef4444;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--bg);
color: var(--text-main);
min-height: 100vh;
display: flex;
overflow: hidden;
}
/* --- Background Animation --- */
.bg-glow {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
background: radial-gradient(circle at 20% 30%, rgba(99, 102, 241, 0.15), transparent 40%),
radial-gradient(circle at 80% 70%, rgba(16, 185, 129, 0.1), transparent 40%);
}
/* --- Sidebar --- */
aside {
width: 280px;
background: rgba(15, 23, 42, 0.8);
backdrop-filter: blur(10px);
border-right: 1px solid var(--card-border);
display: flex;
flex-direction: column;
padding: 2rem;
height: 100vh;
}
.logo {
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 3rem;
display: flex;
align-items: center;
gap: 10px;
color: var(--primary);
}
nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 0.8rem 1rem;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
color: var(--text-muted);
text-decoration: none;
}
.nav-item:hover, .nav-item.active {
background: rgba(99, 102, 241, 0.1);
color: var(--text-main);
}
.nav-item.active {
border-left: 4px solid var(--primary);
}
/* --- Main Content --- */
main {
flex: 1;
padding: 2rem;
overflow-y: auto;
position: relative;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
h1 { font-family: 'Orbitron', sans-serif; font-size: 1.8rem; }
/* --- Grid Layout --- */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
/* --- Glass Card --- */
.card {
background: var(--card-bg);
backdrop-filter: blur(12px);
border: 1px solid var(--card-border);
border-radius: 20px;
padding: 1.5rem;
transition: transform 0.3s ease;
}
.card:hover { transform: translateY(-5px); }
.stat-value { font-size: 2.5rem; font-weight: 700; color: var(--primary); margin: 0.5rem 0; }
.stat-label { color: var(--text-muted); font-size: 0.9rem; text-transform: uppercase; }
/* --- Sections --- */
.section { display: none; }
.section.active { display: block; animation: fadeIn 0.4s ease forwards; }
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* --- Tables & Lists --- */
table { width: 100%; border-collapse: collapse; margin-top: 1rem; }
th { text-align: left; padding: 1rem; border-bottom: 1px solid var(--card-border); color: var(--text-muted); }
td { padding: 1rem; border-bottom: 1px solid var(--card-border); }
tr:last-child td { border-bottom: none; }
.btn {
background: var(--primary);
color: white;
border: none;
padding: 0.6rem 1.2rem;
border-radius: 10px;
cursor: pointer;
font-weight: 600;
transition: 0.3s;
}
.btn:hover { opacity: 0.9; box-shadow: 0 0 15px var(--primary-glow); }
.btn-danger { background: var(--danger); }
.btn-sm { padding: 0.4rem 0.8rem; font-size: 0.8rem; }
input {
background: rgba(255,255,255,0.05);
border: 1px solid var(--card-border);
padding: 0.8rem;
border-radius: 10px;
color: white;
outline: none;
width: 100%;
margin-bottom: 1rem;
}
.tag {
background: rgba(16, 185, 129, 0.1);
color: var(--accent);
padding: 0.2rem 0.6rem;
border-radius: 10px;
font-size: 0.8rem;
}
.log-item {
padding: 1rem;
border-bottom: 1px solid var(--card-border);
font-family: monospace;
font-size: 0.9rem;
}
.symptom-bar-container {
margin-bottom: 1rem;
}
.symptom-label-row {
display: flex;
justify-content: space-between;
margin-bottom: 0.4rem;
font-size: 0.9rem;
}
.symptom-bar-bg {
background: rgba(255,255,255,0.05);
height: 8px;
border-radius: 4px;
overflow: hidden;
}
.symptom-bar-fill {
background: var(--primary);
height: 100%;
border-radius: 4px;
transition: width 1s ease-in-out;
}
</style>
</head>
<body>
<div class="bg-glow"></div>
<aside>
<div class="logo">
<i data-lucide="zap"></i>
NOVA AI
</div>
<nav>
<div class="nav-item active" onclick="showSection('dashboard', this)">
<i data-lucide="layout-dashboard"></i> Dashboard
</div>
<div class="nav-item" onclick="showSection('gaps', this)">
<i data-lucide="brain-circuit"></i> Knowledge Gaps
</div>
<div class="nav-item" onclick="showSection('synonyms', this)">
<i data-lucide="languages"></i> Synonyms
</div>
<div class="nav-item" onclick="showSection('patches', this)">
<i data-lucide="shield-check"></i> Patches
</div>
<div class="nav-item" onclick="showSection('logs', this)">
<i data-lucide="scroll-text"></i> Live Logs
</div>
<div class="nav-item" onclick="showSection('tests', this)">
<i data-lucide="flask-conical"></i> System Tests
</div>
<div class="nav-item" onclick="showSection('flags', this)">
<i data-lucide="toggle-left"></i> Feature Flags
</div>
<div class="nav-item" onclick="showSection('audit', this)">
<i data-lucide="clipboard-list"></i> Audit Log
</div>
</nav>
</aside>
<main>
<div class="header">
<h1 id="section-title">Dashboard</h1>
<div id="status-bubble" class="tag">System Online</div>
</div>
<!-- Dashboard Section -->
<div id="dashboard" class="section active">
<div class="stats-grid">
<div class="card">
<div class="stat-label">Total Users</div>
<div class="stat-value" id="stat-users">--</div>
<i data-lucide="users" style="color: var(--text-muted)"></i>
</div>
<div class="card">
<div class="stat-label">Active Sessions</div>
<div class="stat-value" id="stat-sessions">--</div>
<i data-lucide="activity" style="color: var(--text-muted)"></i>
</div>
<div class="card">
<div class="stat-label">Guest Queries</div>
<div class="stat-value" id="stat-guests">--</div>
<i data-lucide="fingerprint" style="color: var(--text-muted)"></i>
</div>
<div class="card" id="card-gaps" style="border-color: rgba(250,200,50,0.3);">
<div class="stat-label" style="color: #f59e0b;">Pending Gaps</div>
<div class="stat-value" id="stat-gaps" style="color: #f59e0b;">--</div>
<i data-lucide="alert-triangle" style="color: #f59e0b;"></i>
</div>
<div class="card" id="card-patches" style="border-color: rgba(99,102,241,0.3);">
<div class="stat-label" style="color: var(--primary);">Active Patches</div>
<div class="stat-value" id="stat-patches">--</div>
<i data-lucide="layers" style="color: var(--primary);"></i>
</div>
</div>
<div style="display: grid; grid-template-columns: 1.5fr 1fr; gap: 1.5rem;">
<div class="card">
<h3>Recent Activity</h3>
<div id="activity-list" style="margin-top: 1rem;">
<!-- Activity items here -->
</div>
</div>
<div class="card">
<h3>Popular Symptoms</h3>
<p style="color: var(--text-muted); font-size: 0.8rem; margin-bottom: 1.5rem;">Most frequent user entries.</p>
<div id="symptom-stats">
<!-- Symptom bars here -->
</div>
</div>
</div>
</div>
<!-- Gaps Section -->
<div id="gaps" class="section">
<div class="card">
<h3>Unmapped Symptom Phrases</h3>
<p style="color: var(--text-muted); margin-bottom: 1rem;">Review symptoms Nova couldn't identify and map them to medical terms.</p>
<table>
<thead>
<tr>
<th>User Phrase</th>
<th>Analysis</th>
<th>Action</th>
</tr>
</thead>
<tbody id="gaps-table-body"></tbody>
</table>
</div>
</div>
<!-- Synonyms Section -->
<div id="synonyms" class="section">
<div class="card" style="margin-bottom: 1.5rem;">
<h3>Add New Synonym Mapping</h3>
<div style="display: flex; gap: 1rem; margin-top: 1rem;">
<input type="text" id="synonym-phrase" placeholder="Informal Phrase (e.g. belly ache)">
<input type="text" id="synonym-expansion" placeholder="Medical Term (e.g. abdominal pain)">
<button class="btn" style="height: 48px;" onclick="addSynonym()">Add</button>
</div>
</div>
<div class="card">
<h3>Current Synonym Dictionary</h3>
<table>
<thead>
<tr>
<th>Informal</th>
<th>Medical Mapping</th>
<th>Action</th>
</tr>
</thead>
<tbody id="synonym-table-body"></tbody>
</table>
</div>
</div>
<!-- Patches Section -->
<div id="patches" class="section">
<div class="card">
<h3>Learned Patches</h3>
<p style="color: var(--text-muted); margin-bottom: 1rem;">These are custom rules taught by you during gap verification.</p>
<table>
<thead>
<tr>
<th>Symptom Pattern</th>
<th>Resulting Disease</th>
<th>Action</th>
</tr>
</thead>
<tbody id="patch-table-body"></tbody>
</table>
</div>
</div>
<!-- Logs Section -->
<div id="logs" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<h3>Live System Logs</h3>
<button class="btn btn-sm" onclick="fetchLogs()">Refresh</button>
</div>
<div id="log-container" style="max-height: 60vh; overflow-y: auto; background: rgba(0,0,0,0.2); border-radius: 10px;">
<!-- Logs will appear here -->
</div>
</div>
</div>
<!-- System Tests Section -->
<div id="tests" class="section">
<div class="card" style="margin-bottom: 1.5rem;">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div>
<h3>Gold Evaluation Suite</h3>
<p style="color: var(--text-muted); margin-top: 0.4rem; font-size: 0.9rem;">Runs 20 curated clinical test cases to validate disease prediction and risk-level accuracy. Per <em>new_update.md Β§6.4</em>.</p>
</div>
<button class="btn" id="run-tests-btn" onclick="runGoldEval()">&#9654; Run Tests</button>
</div>
</div>
<div class="card">
<h3>Last Results <span id="test-run-time" style="font-size:0.8rem; color: var(--text-muted); font-weight:400;"></span></h3>
<div id="test-summary" style="margin: 1rem 0; font-size: 1rem;"></div>
<table>
<thead>
<tr>
<th>ID</th>
<th>Description</th>
<th>Result</th>
<th>Errors</th>
</tr>
</thead>
<tbody id="tests-table-body"></tbody>
</table>
</div>
</div>
<!-- Feature Flags Section -->
<div id="flags" class="section">
<div class="card">
<h3>Feature Flags & Rollouts (A/B Testing)</h3>
<p style="color: var(--text-muted); margin-bottom: 1.5rem;">Manage feature gates and gradual A/B rollout percentages. Toggles apply immediately to subsequent requests.</p>
<table>
<thead>
<tr>
<th>Flag Name</th>
<th>Enabled</th>
<th>Rollout %</th>
<th>Action</th>
</tr>
</thead>
<tbody id="flags-table-body">
<!-- Flag items will populate here -->
</tbody>
</table>
</div>
</div>
<!-- Audit Log Section -->
<div id="audit" class="section">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
<div>
<h3>Admin Audit Log</h3>
<p style="color: var(--text-muted); margin-top: 0.4rem; font-size: 0.9rem;">Every admin mutation is recorded here. Per <em>new_update.md Β§5.6 &amp; Β§6.2</em>.</p>
</div>
<button class="btn btn-sm" onclick="fetchAuditLog()">Refresh</button>
</div>
<table>
<thead>
<tr>
<th>Timestamp</th>
<th>Admin</th>
<th>Action</th>
<th>Details</th>
</tr>
</thead>
<tbody id="audit-table-body"></tbody>
</table>
</div>
</div>
</main>
<script>
// Init Lucide Icons
lucide.createIcons();
function showSection(sectionId, navEl) {
document.querySelectorAll('.section').forEach(s => s.classList.remove('active'));
document.getElementById(sectionId).classList.add('active');
document.querySelectorAll('.nav-item').forEach(i => i.classList.remove('active'));
navEl.classList.add('active');
document.getElementById('section-title').innerText = navEl.innerText.trim();
if(sectionId === 'dashboard') fetchStats();
if(sectionId === 'gaps') fetchGaps();
if(sectionId === 'synonyms') fetchSynonyms();
if(sectionId === 'patches') fetchPatches();
if(sectionId === 'logs') fetchLogs();
if(sectionId === 'tests') fetchTestResults();
if(sectionId === 'flags') fetchFlags();
if(sectionId === 'audit') fetchAuditLog();
}
// --- API HELPERS ---
async function api(path, method = 'GET', body = null) {
const options = { method, headers: { 'Content-Type': 'application/json' } };
if(body) options.body = JSON.stringify(body);
try {
const res = await fetch(path, options);
if(res.status === 401) {
alert("Unauthorized. Please ensure Basic Auth headers are present.");
return null;
}
return await res.json();
} catch(e) { console.error(e); return null; }
}
async function fetchStats() {
const data = await api('/api/admin/stats');
if(!data) return;
document.getElementById('stat-users').innerText = data.total_users;
document.getElementById('stat-sessions').innerText = data.total_sessions;
document.getElementById('stat-guests').innerText = data.guest_sessions;
// v17.1: Loop health stats
const gaps = data.pending_gaps ?? '--';
const patches = data.active_patches ?? '--';
document.getElementById('stat-gaps').innerText = gaps;
document.getElementById('stat-patches').innerText = patches;
const gapCard = document.getElementById('card-gaps');
if (typeof gaps === 'number' && gaps > 0) {
gapCard.style.borderColor = 'rgba(245,158,11,0.6)';
gapCard.style.boxShadow = '0 0 12px rgba(245,158,11,0.25)';
}
const list = document.getElementById('activity-list');
list.innerHTML = data.recent_activity.map(a => `
<div class="log-item">
<span class="tag" style="margin-right: 1rem">${a.is_guest ? 'Guest' : 'Member'}</span>
<strong>${a.title}</strong>
<span style="color: var(--text-muted); float: right">${new Date(a.updated_at).toLocaleTimeString()}</span>
</div>
`).join('');
const symptomList = document.getElementById('symptom-stats');
if (data.top_symptoms && data.top_symptoms.length > 0) {
const maxCount = data.top_symptoms[0].count;
symptomList.innerHTML = data.top_symptoms.map(s => {
const percent = Math.round((s.count / maxCount) * 100);
return `
<div class="symptom-bar-container">
<div class="symptom-label-row">
<span>${s.symptom}</span>
<span style="color: var(--text-muted)">${s.count} entries</span>
</div>
<div class="symptom-bar-bg">
<div class="symptom-bar-fill" style="width: ${percent}%"></div>
</div>
</div>
`;
}).join('');
} else {
symptomList.innerHTML = `<p style="color: var(--text-muted); text-align: center; margin-top: 2rem;">No symptom data yet.</p>`;
}
}
async function fetchGaps() {
const data = await api('/api/admin/gaps');
if(!data) return;
const tbody = document.getElementById('gaps-table-body');
// Diagnostics
if (data.exists === false) {
tbody.innerHTML = `<tr><td colspan="3" style="color: #ff4d4d; text-align: center; padding: 2rem;">
⚠️ CRITICAL: Gaps file not found at: <code style="display:block;margin-top:0.5rem">${data.debug_path}</code>
</td></tr>`;
return;
}
if (!data.gaps || data.gaps.length === 0) {
tbody.innerHTML = `<tr><td colspan="3" style="color: var(--text-muted); text-align: center; padding: 2rem;">
No knowledge gaps currently logged.<br>
<small style="opacity:0.6">(Checked: ${data.debug_path})</small>
</td></tr>`;
return;
}
tbody.innerHTML = (data.gaps || []).map((g, idx) => `
<tr>
<td><strong>${g.user_input}</strong></td>
<td>${g.reason || 'Gap detected'}</td>
<td>
<button class="btn btn-sm" onclick="verifyGap(${idx}, '${g.user_input}')">Approve as...</button>
<button class="btn btn-sm btn-danger" onclick="removeGap(${idx})">Dismiss</button>
</td>
</tr>
`).join('');
}
async function verifyGap(idx, input) {
const disease = prompt(`Map "${input}" to which disease?`);
if(!disease) return;
const res = await api('/api/admin/verify', 'POST', { gap_idx: idx, disease: disease });
if(res.success) { alert(res.message); fetchGaps(); }
}
async function removeGap(idx) {
if(!confirm("Remove this gap log?")) return;
await api('/api/admin/remove-gap', 'DELETE', { gap_idx: idx });
fetchGaps();
}
async function fetchSynonyms() {
const data = await api('/api/admin/synonyms');
const tbody = document.getElementById('synonym-table-body');
tbody.innerHTML = Object.entries(data.synonyms || {}).map(([ph, ex]) => `
<tr>
<td>${ph}</td>
<td><span class="tag">${ex}</span></td>
<td><button class="btn btn-sm btn-danger" onclick="removeSynonym('${ph}')">Delete</button></td>
</tr>
`).join('');
}
async function addSynonym() {
const phrase = document.getElementById('synonym-phrase').value;
const expansion = document.getElementById('synonym-expansion').value;
if(!phrase || !expansion) return;
const res = await api('/api/admin/synonyms', 'POST', { phrase, expansion });
if(res.success) {
document.getElementById('synonym-phrase').value = '';
document.getElementById('synonym-expansion').value = '';
fetchSynonyms();
}
}
async function removeSynonym(phrase) {
await api(`/api/admin/synonyms/${encodeURIComponent(phrase)}`, 'DELETE');
fetchSynonyms();
}
async function fetchPatches() {
const data = await api('/api/admin/patches');
const tbody = document.getElementById('patch-table-body');
tbody.innerHTML = (data.patches || []).map((p, idx) => `
<tr>
<td>${p.pattern}</td>
<td><span class="tag">${p.disease}</span></td>
<td><button class="btn btn-sm btn-danger" onclick="removePatch(${idx})">Remove Rule</button></td>
</tr>
`).join('');
}
async function removePatch(idx) {
await api(`/api/admin/patches/${idx}`, 'DELETE');
fetchPatches();
}
async function fetchLogs() {
const data = await api('/api/admin/logs');
const container = document.getElementById('log-container');
container.innerHTML = (data.logs || []).map(l => `
<div class="log-item">
<span style="color: var(--primary)">[${new Date(l.timestamp).toLocaleTimeString()}]</span>
<span style="color: var(--accent); font-weight: bold">${l.role.toUpperCase()}</span>
<span style="color: var(--text-muted)">(${l.user_display})</span>:
${l.text}
</div>
`).join('');
}
// v17.1 ── System Tests ────────────────────────────────────────────
async function runGoldEval() {
const btn = document.getElementById('run-tests-btn');
btn.disabled = true;
btn.textContent = 'Running...';
const data = await api('/api/admin/run-tests', 'POST');
btn.disabled = false;
btn.textContent = '\u25B6 Run Tests';
if(!data) return;
if(data.last_results) renderTestResults(data.last_results);
else document.getElementById('test-summary').innerHTML = '<em style="color:var(--text-muted)">Test running in background. Refresh in ~60s.</em>';
}
async function fetchTestResults() {
// Just try to GET last results via run-tests (returns immediately)
const data = await api('/api/admin/run-tests', 'POST');
if(data && data.last_results) renderTestResults(data.last_results);
}
function renderTestResults(r) {
const summary = document.getElementById('test-summary');
const passed = r.passed; const total = r.total; const failed = r.failed;
const pct = Math.round((passed/total)*100);
const colour = failed === 0 ? 'var(--accent)' : 'var(--danger)';
summary.innerHTML = `<span style="font-size:1.6rem;font-weight:700;color:${colour}">${passed}/${total} PASSED</span> <span style="color:var(--text-muted);font-size:0.9rem;">(${pct}%)</span>`;
document.getElementById('test-run-time').textContent = r.run_at ? 'β€” ' + new Date(r.run_at).toLocaleString() : '';
const tbody = document.getElementById('tests-table-body');
tbody.innerHTML = (r.results || []).map(res => `
<tr>
<td><code>${res.case_id}</code></td>
<td>${res.description}</td>
<td style="color:${res.passed ? 'var(--accent)' : 'var(--danger)'}; font-weight:600">${res.passed ? '\u2705 PASS' : '\u274C FAIL'}</td>
<td style="font-size:0.82rem; color:var(--text-muted)">${(res.errors||[]).join('<br>') || '&mdash;'}</td>
</tr>
`).join('');
}
// v17.1 ── Audit Log ───────────────────────────────────────────────
async function fetchAuditLog() {
const data = await api('/api/admin/audit-log');
if(!data) return;
const tbody = document.getElementById('audit-table-body');
const entries = data.audit_log || [];
if(entries.length === 0) {
tbody.innerHTML = '<tr><td colspan="4" style="text-align:center;color:var(--text-muted);padding:2rem;">No audit entries yet.</td></tr>';
return;
}
tbody.innerHTML = entries.map(e => `
<tr>
<td style="font-size:0.82rem;color:var(--text-muted)">${new Date(e.timestamp).toLocaleString()}</td>
<td><span class="tag">${e.admin}</span></td>
<td><strong>${e.action}</strong></td>
<td style="font-size:0.82rem;color:var(--text-muted)">${JSON.stringify(e.details)}</td>
</tr>
`).join('');
}
// v17.1 ── Feature Flags ───────────────────────────────────────────
async function fetchFlags() {
const data = await api('/api/admin/feature-flags');
if(!data) return;
const tbody = document.getElementById('flags-table-body');
tbody.innerHTML = Object.entries(data).map(([name, flag]) => `
<tr>
<td><code>${name}</code></td>
<td>
<span class="tag" style="background: ${flag.enabled ? 'rgba(16, 185, 129, 0.1)' : 'rgba(239, 68, 68, 0.1)'}; color: ${flag.enabled ? 'var(--accent)' : 'var(--danger)'}">
${flag.enabled ? 'Active' : 'Disabled'}
</span>
</td>
<td>
<input type="number" min="0" max="100" value="${flag.rollout_pct}" id="pct-${name}" style="width: 80px; padding: 0.3rem 0.5rem; margin-bottom: 0; display: inline-block;"> %
</td>
<td>
<button class="btn btn-sm" onclick="updateFlag('${name}', true)">Enable</button>
<button class="btn btn-sm btn-danger" onclick="updateFlag('${name}', false)">Disable</button>
</td>
</tr>
`).join('');
}
async function updateFlag(name, enabled) {
const pct = parseInt(document.getElementById(`pct-${name}`).value) || 0;
const res = await api(`/api/admin/feature-flags/${name}`, 'POST', { enabled, rollout_pct: pct });
if(res && res.updated) {
alert(`Successfully updated flag ${name}`);
fetchFlags();
} else {
alert("Failed to update feature flag.");
}
}
// Initial Load
fetchStats();
</script>
</body>
</html>