// 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 = `
Demand
Trend-driven design jobs.

Run config

Jobs

Reports

select a report
`; function kpi(label, value, sub) { return `
${escapeHtml(label)}
${escapeHtml(String(value))}
${escapeHtml(sub || '')}
`; } 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' ? `` : ''; return `
${escapeHtml(j.id)} ${escapeHtml(j.status)} ${dur}
${escapeHtml(j.report_path || j.error || '')}
${cancelBtn}
`; }).join('') : '
no jobs yet
'; 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 = `
${escapeHtml(e.message)}
`; } } 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 => `
${escapeHtml(it.name)} ${it.size}b
`).join('') : '
no reports
'; 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 = `
${escapeHtml(e.message)}
`; } } 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 = `` + platforms.map(p => ``).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, });