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.
200 lines
9.6 KiB
JavaScript
200 lines
9.6 KiB
JavaScript
// panes/demand.js — cxai-demand jobs with presets, live polling, report viewer.
|
|
import { registerPane } from '../app.js';
|
|
import { jget, jpost, escapeHtml, statusClass } from '../lib/api.js';
|
|
import { ok, err, fmtJSON, copyToClipboard } from '../lib/ui.js';
|
|
|
|
const PRESETS = {
|
|
lite: { designs_per_run: 1, top_trends: 5, variations_per_trend: 1, min_score: 0.7, quantum: false },
|
|
normal: { designs_per_run: 3, top_trends: 10, variations_per_trend: 2, min_score: 0.6, quantum: false },
|
|
aggressive: { designs_per_run: 10,top_trends: 25, variations_per_trend: 4, min_score: 0.4, quantum: true },
|
|
};
|
|
|
|
const TPL = `
|
|
<div class="pane-head">
|
|
<div><div class="title">Demand</div><div class="sub">Trend-driven design jobs.</div></div>
|
|
<div class="grow"></div>
|
|
<label class="check"><input type="checkbox" id="demand-auto" checked/> auto-refresh (5s)</label>
|
|
<button class="btn btn-secondary" id="demand-refresh">Refresh</button>
|
|
</div>
|
|
|
|
<div class="grid cols-4 mb-3" id="demand-kpis"></div>
|
|
|
|
<div class="grid cols-2 mb-3">
|
|
<div class="card">
|
|
<div class="card-title"><h2>Run config</h2><span class="muted xsmall" id="demand-status">—</span></div>
|
|
<div class="chips mb-3">
|
|
<button class="chip" data-preset="lite" type="button">Lite</button>
|
|
<button class="chip" data-preset="normal" type="button">Normal</button>
|
|
<button class="chip" data-preset="aggressive" type="button">Aggressive</button>
|
|
</div>
|
|
<form id="demand-form" class="grid cols-2 gap-3">
|
|
<label class="field"><span class="lbl">Designs / run</span><input class="input" id="demand-count" type="number" min="1" value="3"/></label>
|
|
<label class="field"><span class="lbl">Top trends</span><input class="input" id="demand-top" type="number" placeholder="—"/></label>
|
|
<label class="field"><span class="lbl">Variations / trend</span><input class="input" id="demand-var" type="number" placeholder="—"/></label>
|
|
<label class="field"><span class="lbl">Min score</span><input class="input" id="demand-min" type="number" step="0.01" placeholder="—"/></label>
|
|
<label class="field" style="grid-column: span 2">
|
|
<span class="lbl">Platform</span>
|
|
<select class="input" id="demand-platform"></select>
|
|
</label>
|
|
<label class="check"><input type="checkbox" id="demand-dry"/> dry-run</label>
|
|
<label class="check"><input type="checkbox" id="demand-up"/> upload</label>
|
|
<label class="check"><input type="checkbox" id="demand-web"/> web trends</label>
|
|
<label class="check"><input type="checkbox" id="demand-q"/> quantum</label>
|
|
<div class="btn-row" style="grid-column: span 2">
|
|
<button class="btn btn-primary" type="submit">Queue run</button>
|
|
<button class="btn btn-ghost" type="reset">Clear</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title"><h2>Jobs</h2><span class="muted xsmall" id="demand-job-meta">—</span></div>
|
|
<div id="demand-jobs" class="scroll" style="max-height:54vh">…</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-title"><h2>Reports</h2>
|
|
<div class="flex gap-2"><input class="input" id="demand-rfilter" placeholder="filter…" style="max-width:200px"/>
|
|
<button class="btn btn-ghost" id="demand-rcopy">Copy report</button></div>
|
|
</div>
|
|
<div class="grid cols-2 gap-3">
|
|
<div id="demand-reports" class="scroll" style="max-height:46vh"></div>
|
|
<pre id="demand-report-body" class="code muted" style="max-height:46vh">select a report</pre>
|
|
</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 num(v) { if (v === '' || v == null) return null; const n = +v; return Number.isFinite(n) ? n : null; }
|
|
|
|
let allReports = [];
|
|
|
|
async function loadJobs(host) {
|
|
try {
|
|
const r = await jget('/api/demand/runs');
|
|
const jobs = r.jobs || [];
|
|
host.querySelector('#demand-job-meta').textContent = `${jobs.length} total`;
|
|
host.querySelector('#demand-jobs').innerHTML = jobs.length ? jobs.map(j => {
|
|
const dur = j.finished_at && j.started_at ? `${(j.finished_at - j.started_at).toFixed(1)}s`
|
|
: (j.status === 'running' ? '…' : '—');
|
|
const cancelBtn = j.status === 'running'
|
|
? `<button class="act" data-cancel="${escapeHtml(j.id)}">cancel</button>` : '';
|
|
return `<div class="row">
|
|
<span class="mono muted xsmall">${escapeHtml(j.id)}</span>
|
|
<span class="pill ${statusClass(j.status)}">${escapeHtml(j.status)}</span>
|
|
<span class="muted mono xsmall">${dur}</span>
|
|
<div class="grow desc truncate">${escapeHtml(j.report_path || j.error || '')}</div>
|
|
${cancelBtn}
|
|
</div>`;
|
|
}).join('') : '<div class="muted" style="padding:12px">no jobs yet</div>';
|
|
|
|
host.querySelectorAll('[data-cancel]').forEach(b => b.addEventListener('click', async () => {
|
|
try { await jpost(`/api/demand/runs/${b.dataset.cancel}/cancel`, {}); ok('cancel requested'); loadJobs(host); }
|
|
catch (e) { err(e.message); }
|
|
}));
|
|
|
|
const running = jobs.filter(j => j.status === 'running').length;
|
|
const success = jobs.filter(j => j.status === 'success' || j.status === 'completed').length;
|
|
const failed = jobs.filter(j => (j.status || '').includes('fail') || (j.status || '').includes('error')).length;
|
|
host.querySelector('#demand-kpis').innerHTML = [
|
|
kpi('Jobs', jobs.length, 'all time'),
|
|
kpi('Running', running, 'now'),
|
|
kpi('Success', success, 'completed'),
|
|
kpi('Failed', failed, 'errors'),
|
|
].join('');
|
|
} catch (e) {
|
|
host.querySelector('#demand-jobs').innerHTML = `<div class="muted">${escapeHtml(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
function renderReports(host) {
|
|
const q = host.querySelector('#demand-rfilter').value.trim().toLowerCase();
|
|
const items = allReports.filter(it => !q || it.name.toLowerCase().includes(q));
|
|
host.querySelector('#demand-reports').innerHTML = items.length
|
|
? items.map(it => `<div class="row">
|
|
<a href="#" data-name="${escapeHtml(it.name)}" class="report-link grow truncate">${escapeHtml(it.name)}</a>
|
|
<span class="muted mono xsmall">${it.size}b</span>
|
|
</div>`).join('')
|
|
: '<div class="muted xsmall p-2">no reports</div>';
|
|
host.querySelectorAll('.report-link').forEach(b => b.addEventListener('click', async (e) => {
|
|
e.preventDefault();
|
|
try { host.querySelector('#demand-report-body').textContent = fmtJSON(await jget('/api/demand/reports/' + b.dataset.name)); }
|
|
catch (err2) { host.querySelector('#demand-report-body').textContent = err2.message; }
|
|
}));
|
|
}
|
|
|
|
async function loadReports(host) {
|
|
try {
|
|
const r = await jget('/api/demand/reports');
|
|
allReports = r.reports || [];
|
|
renderReports(host);
|
|
} catch (e) {
|
|
host.querySelector('#demand-reports').innerHTML = `<div class="muted">${escapeHtml(e.message)}</div>`;
|
|
}
|
|
}
|
|
|
|
async function refresh(host) {
|
|
try {
|
|
const platforms = (await jget('/api/demand/platforms')).platforms || [];
|
|
const sel = host.querySelector('#demand-platform');
|
|
if (sel && sel.children.length === 0) {
|
|
sel.innerHTML = `<option value="">(default)</option>` + platforms.map(p => `<option value="${p}">${p}</option>`).join('');
|
|
}
|
|
await loadJobs(host); await loadReports(host);
|
|
} catch (e) {
|
|
host.querySelector('#demand-status').textContent = 'upstream unavailable: ' + e.message;
|
|
}
|
|
}
|
|
|
|
let timer = null;
|
|
registerPane('demand', {
|
|
label: 'Demand',
|
|
init(host) {
|
|
host.innerHTML = TPL;
|
|
host.querySelector('#demand-refresh').addEventListener('click', () => refresh(host));
|
|
host.querySelector('#demand-rfilter').addEventListener('input', () => renderReports(host));
|
|
host.querySelector('#demand-rcopy').addEventListener('click', () => copyToClipboard(host.querySelector('#demand-report-body').textContent));
|
|
|
|
host.querySelectorAll('[data-preset]').forEach(b => b.addEventListener('click', () => {
|
|
const p = PRESETS[b.dataset.preset]; if (!p) return;
|
|
host.querySelector('#demand-count').value = p.designs_per_run;
|
|
host.querySelector('#demand-top').value = p.top_trends;
|
|
host.querySelector('#demand-var').value = p.variations_per_trend;
|
|
host.querySelector('#demand-min').value = p.min_score;
|
|
host.querySelector('#demand-q').checked = p.quantum;
|
|
}));
|
|
|
|
host.querySelector('#demand-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const body = {
|
|
designs_per_run: +host.querySelector('#demand-count').value || 0,
|
|
top_trends: num(host.querySelector('#demand-top').value),
|
|
variations_per_trend: num(host.querySelector('#demand-var').value),
|
|
min_score: num(host.querySelector('#demand-min').value),
|
|
platform: host.querySelector('#demand-platform').value || null,
|
|
dry_run: host.querySelector('#demand-dry').checked,
|
|
upload: host.querySelector('#demand-up').checked,
|
|
web_trends: host.querySelector('#demand-web').checked,
|
|
quantum: host.querySelector('#demand-q').checked,
|
|
};
|
|
try {
|
|
const r = await jpost('/api/demand/runs', body);
|
|
host.querySelector('#demand-status').textContent = `queued: ${r.job_id}`;
|
|
ok(`queued ${r.job_id}`);
|
|
await loadJobs(host);
|
|
} catch (e) { host.querySelector('#demand-status').textContent = 'error: ' + e.message; err(e.message); }
|
|
});
|
|
|
|
refresh(host);
|
|
timer = setInterval(() => {
|
|
if (!host.classList.contains('active')) return;
|
|
if (!host.querySelector('#demand-auto')?.checked) return;
|
|
loadJobs(host);
|
|
}, 5000);
|
|
},
|
|
refresh,
|
|
});
|