CxWebApp/static/js/panes/inbox.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

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);
},
});