Some checks are pending
build-and-push / image (push) Waiting to run
- 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.
237 lines
10 KiB
JavaScript
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);
|
|
},
|
|
});
|