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.
201 lines
9.8 KiB
JavaScript
201 lines
9.8 KiB
JavaScript
// panes/inbox.js — sweep & route CxAI/_inbox via MCP tools with slider, history, presets.
|
|
import { registerPane } from '../app.js';
|
|
import { mcp, escapeHtml } from '../lib/api.js';
|
|
import { ok, err, fmtJSON, meterRow, historyStore, copyToClipboard } from '../lib/ui.js';
|
|
|
|
const sweepHist = historyStore('cx_inbox_sweeps', 15);
|
|
const routeHist = historyStore('cx_inbox_routes', 15);
|
|
|
|
const TPL = `
|
|
<div class="pane-head">
|
|
<div><div class="title">Inbox</div><div class="sub">Route artifacts through the CxAI inbox classifier.</div></div>
|
|
<div class="grow"></div>
|
|
<button class="btn btn-secondary" id="inbox-refresh">Sweep now</button>
|
|
</div>
|
|
|
|
<div class="grid cols-3 mb-3" id="inbox-kpis"></div>
|
|
|
|
<div class="grid cols-2 mb-3">
|
|
<div class="card">
|
|
<div class="card-title"><h2>Sweep</h2><span class="muted mono xsmall" id="inbox-sweep-meta">idle</span></div>
|
|
<form id="inbox-sweep-form" class="grid cols-2 gap-3">
|
|
<label class="field">
|
|
<span class="lbl">Mode</span>
|
|
<select class="input" id="inbox-mode">
|
|
<option value="all">all (inbox + needs-review)</option>
|
|
<option value="inbox">inbox only</option>
|
|
<option value="needs_review">needs-review only</option>
|
|
</select>
|
|
</label>
|
|
<label class="field">
|
|
<span class="lbl">Limit</span>
|
|
<input class="input" id="inbox-limit" type="number" min="0" value="0" placeholder="0 = no limit"/>
|
|
</label>
|
|
<div style="grid-column: span 2">
|
|
<label class="lbl">Confidence threshold <span class="mono" id="inbox-thr-val">0.70</span></label>
|
|
<input type="range" class="slider" id="inbox-threshold" min="0" max="1" step="0.05" value="0.7" style="width:100%"/>
|
|
<div id="inbox-thr-meter"></div>
|
|
</div>
|
|
<label class="check"><input type="checkbox" id="inbox-dry" checked/> dry-run</label>
|
|
<label class="check"><input type="checkbox" id="inbox-verbose"/> verbose</label>
|
|
<div class="btn-row" style="grid-column: span 2">
|
|
<button class="btn btn-primary" type="submit">Run sweep</button>
|
|
<button type="button" class="btn btn-secondary" data-preset="strict">Strict (0.9)</button>
|
|
<button type="button" class="btn btn-secondary" data-preset="normal">Normal (0.7)</button>
|
|
<button type="button" class="btn btn-secondary" data-preset="loose">Loose (0.4)</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title"><h2>Route a single file</h2><span class="muted xsmall" id="inbox-route-meta"></span></div>
|
|
<form id="inbox-route-form" class="grid gap-2 mb-2">
|
|
<input class="input" id="inbox-route-path" placeholder="/absolute/path/to/file.md"/>
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" type="submit">Route</button>
|
|
<label class="check"><input type="checkbox" id="inbox-route-dry" checked/> dry-run</label>
|
|
</div>
|
|
</form>
|
|
<details class="card-d"><summary>Recent routes</summary>
|
|
<div id="inbox-route-history" class="body"></div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid cols-2">
|
|
<div class="card">
|
|
<div class="card-title"><h2>Last sweep</h2><button class="btn btn-ghost" id="inbox-copy">Copy</button></div>
|
|
<pre id="inbox-result" class="code muted" style="max-height:36vh">no sweep yet</pre>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-title"><h2>Route result</h2></div>
|
|
<pre id="inbox-route-result" class="code muted" style="max-height:36vh">no route yet</pre>
|
|
|
|
<div class="divider"></div>
|
|
<div class="card-title"><h2 style="font-size:14px">Sweep history</h2></div>
|
|
<div id="inbox-sweep-history"></div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
function kpi(label, value, sub) {
|
|
return `<div class="kpi"><div class="label">${escapeHtml(label)}</div><div class="value">${escapeHtml(String(value))}</div><div class="sub">${escapeHtml(sub || '')}</div></div>`;
|
|
}
|
|
|
|
function updateThr(host) {
|
|
const v = +host.querySelector('#inbox-threshold').value;
|
|
host.querySelector('#inbox-thr-val').textContent = v.toFixed(2);
|
|
const state = v >= 0.8 ? 'ok' : v >= 0.5 ? 'warn' : 'err';
|
|
host.querySelector('#inbox-thr-meter').innerHTML = meterRow('confidence', v * 100, `${(v * 100).toFixed(0)}%`, state);
|
|
}
|
|
|
|
function renderHist(host) {
|
|
const sw = sweepHist.list();
|
|
host.querySelector('#inbox-sweep-history').innerHTML = sw.length
|
|
? sw.map((r, i) => `<button class="nav-item" data-hi="${i}">
|
|
<span class="muted xsmall mono">${new Date(r._ts).toLocaleTimeString()}</span>
|
|
<span>${escapeHtml(r.mode)} · thr=${r.threshold}</span>
|
|
<span class="pill ${r.ok ? 'ok' : 'err'}">${r.ok ? `${r.routed ?? 0} routed` : 'error'}</span>
|
|
</button>`).join('')
|
|
: '<div class="muted xsmall p-2">no sweeps yet</div>';
|
|
host.querySelectorAll('#inbox-sweep-history [data-hi]').forEach(b => b.addEventListener('click', () => {
|
|
host.querySelector('#inbox-result').textContent = fmtJSON(sweepHist.list()[+b.dataset.hi]);
|
|
}));
|
|
|
|
const ro = routeHist.list();
|
|
host.querySelector('#inbox-route-history').innerHTML = ro.length
|
|
? ro.map((r, i) => `<button class="nav-item" data-rhi="${i}">
|
|
<span class="muted xsmall mono">${new Date(r._ts).toLocaleTimeString()}</span>
|
|
<span class="truncate">${escapeHtml(r.path)}</span>
|
|
<span class="pill ${r.ok ? 'ok' : 'err'}">${r.target || (r.ok ? 'ok' : 'err')}</span>
|
|
</button>`).join('')
|
|
: '<div class="muted xsmall p-2">no routes yet</div>';
|
|
host.querySelectorAll('#inbox-route-history [data-rhi]').forEach(b => b.addEventListener('click', () => {
|
|
const r = routeHist.list()[+b.dataset.rhi];
|
|
host.querySelector('#inbox-route-path').value = r.path;
|
|
host.querySelector('#inbox-route-result').textContent = fmtJSON(r);
|
|
}));
|
|
}
|
|
|
|
async function runSweep(host) {
|
|
const mode = host.querySelector('#inbox-mode').value;
|
|
const threshold = parseFloat(host.querySelector('#inbox-threshold').value);
|
|
const limit = +host.querySelector('#inbox-limit').value || 0;
|
|
const dry_run = host.querySelector('#inbox-dry').checked;
|
|
const verbose = host.querySelector('#inbox-verbose').checked;
|
|
host.querySelector('#inbox-sweep-meta').textContent = 'running…';
|
|
host.querySelector('#inbox-result').textContent = 'running…';
|
|
const args = { mode, threshold, dry_run };
|
|
if (limit > 0) args.limit = limit;
|
|
if (verbose) args.verbose = true;
|
|
try {
|
|
const r = await mcp.call('inbox_sweep_tool', args);
|
|
host.querySelector('#inbox-result').textContent = fmtJSON(r);
|
|
const total = (Array.isArray(r?.routed) ? r.routed.length : r?.routed) ?? r?.summary?.routed ?? 0;
|
|
host.querySelector('#inbox-sweep-meta').textContent = dry_run ? `dry-run · ${total} candidates` : `routed ${total}`;
|
|
const pill = document.getElementById('nav-inbox-pill');
|
|
if (pill && !dry_run) pill.textContent = String(total || '');
|
|
sweepHist.push({ mode, threshold, dry_run, routed: total, ok: true, response: r });
|
|
renderHist(host); updateKpis(host, r);
|
|
ok(`Sweep complete${dry_run ? ' (dry)' : ''}`);
|
|
} catch (e) {
|
|
host.querySelector('#inbox-result').textContent = fmtJSON(e.body ?? { error: e.message });
|
|
host.querySelector('#inbox-sweep-meta').textContent = `error ${e.status ?? ''}`;
|
|
sweepHist.push({ mode, threshold, dry_run, ok: false, error: e.message });
|
|
renderHist(host);
|
|
err(`sweep: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
async function routeOne(host) {
|
|
const path = host.querySelector('#inbox-route-path').value.trim();
|
|
if (!path) return;
|
|
const dry_run = host.querySelector('#inbox-route-dry').checked;
|
|
host.querySelector('#inbox-route-meta').textContent = 'routing…';
|
|
host.querySelector('#inbox-route-result').textContent = 'routing…';
|
|
try {
|
|
const r = await mcp.call('route_file_tool', { path, dry_run });
|
|
host.querySelector('#inbox-route-result').textContent = fmtJSON(r);
|
|
const target = r?.target || r?.destination || 'ok';
|
|
host.querySelector('#inbox-route-meta').textContent = target;
|
|
routeHist.push({ path, dry_run, target, ok: true });
|
|
renderHist(host);
|
|
ok(`Routed ${path.split('/').pop()}`);
|
|
} catch (e) {
|
|
host.querySelector('#inbox-route-result').textContent = fmtJSON(e.body ?? { error: e.message });
|
|
host.querySelector('#inbox-route-meta').textContent = `error ${e.status ?? ''}`;
|
|
routeHist.push({ path, dry_run, ok: false, error: e.message });
|
|
renderHist(host);
|
|
err(`route: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
function updateKpis(host, r) {
|
|
const routed = (Array.isArray(r?.routed) ? r.routed.length : r?.routed) ?? r?.summary?.routed ?? 0;
|
|
const skipped = (Array.isArray(r?.skipped) ? r.skipped.length : r?.skipped) ?? r?.summary?.skipped ?? 0;
|
|
const queued = r?.summary?.queued ?? r?.queued ?? '—';
|
|
host.querySelector('#inbox-kpis').innerHTML = [
|
|
kpi('Routed (last)', routed, 'files moved'),
|
|
kpi('Skipped (last)', skipped, 'low-confidence'),
|
|
kpi('Queue', queued, 'remaining'),
|
|
].join('');
|
|
}
|
|
|
|
registerPane('inbox', {
|
|
label: 'Inbox',
|
|
init(host) {
|
|
host.innerHTML = TPL;
|
|
updateThr(host); updateKpis(host, {});
|
|
host.querySelector('#inbox-threshold').addEventListener('input', () => updateThr(host));
|
|
host.querySelectorAll('[data-preset]').forEach(b => b.addEventListener('click', () => {
|
|
const v = { strict: 0.9, normal: 0.7, loose: 0.4 }[b.dataset.preset];
|
|
host.querySelector('#inbox-threshold').value = v; updateThr(host);
|
|
}));
|
|
host.querySelector('#inbox-sweep-form').addEventListener('submit', (e) => { e.preventDefault(); runSweep(host); });
|
|
host.querySelector('#inbox-route-form').addEventListener('submit', (e) => { e.preventDefault(); routeOne(host); });
|
|
host.querySelector('#inbox-refresh').addEventListener('click', () => runSweep(host));
|
|
host.querySelector('#inbox-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#inbox-result').textContent));
|
|
renderHist(host);
|
|
},
|
|
});
|