(d: DashboardData, opts: { token?: string } = {})
| 44 | |
| 45 | /** Render the dashboard as one self-contained HTML page (PURE). Chart.js via CDN; everything else |
| 46 | * inline. When `token` is given the page is a live CONTROL panel (toggles/buttons call the local |
| 47 | * API); without it the same page renders read-only. */ |
| 48 | export function buildDashboardHtml(d: DashboardData, opts: { token?: string } = {}): string { |
| 49 | const live = !!opts.token; |
| 50 | const card = (label: string, value: string, accent: string) => |
| 51 | `<div class="card"><div class="v" style="color:${accent}">${value}</div><div class="l">${label}</div></div>`; |
| 52 | |
| 53 | // ── Controls: config toggles/selects grouped, + scheduled tasks with enable/remove ── |
| 54 | const groups = [...new Set(d.controls.map(c => c.group))]; |
| 55 | const controlPanel = d.controls.length === 0 ? '' : groups.map(g => { |
| 56 | const rows = d.controls.filter(c => c.group === g).map(c => { |
| 57 | if (c.type === 'bool') { |
| 58 | const on = c.current === 'true'; |
| 59 | const btn = live |
| 60 | ? `<button class="tg ${on ? 'on' : ''}" onclick="act('config.set',{key:'${c.path}',value:${!on}})">${on ? 'ON' : 'OFF'}</button>` |
| 61 | : `<span class="${on ? 'ok' : 'dim'}">${on ? 'ON' : 'OFF'}</span>`; |
| 62 | return `<div class="ctl"><span>${esc(c.label)}</span>${btn}</div>`; |
| 63 | } |
| 64 | const sel = live |
| 65 | ? `<select onchange="act('config.set',{key:'${c.path}',value:this.value})">${(c.values ?? []).map(v => `<option${v === c.current ? ' selected' : ''}>${esc(v)}</option>`).join('')}</select>` |
| 66 | : `<span class="mono">${esc(c.current)}</span>`; |
| 67 | return `<div class="ctl"><span>${esc(c.label)}</span>${sel}</div>`; |
| 68 | }).join(''); |
| 69 | return `<div class="panel"><h2>${esc(g)}</h2>${rows}</div>`; |
| 70 | }).join(''); |
| 71 | // These knobs are read from config ONCE when a qodex session starts (config is not hot- |
| 72 | // reloaded mid-session). A toggle here writes the config file immediately, but a session |
| 73 | // that's already running keeps its old value until it restarts — the #1 "I turned it on |
| 74 | // but nothing changed" confusion. Say so, prominently, right above the toggles. |
| 75 | const controlNote = (live && d.controls.length) |
| 76 | ? `<p class="dim" style="margin:-4px 0 14px">Changes are written to <code>~/.qodex/config.yaml</code> and picked up on your <b>next message/task</b> — a running QodeX session hot-reloads these knobs at the start of each run (no restart needed).</p>` |
| 77 | : ''; |
| 78 | |
| 79 | const scheduleRows = d.schedules.length ? d.schedules.map(s => `<tr> |
| 80 | <td>${s.enabled ? '🟢' : '⚪'} <b>${esc(s.name)}</b>${s.recipe ? ` <span class="mono dim">${esc(s.recipe)}</span>` : ''}</td> |
| 81 | <td class="mono dim">${esc(s.cron)}</td> |
| 82 | <td class="r">${live ? `<button onclick="act('schedule.setEnabled',{id:'${esc(s.id)}',enabled:${!s.enabled}})">${s.enabled ? 'Disable' : 'Enable'}</button> <button class="danger" onclick="if(confirm('Remove ${esc(s.name)}?'))act('schedule.remove',{id:'${esc(s.id)}'})">Remove</button>` : `<span class="dim">${s.enabled ? 'enabled' : 'disabled'}</span>`}</td> |
| 83 | </tr>`).join('') : '<tr><td colspan="3" class="dim">No scheduled tasks. Add one with `qodex schedule add`.</td></tr>'; |
| 84 | const addForm = live ? `<div class="addform"> |
| 85 | <input id="s_name" placeholder="name (e.g. nightly-fix)"> |
| 86 | <input id="s_cron" placeholder="cron (@daily, 0 3 * * *)"> |
| 87 | <input id="s_prompt" placeholder="prompt — or maintain: dead-code | unused-imports/locals/params | lint-fix | dep-bump [--dry-run] [path]" style="flex:2"> |
| 88 | <select id="s_recipe"><option value="">plain</option><option value="verified-pr">verified-pr</option><option value="maintain">maintain (self-improving → verified PR)</option></select> |
| 89 | <input id="s_deliver" placeholder="deliver (telegram:<id>)"> |
| 90 | <button onclick="act('schedule.add',{name:s_name.value,cron:s_cron.value,prompt:s_prompt.value,recipe:s_recipe.value,deliver:s_deliver.value})">Schedule</button> |
| 91 | </div>` : ''; |
| 92 | const schedulePanel = `<div class="panel"><h2>Scheduled tasks</h2><table><thead><tr><th>task</th><th>cron</th><th class="r">${live ? 'actions' : 'state'}</th></tr></thead><tbody>${scheduleRows}</tbody></table>${addForm}</div>`; |
| 93 | |
| 94 | // Run history + trust receipts (proof-carrying autonomy, surfaced). |
| 95 | const runRows = d.runs.length ? d.runs.map(r => { |
| 96 | const rc = r.receipt; |
| 97 | const verdict = rc ? `<span class="${rc.status === 'opened' || rc.status === 'done' ? 'ok' : rc.status === 'blocked' ? 'warn' : 'dim'}">🧾 ${esc(rc.status)}</span>${rc.prUrl ? ` <a href="${esc(rc.prUrl)}" target="_blank" class="mono">PR</a>` : ''}${rc.verification?.length ? ` <span class="dim mono">${rc.verification.map(v => (v.passed ? '✓' : '✗') + v.command).join(' ')}</span>` : ''}` : '<span class="dim">—</span>'; |
| 98 | return `<tr><td><b>${esc(r.schedule)}</b>${r.recipe ? ` <span class="mono dim">${esc(r.recipe)}</span>` : ''}</td><td class="dim">${esc(r.when)}</td><td>${esc(r.status)}</td><td>${verdict}</td></tr>`; |
| 99 | }).join('') : '<tr><td colspan="4" class="dim">No runs yet. A verified-pr / maintain schedule produces a 🧾 receipt.</td></tr>'; |
| 100 | const runsPanel = `<div class="panel"><h2>Run history & receipts</h2><table><thead><tr><th>schedule · recipe</th><th>when</th><th>status</th><th>receipt</th></tr></thead><tbody>${runRows}</tbody></table></div>`; |
| 101 | |
| 102 | // Maintain status & analytics (self-improvement loop at a glance). |
| 103 | const ms = d.maintainStats; |
no test coverage detected