diff --git a/public_html/admin/index.php b/public_html/admin/index.php index cc0c3f5..bd1a80f 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -425,6 +425,8 @@ select.filter-sel:focus{border-color:var(--cyan)} /* ── MISC ── */ .empty{color:var(--dim);font-size:0.7rem;letter-spacing:1px;padding:30px;text-align:center} +@keyframes agentIn{from{opacity:0;transform:translateX(-12px)}to{opacity:1;transform:translateX(0)}} +.agent-row{animation:agentIn .18s ease forwards;opacity:0} .loading{color:var(--dim);font-size:0.7rem;letter-spacing:2px;padding:30px;text-align:center;animation:pulse 1s infinite} @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}} .meter{height:4px;background:var(--border);margin-top:4px;position:relative} @@ -485,16 +487,16 @@ select.filter-sel:focus{border-color:var(--cyan)}
DASHBOARD
-
LOADING...
+
SCANNING...
-
AGENTS +
AGENTS
-
LOADING...
+
@@ -514,7 +516,7 @@ select.filter-sel:focus{border-color:var(--cyan)}  
-
LOADING...
+
SCANNING...
@@ -533,7 +535,7 @@ select.filter-sel:focus{border-color:var(--cyan)} -
LOADING...
+
SCANNING...
@@ -550,7 +552,7 @@ select.filter-sel:focus{border-color:var(--cyan)} -
LOADING...
+
SCANNING...
@@ -561,7 +563,7 @@ select.filter-sel:focus{border-color:var(--cyan)} -
LOADING...
+
SCANNING...
@@ -577,7 +579,7 @@ select.filter-sel:focus{border-color:var(--cyan)}   -
LOADING...
+
SCANNING...
@@ -591,11 +593,11 @@ select.filter-sel:focus{border-color:var(--cyan)}
PINNED / CUSTOM NEWS
-
LOADING...
+
SCANNING...
LIVE FEED (auto-refreshed)
-
LOADING...
+
SCANNING...
@@ -605,19 +607,19 @@ select.filter-sel:focus{border-color:var(--cyan)}
PROXMOX VMs
-
LOADING...
+
SCANNING...
SITE HEALTH
-
LOADING...
+
SCANNING...
USERS
-
LOADING...
+
SCANNING...
@@ -786,32 +788,110 @@ async function loadDashboard() { `; } +// ── PROGRESSIVE RENDER HELPER ───────────────────────────────────────────────── +// Renders rows one-by-one into a tbody, staggered so the table "fills in" live. +// titleEl: element to show scanning progress. headers: th array. rowFn: item→html string. +function progressiveRender(items, tbodyId, rowFn, titleEl, titleDone) { + const tbody = document.getElementById(tbodyId); + if (!tbody) return; + if (!items.length) { + tbody.closest('table')?.parentElement && (tbody.closest('.tbl-wrap').innerHTML = '
NO DATA
'); + if (titleEl) titleEl.textContent = titleDone || ''; + return; + } + const n = items.length; + const stagger = Math.min(100, Math.max(15, Math.floor(1800 / n))); // cap total at ~1.8s + items.forEach((item, i) => { + setTimeout(() => { + const tr = document.createElement('tr'); + tr.className = 'agent-row'; // reuse slide-in animation + tr.innerHTML = rowFn(item, i); + tbody.appendChild(tr); + if (titleEl) { + const done = i + 1; + titleEl.innerHTML = done < n + ? `${titleDone.split(' ')[0]} SCANNING... ${done}/${n}` + : `${titleDone} ${n} TOTAL`; + } + }, i * stagger); + }); +} + +// Sets up the empty table shell immediately while fetch is in flight +function scanShell(tblWrapId, headers, titleEl, scanLabel) { + const wrap = document.getElementById(tblWrapId); + if (!wrap) return; + const ths = headers.map(h=>`${h}`).join(''); + wrap.innerHTML = `${ths}
`; + if (titleEl) titleEl.innerHTML = `${scanLabel} SCANNING...`; +} + // ── AGENTS ──────────────────────────────────────────────────────────────────── async function loadAgents() { - document.getElementById('agents-tbl').innerHTML = '
LOADING...
'; + const tbl = document.getElementById('agents-tbl'); + const title = document.getElementById('agents-title'); + + // Build empty table shell immediately — no waiting + tbl.innerHTML = ` + +
HOSTNAMESTATUSTYPEIPMETRICSLAST SEENREGISTERED
`; + + title.innerHTML = 'AGENTS SCANNING...'; + const agents = await api('agents_list'); - if (!agents.length) { document.getElementById('agents-tbl').innerHTML='
NO AGENTS REGISTERED
'; return; } - let rows = agents.map(a => { - const m = a.metrics; - const meterCell = m - ? `${m.cpu_pct??'—'}% CPU · ${m.mem_pct??'—'}% RAM` - : ``; - return ` - ${esc(a.hostname)} - ${statusBadge(a.status)} - ${esc(a.agent_type).toUpperCase()} - ${esc(a.ip_address||'—')} - ${meterCell} - ${ago(a.last_seen)} - ${ts(a.created_at)} -
- -
- `; - }).join(''); - document.getElementById('agents-tbl').innerHTML = ` - - ${rows}
HOSTNAMESTATUSTYPEIPMETRICSLAST SEENREGISTEREDACTIONS
`; + const tbody = document.getElementById('agents-tbody'); + + if (!agents.length) { + tbl.innerHTML = '
NO AGENTS REGISTERED
'; + title.textContent = 'AGENTS'; + return; + } + + // Reveal each agent row with a staggered delay + agents.forEach((a, i) => { + setTimeout(() => { + const m = a.metrics; + const online = a.status === 'online'; + const lastSeen = a.last_seen ? (Date.now() - new Date(a.last_seen)) / 1000 : null; + const fresh = lastSeen !== null && lastSeen < 30; + + // CPU/RAM mini bars + let meterCell = `no metrics`; + if (m) { + function miniBar(pct, warn=70, crit=85) { + if (pct == null) return '—'; + const c = pct>=crit?'var(--red)':pct>=warn?'var(--yellow)':'var(--green)'; + return `${Math.round(pct)}%`; + } + meterCell = `CPU ${miniBar(m.cpu_pct)} · RAM ${miniBar(m.mem_pct)} · DISK ${miniBar(m.disk_pct,80,90)}`; + } + + const row = document.createElement('tr'); + row.className = 'agent-row'; + row.style.animationDelay = '0s'; + row.innerHTML = ` + + + ${esc(a.hostname)} + ${fresh && online ? '● LIVE' : ''} + + ${statusBadge(a.status)} + ${esc(a.agent_type||'linux').toUpperCase()} + ${esc(a.ip_address||'—')} + ${meterCell} + ${ago(a.last_seen)} + ${ts(a.created_at)} + `; + tbody?.appendChild(row); + + // Update title counter as agents appear + const found = i + 1; + const online_count = agents.slice(0, found).filter(x => x.status === 'online').length; + if (title) title.innerHTML = found < agents.length + ? `AGENTS SCANNING... ${found}/${agents.length}` + : `AGENTS ${online_count} ONLINE / ${agents.length} TOTAL`; + }, i * 120); // 120ms stagger per agent + }); } function delAgent(id, name) { @@ -832,7 +912,7 @@ function setNetFilter(f, el) { } async function loadNetwork() { - document.getElementById('network-tbl').innerHTML = '
LOADING...
'; + scanShell('network-tbl', ['NAME','IP','MAC','VENDOR / TYPE','STATUS','LAST SEEN','ACTIONS'], null, null); _allDevices = await api('network_list'); renderNetwork(); } @@ -842,20 +922,18 @@ function renderNetwork() { if (_netFilter === 'online') devs = devs.filter(d => d.status === 'online'); if (_netFilter === 'offline') devs = devs.filter(d => d.status === 'offline'); if (_netFilter === 'named') devs = devs.filter(d => d.alias); - const onlineCount = _allDevices.filter(d=>d.status==='online').length; document.getElementById('net-count').textContent = `${onlineCount}/${_allDevices.length} ONLINE`; - if (!devs.length) { document.getElementById('network-tbl').innerHTML='
NO DEVICES MATCH FILTER
'; return; } - let rows = devs.map(d => { + // Re-build shell (filter changed) + document.getElementById('network-tbl').innerHTML = ` + +
NAMEIPMACVENDOR / TYPESTATUSLAST SEENACTIONS
`; + progressiveRender(devs, 'network-tbl-tbody', d => { const name = d.alias || d.hostname || d.ip; - const isNamed = !!d.alias; const vendor = d.device_type || '—'; - return ` - - - ${esc(name)}${isNamed?'':' (discovered)'} - + return ` + ${esc(name)}${d.alias?'':' (discovered)'} ${esc(d.ip)} ${esc(d.mac||'—')} ${esc(vendor)} @@ -865,12 +943,8 @@ function renderNetwork() { - - `; - }).join(''); - document.getElementById('network-tbl').innerHTML = ` - - ${rows}
NAMEIPMACVENDOR / TYPESTATUSLAST SEENACTIONS
`; + `; + }, null, null); } async function scanNow() { @@ -931,25 +1005,24 @@ function setAlertFilter(f, el) { } async function loadAlerts() { - document.getElementById('alerts-tbl').innerHTML='
LOADING...
'; + scanShell('alerts-tbl', ['SEV','TYPE','TITLE','MESSAGE','STATUS','CREATED','ACTIONS'], null, null); const alerts = await api('alerts_list', {filter:_alertFilter}); if (!alerts.length) { document.getElementById('alerts-tbl').innerHTML='
NO ALERTS
'; return; } - let rows = alerts.map(a => ` - ${sevBadge(a.severity)} + document.getElementById('alerts-tbl').innerHTML = ` + +
SEVTYPETITLEMESSAGESTATUSCREATEDACTIONS
`; + progressiveRender(alerts, 'alerts-tbl-tbody', a => + `${sevBadge(a.severity)} ${esc(a.alert_type)} ${esc(a.title)} ${esc(a.message||'—')} - ${a.resolved ? 'RESOLVED' : 'ACTIVE'} + ${a.resolved?'RESOLVED':'ACTIVE'} ${ts(a.created_at)}
${!a.resolved?``:''} -
- `).join(''); - document.getElementById('alerts-tbl').innerHTML = ` - - ${rows}
SEVTYPETITLEMESSAGESTATUSCREATEDACTIONS
`; + `, null, null); } function alertModal(id=0, type='manual', title='', message='', severity='info') { @@ -978,23 +1051,22 @@ async function loadFactCategories() { } async function loadFacts() { - document.getElementById('facts-tbl').innerHTML='
LOADING...
'; + scanShell('facts-tbl', ['CATEGORY','KEY','VALUE','UPDATED','ACTIONS'], null, null); const cat = document.getElementById('factCat')?.value || '__all__'; const facts = await api('facts_list', {category: cat}); if (!facts.length) { document.getElementById('facts-tbl').innerHTML='
NO FACTS
'; return; } - let rows = facts.map(f => ` - ${esc(f.category)} + document.getElementById('facts-tbl').innerHTML = ` + +
CATEGORYKEYVALUEUPDATEDACTIONS
`; + progressiveRender(facts, 'facts-tbl-tbody', f => + `${esc(f.category)} ${esc(f.fact_key)} ${esc(f.fact_value)} ${ago(f.updated_at)}
-
- `).join(''); - document.getElementById('facts-tbl').innerHTML = ` - - ${rows}
CATEGORYKEYVALUEUPDATEDACTIONS
`; + `, null, null); } function factModal(id=0, category='', key='', value='') { @@ -1011,13 +1083,16 @@ function factModal(id=0, category='', key='', value='') { // ── KB INTENTS ──────────────────────────────────────────────────────────────── async function loadIntents() { - document.getElementById('intents-tbl').innerHTML='
LOADING...
'; + scanShell('intents-tbl', ['NAME','PATTERN','RESPONSE','TYPE','PRI','STATUS','ACTIONS'], null, null); const intents = await api('intents_list'); if (!intents.length) { document.getElementById('intents-tbl').innerHTML='
NO INTENTS
'; return; } - let rows = intents.map(i => ` - ${esc(i.intent_name)} + document.getElementById('intents-tbl').innerHTML = ` + +
NAMEPATTERNRESPONSETYPEPRISTATUSACTIONS
`; + progressiveRender(intents, 'intents-tbl-tbody', i => + `${esc(i.intent_name)} ${esc(i.pattern)} - ${esc(i.response_template||'—')} + ${esc(i.response_template||'—')} ${esc(i.action_type)} ${i.priority} ${i.active?'ON':'OFF'} @@ -1025,11 +1100,7 @@ async function loadIntents() { - - `).join(''); - document.getElementById('intents-tbl').innerHTML = ` - - ${rows}
NAMEPATTERNRESPONSETYPEPRISTATUSACTIONS
`; + `, null, null); } function intentModal(id=0, name='', pattern='', response='', type='response', priority=5, active=1) { @@ -1053,7 +1124,7 @@ function intentModal(id=0, name='', pattern='', response='', type='response', pr // ── SITES ───────────────────────────────────────────────────────────────────── async function loadSites() { - document.getElementById('sites-content').innerHTML='
LOADING...
'; + document.getElementById('sites-content').innerHTML='
SCANNING...
'; const sites = await api('sites_list'); if (!sites.length) { document.getElementById('sites-content').innerHTML='
NO SITE DATA
'; return; } const labels = { @@ -1074,18 +1145,19 @@ async function loadSites() { // ── USERS ───────────────────────────────────────────────────────────────────── async function loadUsers() { - document.getElementById('users-tbl').innerHTML='
LOADING...
'; + scanShell('users-tbl', ['USERNAME','DISPLAY NAME','LAST SEEN','CREATED','ACTIONS'], null, null); const users = await api('users_list'); - let rows = users.map(u => ` - ${esc(u.username)} + if (!users.length) { document.getElementById('users-tbl').innerHTML='
NO USERS
'; return; } + document.getElementById('users-tbl').innerHTML = ` + +
USERNAMEDISPLAY NAMELAST SEENCREATEDACTIONS
`; + progressiveRender(users, 'users-tbl-tbody', u => + `${esc(u.username)} ${esc(u.display_name||'—')} ${ago(u.last_seen)} ${ts(u.created_at)} - - `).join(''); - document.getElementById('users-tbl').innerHTML = ` - - ${rows}
USERNAMEDISPLAY NAMELAST SEENCREATEDACTIONS
`; + `, + null, null); } function userModal(id, display) { @@ -1120,7 +1192,7 @@ document.addEventListener('keydown', e => { if (e.key==='Enter' && e.ctrlKey && let _haEntities = []; async function loadHA() { - document.getElementById('ha-tbl').innerHTML = '
LOADING...
'; + document.getElementById('ha-tbl').innerHTML = '
SCANNING...
'; const domain = document.getElementById('ha-domain')?.value || ''; const data = await api('ha_list', {domain}); _haEntities = data.entities || []; @@ -1172,8 +1244,8 @@ function renderHATable(entities) { // ── NEWS ────────────────────────────────────────────────────────────────────── async function loadNews() { - document.getElementById('news-custom').innerHTML='
LOADING...
'; - document.getElementById('news-live').innerHTML='
LOADING...
'; + document.getElementById('news-custom').innerHTML='
SCANNING...
'; + document.getElementById('news-live').innerHTML='
SCANNING...
'; const data = await api('news_list'); // Custom entries @@ -1219,7 +1291,7 @@ function newsCustomModal(id=0, title='', url='') { // ── PROXMOX VMs ─────────────────────────────────────────────────────────────── async function loadVMs() { - document.getElementById('vms-tbl').innerHTML='
LOADING...
'; + document.getElementById('vms-tbl').innerHTML='
SCANNING...
'; const data = await api('vms_list'); const vms = [...(data.vms||[]), ...(data.containers||[])]; if (!vms.length) { document.getElementById('vms-tbl').innerHTML='
NO VM DATA — Proxmox cache empty, refreshes every 5 min
'; return; }