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.
169 lines
7.8 KiB
JavaScript
169 lines
7.8 KiB
JavaScript
// panes/lang.js — language pipelines with cards, examples, run history.
|
|
import { registerPane } from '../app.js';
|
|
import { jget, jpost, escapeHtml } from '../lib/api.js';
|
|
import { fmtJSON, err, ok, copyToClipboard, historyStore } from '../lib/ui.js';
|
|
|
|
const runHist = historyStore('cx_lang_runs', 20);
|
|
|
|
const TPL = `
|
|
<div class="pane-head">
|
|
<div><div class="title">Lang</div><div class="sub">Run language pipelines via the sidecar.</div></div>
|
|
<div class="grow"></div>
|
|
<span class="pill" id="lang-pill"><span class="dot"></span><span id="lang-pill-text">—</span></span>
|
|
<button class="btn btn-secondary" id="lang-refresh">Refresh</button>
|
|
</div>
|
|
|
|
<div class="grid" style="grid-template-columns: 320px 1fr; gap: 16px;">
|
|
<div class="card no-pad scroll" style="max-height:74vh">
|
|
<div class="p-2"><input class="input" id="lang-search" placeholder="filter pipelines…"/></div>
|
|
<div id="lang-pipelines">loading…</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title">
|
|
<h2 id="lang-name">Select a pipeline</h2>
|
|
<span class="muted xsmall" id="lang-status">—</span>
|
|
</div>
|
|
<p class="muted small" id="lang-desc">Pick a pipeline on the left.</p>
|
|
|
|
<div id="lang-body" hidden>
|
|
<div class="card-title"><h2 style="font-size:14px">Examples</h2></div>
|
|
<div class="chips mb-3" id="lang-examples"></div>
|
|
|
|
<label class="field"><span class="lbl">Input (JSON)</span>
|
|
<textarea class="input mono" id="lang-input" rows="8">{}</textarea>
|
|
</label>
|
|
<div class="btn-row mt-3">
|
|
<button class="btn btn-primary" id="lang-run">Run</button>
|
|
<button class="btn btn-secondary" id="lang-format">Format JSON</button>
|
|
<button class="btn btn-secondary" id="lang-copy-input">Copy input</button>
|
|
<button class="btn btn-ghost" id="lang-health">Health</button>
|
|
</div>
|
|
|
|
<div class="divider"></div>
|
|
<div class="card-title"><h2 style="font-size:14px">Result</h2>
|
|
<div class="flex gap-2">
|
|
<span class="muted xsmall" id="lang-meta"></span>
|
|
<button class="btn btn-ghost" id="lang-copy-out">Copy</button>
|
|
</div>
|
|
</div>
|
|
<pre id="lang-result" class="code muted" style="max-height:32vh">—</pre>
|
|
|
|
<details class="card-d mt-3"><summary>Recent runs</summary>
|
|
<div id="lang-history" class="body"></div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
let pipelines = [];
|
|
let current = null;
|
|
|
|
function renderList(host) {
|
|
const q = host.querySelector('#lang-search').value.trim().toLowerCase();
|
|
const items = pipelines.filter(p => !q || (p.name + ' ' + (p.description || '')).toLowerCase().includes(q));
|
|
host.querySelector('#lang-pipelines').innerHTML = items.length
|
|
? items.map(p => `<button class="nav-item" data-p="${escapeHtml(p.name)}">
|
|
<span class="grow">
|
|
<div class="mono">${escapeHtml(p.name)}</div>
|
|
<div class="muted xsmall truncate">${escapeHtml(p.description || '')}</div>
|
|
</span>
|
|
</button>`).join('')
|
|
: '<div class="muted xsmall p-2">no pipelines</div>';
|
|
host.querySelectorAll('[data-p]').forEach(b => b.addEventListener('click', () => select(host, b.dataset.p)));
|
|
}
|
|
|
|
function renderHistory(host) {
|
|
const list = runHist.list().filter(r => !current || r.name === current.name);
|
|
host.querySelector('#lang-history').innerHTML = list.length
|
|
? list.map((r, i) => `<button class="nav-item" data-hi="${i}">
|
|
<span class="muted xsmall mono">${new Date(r._ts).toLocaleTimeString()}</span>
|
|
<span class="grow truncate">${escapeHtml(r.name)}</span>
|
|
<span class="pill ${r.ok ? 'ok' : 'err'}">${r.ok ? `${r.ms || '?'}ms` : 'err'}</span>
|
|
</button>`).join('')
|
|
: '<div class="muted xsmall p-2">no runs</div>';
|
|
host.querySelectorAll('#lang-history [data-hi]').forEach(b => b.addEventListener('click', () => {
|
|
const r = list[+b.dataset.hi];
|
|
if (r) { host.querySelector('#lang-input').value = fmtJSON(r.input); host.querySelector('#lang-result').textContent = fmtJSON(r.output); }
|
|
}));
|
|
}
|
|
|
|
function select(host, name) {
|
|
current = pipelines.find(p => p.name === name);
|
|
if (!current) return;
|
|
host.querySelector('#lang-name').textContent = current.name;
|
|
host.querySelector('#lang-desc').textContent = current.description || '—';
|
|
host.querySelector('#lang-body').hidden = false;
|
|
|
|
const ex = current.examples || current.sample_inputs || [];
|
|
host.querySelector('#lang-examples').innerHTML = ex.length
|
|
? ex.map((e, i) => `<button class="chip" data-ex="${i}">${escapeHtml(e.label || e.name || `example ${i + 1}`)}</button>`).join('')
|
|
: '<span class="muted xsmall">no examples</span>';
|
|
host.querySelectorAll('#lang-examples .chip').forEach(b => b.addEventListener('click', () => {
|
|
const e = ex[+b.dataset.ex];
|
|
host.querySelector('#lang-input').value = fmtJSON(e.input ?? e.value ?? e);
|
|
}));
|
|
|
|
host.querySelector('#lang-input').value = fmtJSON(current.default_input || {});
|
|
renderHistory(host);
|
|
}
|
|
|
|
async function refresh(host) {
|
|
try {
|
|
const r = await jget('/api/lang/pipelines');
|
|
pipelines = r.pipelines || [];
|
|
renderList(host);
|
|
host.querySelector('#lang-pill').classList.remove('err', 'warn'); host.querySelector('#lang-pill').classList.add('ok');
|
|
host.querySelector('#lang-pill-text').textContent = `${pipelines.length} pipelines`;
|
|
} catch (e) {
|
|
host.querySelector('#lang-pipelines').innerHTML = `<div class="muted xsmall p-2">${escapeHtml(e.message)}</div>`;
|
|
host.querySelector('#lang-pill').classList.add('err'); host.querySelector('#lang-pill-text').textContent = 'down';
|
|
}
|
|
}
|
|
|
|
registerPane('lang', {
|
|
label: 'Lang',
|
|
init(host) {
|
|
host.innerHTML = TPL;
|
|
host.querySelector('#lang-refresh').addEventListener('click', () => refresh(host));
|
|
host.querySelector('#lang-search').addEventListener('input', () => renderList(host));
|
|
|
|
host.querySelector('#lang-run').addEventListener('click', async () => {
|
|
if (!current) return err('select a pipeline');
|
|
let input;
|
|
try { input = JSON.parse(host.querySelector('#lang-input').value || '{}'); }
|
|
catch (e) { return err(`bad JSON: ${e.message}`); }
|
|
host.querySelector('#lang-status').textContent = 'running…';
|
|
const t0 = performance.now();
|
|
try {
|
|
const r = await jpost(`/api/lang/pipelines/${encodeURIComponent(current.name)}`, { input });
|
|
const ms = +r.duration_ms || (performance.now() - t0).toFixed(0);
|
|
host.querySelector('#lang-result').textContent = fmtJSON(r);
|
|
host.querySelector('#lang-status').textContent = `ok · ${ms}ms`;
|
|
host.querySelector('#lang-meta').textContent = `${ms}ms`;
|
|
runHist.push({ name: current.name, input, output: r, ms, ok: true });
|
|
renderHistory(host); ok('done');
|
|
} catch (e) {
|
|
host.querySelector('#lang-result').textContent = fmtJSON(e.body ?? { error: e.message });
|
|
host.querySelector('#lang-status').textContent = `error ${e.status ?? ''}`;
|
|
runHist.push({ name: current.name, input, error: e.message, ok: false });
|
|
renderHistory(host); err(e.message);
|
|
}
|
|
});
|
|
host.querySelector('#lang-format').addEventListener('click', () => {
|
|
const ta = host.querySelector('#lang-input');
|
|
try { ta.value = fmtJSON(JSON.parse(ta.value || '{}')); } catch (e) { err('bad JSON: ' + e.message); }
|
|
});
|
|
host.querySelector('#lang-copy-input').addEventListener('click', () => copyToClipboard(host.querySelector('#lang-input').value));
|
|
host.querySelector('#lang-copy-out').addEventListener('click', () => copyToClipboard(host.querySelector('#lang-result').textContent));
|
|
host.querySelector('#lang-health').addEventListener('click', async () => {
|
|
try { host.querySelector('#lang-result').textContent = fmtJSON(await jget('/api/lang/healthz')); }
|
|
catch (e) { host.querySelector('#lang-result').textContent = e.message; }
|
|
});
|
|
|
|
refresh(host);
|
|
},
|
|
refresh,
|
|
});
|