CxWebApp/static/js/panes/files.js
CxAI Agent 75153b7fe9
Some checks are pending
build-and-push / image (push) Waiting to run
feat(cxwebapp): comprehensive pane enhancements
- All 14 panes rewritten with rich controls: KPIs, sparklines, meter
  gauges, presets, history sidebars, search/filter, bulk actions,
  schema-driven forms, copy buttons, auto-refresh.
- Backend /api/system enriched with cpu_count, rss_bytes, mem_total,
  mem_used_pct, loadavg[], user/system CPU times, max_rss_kb.
- CSS additions: .meter/.gauge, .seg, .chip(s), .spark, .slider, .dl,
  .link-card, details.card-d plus utility classes.
- ui.js helpers: copyToClipboard, withCopy/bindCopyButtons, sparkline,
  meterRow, fmtBytes/fmtNum, historyStore, inputFromSchema,
  collectSchemaForm.
- Files pane added; iCloud duplicate '*2' files removed.
2026-05-17 09:36:19 -05:00

237 lines
10 KiB
JavaScript

// panes/files.js — Surfaces the files.cxllm.io platform data (Providers /
// Edge Functions / Demand / Watcher / Health / File-manager) directly inside
// CxWebApp. All requests go through the C++ backend at /api/files/* which
// injects the PostgREST anon key so the browser never sees the secret.
import { jget, jpost, escapeHtml } from '../lib/api.js';
import { ok, err } from '../lib/ui.js';
import { registerPane } from '../app.js';
const TABS = [
{ id: 'providers', label: 'Providers', endpoint: '/mcp_providers?select=*&order=id.asc' },
{ id: 'functions', label: 'Edge Functions', endpoint: '/platform_functions?select=*&order=slug.asc' },
{ id: 'health', label: 'Health', endpoint: '/platform_health?select=*&order=captured_at.desc&limit=20' },
{ id: 'demand', label: 'Demand Runs', endpoint: '/demand_runs?select=*&order=received_at.desc&limit=20' },
{ id: 'watcher', label: 'Watcher Events', endpoint: '/watcher_events?select=*&order=occurred_at.desc&limit=50' },
{ id: 'categories', label: 'File Manager', endpoint: '/file_manager_categories?select=*&order=category.asc' },
];
let activeTab = 'providers';
const rest = (path) => jget(`/api/files${path}`);
const restPost = (path, body) => jpost(`/api/files${path}`, body, { headers: { Prefer: 'return=minimal' } });
const TPL = `
<div class="pane-head">
<div>
<div class="title">Files · Platform</div>
<div class="sub">Live providers, edge functions, demand, watcher and health from <code>files.cxllm.io</code>.</div>
</div>
<div class="grow"></div>
<a class="btn btn-secondary" href="https://files.cxllm.io" target="_blank" rel="noopener">Open Studio ↗</a>
<button class="btn btn-secondary" id="files-refresh">Refresh</button>
</div>
<div class="card mb-3">
<div class="tabs" id="files-tabs">
${TABS.map(t => `<button class="tab" data-tab="${t.id}">${escapeHtml(t.label)}</button>`).join('')}
</div>
</div>
<div id="files-body"></div>
`;
function tag(kind, txt) {
return `<span class="pill ${kind}">${escapeHtml(txt ?? '')}</span>`;
}
function statusPill(s) {
if (!s) return tag('muted', '—');
const v = String(s).toLowerCase();
if (['ok', 'ready', 'healthy', 'success'].includes(v)) return tag('ok', s);
if (['degraded', 'warn', 'pending', 'running'].includes(v)) return tag('warn', s);
if (['down', 'failed', 'error'].includes(v)) return tag('err', s);
return tag('info', s);
}
function ts(s) { return escapeHtml((s || '').replace('T', ' ').slice(0, 19)); }
// ---- renderers ----
function renderProviders(rows) {
if (!rows.length) return `<div class="card"><div class="muted">No providers configured.</div></div>`;
return `<div class="grid cols-2">${rows.map(p => `
<div class="card">
<div class="card-title"><h2>${escapeHtml(p.name || p.id)}</h2>${statusPill(p.status)}</div>
<div class="muted mb-2">${escapeHtml(p.description || '')}</div>
<pre class="mono small">id: ${escapeHtml(p.id || '')}
model: ${escapeHtml(p.default_model || '-')}
source: ${escapeHtml(p.source_file || '')}
base: ${escapeHtml(p.base_url || '-')}
env: ${(p.requires_env || []).join(', ') || '(none)'}</pre>
</div>`).join('')}</div>`;
}
function renderFunctions(rows) {
if (!rows.length) return `<div class="card"><div class="muted">No edge functions registered.</div></div>`;
return `<div class="grid cols-2">${rows.map(f => `
<div class="card">
<div class="card-title"><h2>${escapeHtml(f.title || f.slug)}</h2>${tag('info', `${f.method || 'GET'} · ${f.runtime || ''}`)}</div>
<div class="muted mb-2">${escapeHtml(f.description || '')}</div>
<pre class="mono small">route: ${escapeHtml(f.route || '')}
source: ${escapeHtml(f.source_file || '')}
env: ${(f.requires_env || []).join(', ') || '(none)'}</pre>
</div>`).join('')}</div>`;
}
function renderHealth(rows) {
return `
<div class="card mb-3">
<div class="row">
<button class="btn" id="files-capture-health">Capture snapshot</button>
<span class="muted ml-2">Probes Postgres / MCP / HF / Gitea from the browser, then inserts a row.</span>
</div>
</div>
<div class="card">
<div class="card-title"><h2>Recent snapshots</h2><span class="muted mono">${rows.length} rows</span></div>
${rows.length === 0 ? '<div class="muted">No snapshots yet.</div>' : `
<table class="table">
<thead><tr><th>captured</th><th>overall</th><th>ms</th><th>pg</th><th>mcp</th><th>hf</th><th>gitea</th></tr></thead>
<tbody>${rows.map(r => `
<tr>
<td class="mono small">${ts(r.captured_at)}</td>
<td>${statusPill(r.overall_status)}</td>
<td class="mono">${r.total_latency_ms ?? ''}</td>
<td>${r.supabase?.ok ? '✓' : '·'}</td>
<td>${r.mcp?.ok ? '✓' : '·'}</td>
<td>${r.huggingface?.ok ? '✓' : '·'}</td>
<td>${r.gitea?.ok ? '✓' : '·'}</td>
</tr>`).join('')}
</tbody>
</table>`}
</div>`;
}
function renderDemand(rows) {
if (!rows.length) return `<div class="card"><div class="muted">No demand runs received. Configure <code>CXCLOUD_MCP_URL</code> in <code>cxai-demand</code>.</div></div>`;
return `<div class="card">
<table class="table">
<thead><tr><th>received</th><th>date</th><th>mode</th><th>designs</th><th>ok</th><th>rej</th><th>quality</th><th>platforms</th></tr></thead>
<tbody>${rows.map(r => `
<tr>
<td class="mono small">${ts(r.received_at)}</td>
<td>${escapeHtml(r.run_date || '')}</td>
<td>${r.dry_run ? tag('warn', 'dry') : tag('ok', 'live')}</td>
<td class="mono">${r.total_designs || 0}</td>
<td class="mono ok">${r.accepted_designs || 0}</td>
<td class="mono err">${r.rejected_designs || 0}</td>
<td class="mono">${r.average_quality_score != null ? Number(r.average_quality_score).toFixed(2) : '—'}</td>
<td class="muted small">${escapeHtml(Object.keys(r.by_platform || {}).join(', ') || '—')}</td>
</tr>`).join('')}
</tbody>
</table>
</div>`;
}
function renderWatcher(rows) {
if (!rows.length) return `<div class="card"><div class="muted">No watcher events yet.</div></div>`;
return `<div class="card">
<table class="table">
<thead><tr><th>when</th><th>kind</th><th>path</th><th>tool</th><th>status</th><th>ms</th></tr></thead>
<tbody>${rows.map(e => `
<tr>
<td class="mono small">${escapeHtml((e.occurred_at || '').replace('T', ' ').slice(11, 19))}</td>
<td>${tag(e.event_kind === 'created' ? 'ok' : e.event_kind === 'removed' ? 'err' : 'info', e.event_kind)}</td>
<td class="mono small">${escapeHtml(e.path || '')}</td>
<td class="muted small">${escapeHtml(e.action_tool || '-')}</td>
<td>${statusPill(e.status)}</td>
<td class="mono">${e.latency_ms ?? ''}</td>
</tr>`).join('')}
</tbody>
</table>
</div>`;
}
function renderCategories(rows) {
if (!rows.length) return `<div class="card"><div class="muted">No file-manager categories.</div></div>`;
return `<div class="grid cols-2">${rows.map(c => `
<div class="card">
<div class="card-title"><h2>${escapeHtml(c.category || '')}</h2><span class="muted mono">${(c.extensions || []).length} ext</span></div>
<div class="small">${(c.extensions || []).map(x => `<span class="pill info">.${escapeHtml(x)}</span>`).join(' ')}</div>
</div>`).join('')}</div>`;
}
const RENDERERS = {
providers: renderProviders,
functions: renderFunctions,
health: renderHealth,
demand: renderDemand,
watcher: renderWatcher,
categories: renderCategories,
};
async function loadTab(host, tabId) {
activeTab = tabId;
const tab = TABS.find(t => t.id === tabId);
const body = host.querySelector('#files-body');
body.innerHTML = `<div class="card"><div class="muted">Loading ${escapeHtml(tab.label)}…</div></div>`;
host.querySelectorAll('#files-tabs .tab').forEach(b => b.classList.toggle('active', b.dataset.tab === tabId));
try {
const data = await rest(tab.endpoint);
const rows = Array.isArray(data) ? data : [];
body.innerHTML = RENDERERS[tabId](rows);
bindActions(host);
} catch (e) {
body.innerHTML = `<div class="card">
<div class="card-title"><h2>Failed to load ${escapeHtml(tab.label)}</h2></div>
<pre class="mono small">${escapeHtml(e.message || String(e))}</pre>
<div class="muted small">Confirm <code>CXAI_FILES_UPSTREAM</code> and <code>FILES_ANON_KEY</code> are set on the backend.</div>
</div>`;
}
}
function bindActions(host) {
const capture = host.querySelector('#files-capture-health');
if (!capture) return;
capture.onclick = async () => {
capture.disabled = true; capture.textContent = 'Capturing…';
const t0 = performance.now();
const probe = async (name, fn) => {
const s = performance.now();
try { const r = await fn(); return { name, ok: !!r, latency_ms: Math.round(performance.now() - s) }; }
catch { return { name, ok: false, latency_ms: Math.round(performance.now() - s) }; }
};
const probes = await Promise.all([
probe('supabase', () => rest('/mcp_providers?select=id&limit=1')),
probe('mcp', () => fetch('/api/health').then(r => r.ok)),
probe('huggingface', () => fetch('https://router.huggingface.co/', { mode: 'no-cors' })),
probe('gitea', () => fetch('https://cxai-studio.com/git/api/v1/version').then(r => r.ok)),
]);
const okCount = probes.filter(p => p.ok).length;
const overall = okCount === 4 ? 'ok' : okCount >= 2 ? 'degraded' : 'down';
try {
await restPost('/platform_health', {
overall_status: overall,
total_latency_ms: Math.round(performance.now() - t0),
supabase: probes[0], mcp: probes[1], huggingface: probes[2], gitea: probes[3],
raw: { source: 'cxwebapp', probes },
});
ok('Snapshot captured', 'Files');
} catch (e) {
err(`Capture failed: ${e.message}`, 'Files');
}
await loadTab(host, 'health');
};
}
registerPane('files', {
label: 'Files · Platform',
async init(host) {
host.innerHTML = TPL;
host.querySelectorAll('#files-tabs .tab').forEach(b => {
b.addEventListener('click', () => loadTab(host, b.dataset.tab));
});
host.querySelector('#files-refresh').addEventListener('click', () => loadTab(host, activeTab));
await loadTab(host, activeTab);
},
async refresh(host) {
await loadTab(host, activeTab);
},
});