aipackage/cezen-portal/appliance.html
2026-06-30 10:51:41 +05:30

387 lines
16 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Appliance Ops — Nexus One AI</title>
<link rel="stylesheet" href="style.css?v=4">
<style>
.content { max-width:1180px; }
.ops-grid { display:grid; grid-template-columns:repeat(12,1fr); gap:16px; align-items:stretch; }
.ops-card {
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius-lg);
padding:20px;
min-width:0;
box-shadow:var(--shadow-sm);
}
.ops-card h2 {
font-size:13px;
margin:0 0 14px;
color:var(--text-primary);
letter-spacing:0;
font-weight:700;
}
.span-4 { grid-column:span 4; }
.span-6 { grid-column:span 6; }
.span-8 { grid-column:span 8; }
.span-12 { grid-column:span 12; }
.metric { font-size:32px; font-weight:800; color:var(--text-primary); line-height:1; letter-spacing:0; }
.metric-sub { color:var(--text-secondary); font-size:13px; margin-top:9px; max-width:320px; }
.pill {
display:inline-flex;
align-items:center;
padding:3px 9px;
border-radius:var(--radius-pill);
font-size:11px;
font-weight:700;
border:1px solid var(--border);
color:var(--text-secondary);
background:#F1F5F9;
text-transform:uppercase;
letter-spacing:.03em;
}
.pill.ok { color:var(--green); background:var(--green-lt); border-color:var(--green-border); }
.pill.warn { color:var(--amber); background:var(--amber-lt); border-color:var(--amber-border); }
.pill.bad { color:var(--red); background:var(--red-lt); border-color:var(--red-border); }
.ops-list { display:flex; flex-direction:column; gap:10px; }
.ops-row { display:flex; justify-content:space-between; gap:12px; padding:10px 0; border-bottom:1px solid var(--border); align-items:center; }
.ops-row:last-child { border-bottom:none; }
.ops-label { color:var(--text-secondary); font-size:13px; }
.ops-value { color:var(--text-primary); font-weight:700; text-align:right; overflow-wrap:anywhere; }
.check-row { display:flex; gap:10px; align-items:flex-start; padding:10px 0; border-bottom:1px solid var(--border); }
.check-row:last-child { border-bottom:none; }
.check-dot { width:10px; height:10px; border-radius:50%; margin-top:5px; background:var(--red); flex-shrink:0; box-shadow:0 0 0 3px var(--red-lt); }
.check-dot.ok { background:var(--green); box-shadow:0 0 0 3px var(--green-lt); }
.check-title { color:var(--text-primary); font-size:13px; font-weight:700; }
.check-note { color:var(--text-secondary); font-size:12px; margin-top:2px; line-height:1.5; }
.ops-actions {
display:flex;
flex-wrap:wrap;
gap:10px;
margin-bottom:16px;
background:var(--surface);
border:1px solid var(--border);
border-radius:var(--radius-lg);
padding:12px;
box-shadow:var(--shadow-xs);
}
.ops-btn {
padding:8px 14px;
border-radius:var(--radius-md);
border:1px solid var(--border);
background:var(--surface);
color:var(--text-secondary);
font:inherit;
font-size:13px;
font-weight:700;
cursor:pointer;
box-shadow:var(--shadow-xs);
}
.ops-btn:hover { border-color:var(--brand-border); color:var(--brand); background:var(--brand-lt); }
.ops-btn.primary { background:var(--brand); color:#fff; border-color:var(--brand); }
.ops-btn.primary:hover { background:var(--brand-hover); color:#fff; }
.ops-btn.danger { color:var(--red); border-color:var(--red-border); box-shadow:none; }
.ops-btn.danger:hover { background:var(--red-lt); border-color:var(--red-border); color:var(--red); }
.ops-table { width:100%; border-collapse:collapse; }
.ops-table th { text-align:left; color:var(--text-tertiary); font-size:11px; text-transform:uppercase; letter-spacing:.04em; padding:9px; border-bottom:1px solid var(--border); background:var(--surface-hover); }
.ops-table td { color:var(--text-primary); font-size:13px; padding:10px 9px; border-bottom:1px solid var(--border); vertical-align:middle; }
.ops-table tr:last-child td { border-bottom:none; }
.mono { font-family:monospace; font-size:12px; }
.empty { color:var(--text-tertiary); font-size:13px; padding:14px 0; }
#toast { position:fixed; right:22px; bottom:22px; padding:11px 16px; border-radius:10px; display:none; font-size:13px; font-weight:700; z-index:99; }
#toast.ok { color:var(--green); background:var(--green-lt); border:1px solid var(--green-border); }
#toast.err { color:var(--red); background:var(--red-lt); border:1px solid var(--red-border); }
@media (max-width: 900px) { .span-4,.span-6,.span-8 { grid-column:span 12; } }
</style>
</head>
<body data-role="admin">
<header class="topnav">
<a href="index.html" class="brand">Nexus One <span>AI</span></a>
<nav>
<a href="index.html">Home</a>
<a href="quickstart.html">Quick Start</a>
<a href="prompts.html">Prompt Library</a>
<a href="usecases.html">Use Cases</a>
<span class="nav-sep"></span>
<div class="nav-dropdown">
<button class="nav-drop-btn">Help ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">LEARN /</span>
<a href="quickstart.html">Quick Start</a>
<a href="models.html">Models</a>
<span class="nav-drop-cat">SUPPORT /</span>
<a href="troubleshooting.html">Troubleshoot</a>
<a href="faq.html">FAQ</a>
<span class="nav-drop-cat">MORE /</span>
<a href="glossary.html">Glossary</a>
<a href="whats-new.html">What's New</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn active">Admin ▾</button>
<div class="nav-drop-menu nav-drop-menu-wide">
<span class="nav-drop-cat">DOCS /</span>
<a href="security.html">Security & Privacy</a>
<a href="admin.html">Admin Guide</a>
<span class="nav-drop-cat">MONITOR /</span>
<a href="dashboard.html">Dashboard</a>
<a href="analytics.html">Usage Analytics</a>
<a href="audit.html">Audit Log</a>
<a href="feedback.html">Feedback &amp; Ratings</a>
<span class="nav-drop-cat">MANAGE /</span>
<a href="users.html">Users</a>
<a href="teams.html">Teams</a>
<a href="models-admin.html">Model Manager</a>
<a href="training.html">Training</a>
<a href="knowledge.html">Knowledge Base</a>
<span class="nav-drop-cat">TOOLS /</span>
<a href="apikeys.html">API Keys</a>
<a href="benchmark.html">Benchmarking</a>
<a href="model-compare.html">Model Compare</a>
<a href="api-playground.html">API Playground</a>
<a href="guardrails.html">Guardrails</a>
<a href="rag-quality.html">RAG Quality</a>
<a href="router.html">Model Router</a>
<a href="connectors.html">Connectors</a>
<span class="nav-drop-cat">SYSTEM /</span>
<a href="appliance.html" class="active">Appliance Ops</a>
<a href="console.html">Console</a>
<a href="settings.html">Settings</a>
</div>
</div>
<div class="nav-dropdown">
<button class="nav-drop-btn">AI Tools ▾</button>
<div class="nav-drop-menu">
<span class="nav-drop-cat">INTELLIGENCE /</span>
<a href="documents.html">Document Intelligence</a>
<a href="chat-multi.html">Multimodal Chat</a>
<a href="prompt-studio.html">Prompt Studio</a>
<a href="meeting.html">Meeting Assistant</a>
<span class="nav-drop-cat">AUTOMATION /</span>
<a href="agents.html">Agent Builder</a>
<a href="schedules.html">Scheduled Jobs</a>
<a href="workflows.html">Workflow Automation</a>
<span class="nav-drop-cat">QUALITY /</span>
<a href="evals.html">AI Eval Suite</a>
<a href="chatrooms.html">Chat Rooms</a>
</div>
</div>
</nav>
<a href="notifications.html" style="position:relative">🔔</a>
<span class="badge" data-brand="tier">Basic Tier</span>
<div id="nav-org-logo" class="nav-org-logo"></div>
</header>
<div class="page-hero">
<div class="label">Admin · System</div>
<h1>Appliance Ops</h1>
<p>Readiness, license tier, audit summary, and local data protection.</p>
</div>
<div class="content">
<div class="ops-actions">
<button class="ops-btn primary" onclick="loadAll()">Refresh</button>
<button class="ops-btn" onclick="createBackup()">Create Backup</button>
<button class="ops-btn" onclick="exportReadiness()">Export Readiness JSON</button>
</div>
<div class="ops-grid">
<section class="ops-card span-4">
<h2>Readiness</h2>
<div class="metric" id="readiness-score"></div>
<div class="metric-sub"><span class="pill" id="readiness-status">Loading</span></div>
</section>
<section class="ops-card span-4">
<h2>License</h2>
<div class="metric" id="license-tier"></div>
<div class="metric-sub" id="license-position"></div>
</section>
<section class="ops-card span-4">
<h2>Audit</h2>
<div class="metric" id="audit-events"></div>
<div class="metric-sub"><span id="audit-failures"></span> failures in 7 days</div>
</section>
<section class="ops-card span-6">
<h2>Deployment Fit</h2>
<div class="ops-list" id="fit-list"><div class="empty">Loading…</div></div>
</section>
<section class="ops-card span-6">
<h2>Hardware</h2>
<div class="ops-list" id="hardware-list"><div class="empty">Loading…</div></div>
</section>
<section class="ops-card span-8">
<h2>Readiness Checks</h2>
<div id="checks-list"><div class="empty">Loading…</div></div>
</section>
<section class="ops-card span-4">
<h2>Recommendations</h2>
<div id="recommendations-list"><div class="empty">Loading…</div></div>
</section>
<section class="ops-card span-12">
<h2>Backups</h2>
<div style="overflow:auto">
<table class="ops-table">
<thead><tr><th>Name</th><th>Created</th><th>Size</th><th></th></tr></thead>
<tbody id="backup-table"><tr><td colspan="4"><div class="empty">Loading…</div></td></tr></tbody>
</table>
</div>
</section>
</div>
</div>
<div id="toast"></div>
<script>
let readinessData = null;
function esc(v) {
return String(v ?? '—').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
function fmtBytes(n) {
if (!n) return '0 B';
const units = ['B','KB','MB','GB','TB'];
let i = 0, v = Number(n);
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
return `${v.toFixed(i ? 1 : 0)} ${units[i]}`;
}
function toast(msg, ok=true) {
const el = document.getElementById('toast');
el.textContent = msg;
el.className = ok ? 'ok' : 'err';
el.style.display = 'block';
setTimeout(() => el.style.display = 'none', 3200);
}
async function api(path, opts={}) {
const res = await fetch(path, { credentials:'include', ...opts });
if (!res.ok) throw new Error(await res.text() || res.statusText);
return res.json();
}
function renderReadiness(d) {
readinessData = d;
const score = d.readiness?.score ?? 0;
const status = d.readiness?.status || 'unknown';
document.getElementById('readiness-score').textContent = score + '%';
const statusEl = document.getElementById('readiness-status');
statusEl.textContent = status.replace(/_/g, ' ');
statusEl.className = 'pill ' + (status === 'ready' ? 'ok' : status === 'limited' ? 'warn' : 'bad');
document.getElementById('license-tier').textContent = d.license?.label || '—';
document.getElementById('license-position').textContent = d.license?.positioning || '—';
const auditFit = d.commercial_fit || {};
const rec = d.feasibility?.recommendation || {};
document.getElementById('fit-list').innerHTML = [
['Commercial Fit', auditFit.status || '—'],
['Current Tier', auditFit.current_tier || '—'],
['Recommended Tier', auditFit.recommended_tier || '—'],
['Recommended Profile', rec.recommended_profile || '—'],
['Estimated Users', rec.estimated_concurrent_users || '—'],
].map(row => `<div class="ops-row"><div class="ops-label">${esc(row[0])}</div><div class="ops-value">${esc(row[1])}</div></div>`).join('');
const f = d.feasibility || {};
const disk = f.disk || {};
const gpu = (f.gpus || [])[0];
document.getElementById('hardware-list').innerHTML = [
['Host', f.host || '—'],
['CPU', f.cpu?.cores ? f.cpu.cores + ' cores' : '—'],
['RAM', f.ram_gb ? f.ram_gb + ' GB' : '—'],
['Disk Free', disk.free_gb ? disk.free_gb + ' GB' : '—'],
['GPU', gpu ? `${gpu.name || 'GPU'} · ${gpu.vram_gb || 0} GB` : 'None detected'],
].map(row => `<div class="ops-row"><div class="ops-label">${esc(row[0])}</div><div class="ops-value">${esc(row[1])}</div></div>`).join('');
document.getElementById('checks-list').innerHTML = (d.readiness?.checks || []).map(c => `
<div class="check-row">
<span class="check-dot ${c.passed ? 'ok' : ''}"></span>
<div><div class="check-title">${esc(c.name)}</div><div class="check-note">${esc(c.note)}</div></div>
</div>
`).join('') || '<div class="empty">No checks available.</div>';
document.getElementById('recommendations-list').innerHTML = (d.readiness?.recommendations || []).map(r =>
`<div class="check-row"><span class="check-dot ok"></span><div class="check-note">${esc(r)}</div></div>`
).join('') || '<div class="empty">No recommendations.</div>';
}
function renderAudit(d) {
document.getElementById('audit-events').textContent = d.summary?.events ?? '—';
document.getElementById('audit-failures').textContent = d.summary?.failures ?? '—';
}
function renderBackups(d) {
const rows = d.backups || [];
document.getElementById('backup-table').innerHTML = rows.length ? rows.map(b => `
<tr>
<td class="mono">${esc(b.name)}</td>
<td>${esc(new Date(b.created_at).toLocaleString())}</td>
<td>${fmtBytes(b.size_bytes)}</td>
<td style="text-align:right"><button class="ops-btn danger" onclick="restoreBackup('${esc(b.name)}')">Restore</button></td>
</tr>
`).join('') : '<tr><td colspan="4"><div class="empty">No backups found.</div></td></tr>';
}
async function loadAll() {
try {
const [readiness, audit, backups] = await Promise.all([
api('/api/system/readiness-report'),
api('/api/audit/report?days=7'),
api('/api/system/backups'),
]);
renderReadiness(readiness);
renderAudit(audit);
renderBackups(backups);
} catch (e) {
toast('Could not load appliance data', false);
}
}
async function createBackup() {
try {
toast('Creating backup…');
await api('/api/system/backups', { method:'POST' });
toast('Backup created');
const backups = await api('/api/system/backups');
renderBackups(backups);
} catch (e) {
toast('Backup failed', false);
}
}
async function restoreBackup(name) {
if (!confirm(`Restore backup ${name}? A pre-restore safety snapshot will be created.`)) return;
try {
await api(`/api/system/backups/${encodeURIComponent(name)}/restore`, { method:'POST' });
toast('Restore complete. Restart recommended.');
const backups = await api('/api/system/backups');
renderBackups(backups);
} catch (e) {
toast('Restore failed', false);
}
}
function exportReadiness() {
if (!readinessData) return toast('Readiness report not loaded', false);
const blob = new Blob([JSON.stringify(readinessData, null, 2)], { type:'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `cezen-readiness-${new Date().toISOString().slice(0,10)}.json`;
a.click();
URL.revokeObjectURL(url);
}
loadAll();
</script>
<script src="auth.js"></script>
<script src="branding.js"></script>
</body>
</html>