From 76392fbefb52235456e36babc20391562743a6ff Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:59:32 +0000 Subject: [PATCH 1/4] feat(dashboard): add dependency-free CodegenRestDashboard with commands, local proxy UI, CF webhook, auto-refresh watchers, templates + follow-up automation - CodegenRestDashboard/.env.example and .gitignore for secrets - Node commands: create/resume/list/get/generate_setup_commands - Vanilla JS dashboard: header active count + hover list, pinned runs, run dialog with streaming logs, filters, model dropdown, templates tab - Local http server proxy to inject Authorization; no token in browser - Cloudflare Worker webhook handler at /webhook - Offline/mock mode for local testing without network Co-authored-by: Zeeeepa --- .gitignore | 5 + CodegenRestDashboard/.env.example | 11 ++ CodegenRestDashboard/README.md | 55 ++++++++ .../commands/create_agent_run.js | 48 +++++++ .../commands/generate_setup_commands.js | 31 +++++ .../commands/get_agent_run.js | 35 +++++ CodegenRestDashboard/commands/index.js | 13 ++ .../commands/list_agent_runs.js | 28 ++++ .../commands/resume_agent_run.js | 31 +++++ CodegenRestDashboard/commands/runCommand.js | 15 +++ CodegenRestDashboard/dashboard/api/client.js | 23 ++++ .../dashboard/automation/followUpManager.js | 28 ++++ .../dashboard/components/header.js | 44 +++++++ .../dashboard/components/notifications.js | 12 ++ .../dashboard/components/pinnedRuns.js | 30 +++++ .../dashboard/components/runDialog.js | 75 +++++++++++ .../dashboard/components/runList.js | 76 +++++++++++ .../dashboard/components/tabControl.js | 31 +++++ .../dashboard/components/templateManager.js | 35 +++++ CodegenRestDashboard/dashboard/index.html | 33 +++++ CodegenRestDashboard/dashboard/main.js | 16 +++ .../dashboard/services/watcher.js | 27 ++++ CodegenRestDashboard/dashboard/state/store.js | 27 ++++ CodegenRestDashboard/dashboard/styles.css | 25 ++++ CodegenRestDashboard/mock/mockData.json | 7 + CodegenRestDashboard/server.js | 74 +++++++++++ CodegenRestDashboard/utils/apiClient.js | 124 ++++++++++++++++++ CodegenRestDashboard/utils/env.js | 25 ++++ CodegenRestDashboard/webhook_server.js | 42 ++++++ 29 files changed, 1026 insertions(+) create mode 100644 CodegenRestDashboard/.env.example create mode 100644 CodegenRestDashboard/README.md create mode 100644 CodegenRestDashboard/commands/create_agent_run.js create mode 100644 CodegenRestDashboard/commands/generate_setup_commands.js create mode 100644 CodegenRestDashboard/commands/get_agent_run.js create mode 100644 CodegenRestDashboard/commands/index.js create mode 100644 CodegenRestDashboard/commands/list_agent_runs.js create mode 100644 CodegenRestDashboard/commands/resume_agent_run.js create mode 100644 CodegenRestDashboard/commands/runCommand.js create mode 100644 CodegenRestDashboard/dashboard/api/client.js create mode 100644 CodegenRestDashboard/dashboard/automation/followUpManager.js create mode 100644 CodegenRestDashboard/dashboard/components/header.js create mode 100644 CodegenRestDashboard/dashboard/components/notifications.js create mode 100644 CodegenRestDashboard/dashboard/components/pinnedRuns.js create mode 100644 CodegenRestDashboard/dashboard/components/runDialog.js create mode 100644 CodegenRestDashboard/dashboard/components/runList.js create mode 100644 CodegenRestDashboard/dashboard/components/tabControl.js create mode 100644 CodegenRestDashboard/dashboard/components/templateManager.js create mode 100644 CodegenRestDashboard/dashboard/index.html create mode 100644 CodegenRestDashboard/dashboard/main.js create mode 100644 CodegenRestDashboard/dashboard/services/watcher.js create mode 100644 CodegenRestDashboard/dashboard/state/store.js create mode 100644 CodegenRestDashboard/dashboard/styles.css create mode 100644 CodegenRestDashboard/mock/mockData.json create mode 100644 CodegenRestDashboard/server.js create mode 100644 CodegenRestDashboard/utils/apiClient.js create mode 100644 CodegenRestDashboard/utils/env.js create mode 100644 CodegenRestDashboard/webhook_server.js diff --git a/.gitignore b/.gitignore index 00f68e676..d57ef064b 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,8 @@ results.*.json codegen-examples/examples/swebench_agent_run/results/* codegen-examples/examples/swebench_agent_run/predictions/* codegen-examples/examples/swebench_agent_run/logs/* + +# CodegenRestDashboard secrets +CodegenRestDashboard/.env +CodegenRestDashboard/mock/*.json-local + diff --git a/CodegenRestDashboard/.env.example b/CodegenRestDashboard/.env.example new file mode 100644 index 000000000..d933ad931 --- /dev/null +++ b/CodegenRestDashboard/.env.example @@ -0,0 +1,11 @@ +# Copy to .env and fill your values. Do NOT commit the .env file. +CODEGEN_API_BASE=https://api.codegen.com +CODEGEN_ORG_ID=323 +CODEGEN_TOKEN=sk-REPLACE_ME +# Optional: run in mock/offline mode (server serves fixtures; no network calls) +CODEGEN_OFFLINE=0 +# Optional: Webhook HMAC secret (if you enable signature verification) +CODEGEN_WEBHOOK_SECRET= +# Server port +PORT=8787 + diff --git a/CodegenRestDashboard/README.md b/CodegenRestDashboard/README.md new file mode 100644 index 000000000..0ece7ba0d --- /dev/null +++ b/CodegenRestDashboard/README.md @@ -0,0 +1,55 @@ +# CodegenRestDashboard (No-deps REST UI + Commands) + +This package adds a dependency-free dashboard and Node.js command scripts to interact with the Codegen REST API. + +Important: +- Do NOT commit real secrets. Create CodegenRestDashboard/.env locally (see .env.example) +- The browser UI never receives your API token; a tiny local proxy server injects auth headers +- Cloudflare Worker webhook is provided separately for production webhooks + +## Quick start + +1) Copy .env.example to .env and fill in your values +``` +cp CodegenRestDashboard/.env.example CodegenRestDashboard/.env +``` + +2) Start the local server (serves UI and proxies API) +``` +node CodegenRestDashboard/server.js +``` +Then open http://localhost:8787 + +3) Use commands (examples) +``` +node CodegenRestDashboard/commands/create_agent_run.js --prompt "Hello" --model "Sonnet 4.5" +node CodegenRestDashboard/commands/list_agent_runs.js --state active --limit 20 +node CodegenRestDashboard/commands/get_agent_run.js --id 123 +node CodegenRestDashboard/commands/resume_agent_run.js --id 123 --prompt "Continue" +node CodegenRestDashboard/commands/generate_setup_commands.js --repo_id 999 +``` + +4) Mock mode (no network) +``` +CODEGEN_OFFLINE=1 node CodegenRestDashboard/server.js +``` + +## Files +- commands/: Node CLI scripts, no external deps +- dashboard/: Vanilla HTML/CSS/JS UI +- utils/env.js: safe .env loader (Node-only) +- utils/apiClient.js: shared REST client for Node context +- server.js: local static server + API proxy (injects Authorization) +- webhook_server.js: Cloudflare Worker handler for /webhook +- mock/: local fixtures for offline development + +## Security +- .env is in .gitignore. Do not commit it. +- The token is used only in Node (server/commands). The browser never sees it. + +## Webhook (Cloudflare) +- Deploy CodegenRestDashboard/webhook_server.js as a Worker (route /webhook) +- Configure your DNS so https://www.pixelium.uk/webhook points to the Worker +- Optionally set CODEGEN_WEBHOOK_SECRET and verify HMAC in the worker + + diff --git a/CodegenRestDashboard/commands/create_agent_run.js b/CodegenRestDashboard/commands/create_agent_run.js new file mode 100644 index 000000000..8a77f5d6a --- /dev/null +++ b/CodegenRestDashboard/commands/create_agent_run.js @@ -0,0 +1,48 @@ +#!/usr/bin/env node +const { apiPost, pathCreate } = require('../utils/apiClient'); + +const MODELS = [ + 'Sonnet 4.5', + 'GPT-5', + 'GPT 5 Codex', + 'Claude opus 4.5', + 'Grok 4', + 'Grok 4 Fast reasoning', + 'Grok Code Fast 1', +]; + +async function main() { + const args = process.argv.slice(2); + let prompt = ''; + let model = ''; + let repo_id = undefined; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--prompt') prompt = args[++i] || ''; + else if (args[i] === '--model') model = args[++i] || ''; + else if (args[i] === '--repo_id') repo_id = Number(args[++i]); + } + + if (!prompt) { + console.error('Usage: create_agent_run.js --prompt "..." [--model "Sonnet 4.5"|...] [--repo_id 123]'); + process.exit(2); + } + if (model && !MODELS.includes(model)) { + console.error(`Model must be one of: ${MODELS.join(', ')}`); + process.exit(2); + } + + const body = { prompt }; + if (model) body.model = model; + if (repo_id) body.repo_id = repo_id; + + const data = await apiPost(pathCreate(), body); + console.log(JSON.stringify(data, null, 2)); +} + +if (require.main === module) { + main().catch((e) => { console.error(e.message || e); process.exit(1); }); +} + +module.exports = main; + diff --git a/CodegenRestDashboard/commands/generate_setup_commands.js b/CodegenRestDashboard/commands/generate_setup_commands.js new file mode 100644 index 000000000..3c9c21051 --- /dev/null +++ b/CodegenRestDashboard/commands/generate_setup_commands.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +const { apiPost, pathGenerateSetup } = require('../utils/apiClient'); + +async function main() { + const args = process.argv.slice(2); + let repo_id = undefined; + let language = undefined; // optional + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--repo_id') repo_id = Number(args[++i]); + else if (args[i] === '--language') language = args[++i] || undefined; + } + + if (!repo_id) { + console.error('Usage: generate_setup_commands.js --repo_id 123 [--language node|python|...]'); + process.exit(2); + } + + const body = { repo_id }; + if (language) body.language = language; + + const data = await apiPost(pathGenerateSetup(), body); + console.log(JSON.stringify(data, null, 2)); +} + +if (require.main === module) { + main().catch((e) => { console.error(e.message || e); process.exit(1); }); +} + +module.exports = main; + diff --git a/CodegenRestDashboard/commands/get_agent_run.js b/CodegenRestDashboard/commands/get_agent_run.js new file mode 100644 index 000000000..3964ba833 --- /dev/null +++ b/CodegenRestDashboard/commands/get_agent_run.js @@ -0,0 +1,35 @@ +#!/usr/bin/env node +const { apiGet, pathGet, pathLogs } = require('../utils/apiClient'); + +async function main() { + const args = process.argv.slice(2); + let id = undefined; + let withLogs = false; + let skip = 0; + let limit = 50; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--id') id = Number(args[++i]); + else if (args[i] === '--logs') withLogs = true; + else if (args[i] === '--skip') skip = Number(args[++i]); + else if (args[i] === '--limit') limit = Number(args[++i]); + } + + if (!id) { + console.error('Usage: get_agent_run.js --id 123 [--logs] [--skip 0] [--limit 50]'); + process.exit(2); + } + + const res = await apiGet(pathGet(id)); + if (!withLogs) return console.log(JSON.stringify(res, null, 2)); + + const logs = await apiGet(pathLogs(id), { skip, limit }); + console.log(JSON.stringify({ run: res, logs }, null, 2)); +} + +if (require.main === module) { + main().catch((e) => { console.error(e.message || e); process.exit(1); }); +} + +module.exports = main; + diff --git a/CodegenRestDashboard/commands/index.js b/CodegenRestDashboard/commands/index.js new file mode 100644 index 000000000..2f24662d3 --- /dev/null +++ b/CodegenRestDashboard/commands/index.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +const path = require('path'); +const { loadEnv } = require('../utils/env'); +loadEnv(path.join(__dirname, '..', '.env')); + +module.exports = { + create: require('./create_agent_run'), + resume: require('./resume_agent_run'), + list: require('./list_agent_runs'), + get: require('./get_agent_run'), + genSetup: require('./generate_setup_commands'), +}; + diff --git a/CodegenRestDashboard/commands/list_agent_runs.js b/CodegenRestDashboard/commands/list_agent_runs.js new file mode 100644 index 000000000..3c00b7258 --- /dev/null +++ b/CodegenRestDashboard/commands/list_agent_runs.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +const { apiGet, pathList } = require('../utils/apiClient'); + +async function main() { + const args = process.argv.slice(2); + let state = ''; + let page = 1; + let limit = 50; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--state') state = args[++i] || ''; + else if (args[i] === '--page') page = Number(args[++i]); + else if (args[i] === '--limit') limit = Number(args[++i]); + } + + const params = { page, limit }; + if (state) params.state = state; // server may ignore if unsupported + + const data = await apiGet(pathList(), params); + console.log(JSON.stringify(data, null, 2)); +} + +if (require.main === module) { + main().catch((e) => { console.error(e.message || e); process.exit(1); }); +} + +module.exports = main; + diff --git a/CodegenRestDashboard/commands/resume_agent_run.js b/CodegenRestDashboard/commands/resume_agent_run.js new file mode 100644 index 000000000..ddf4938c2 --- /dev/null +++ b/CodegenRestDashboard/commands/resume_agent_run.js @@ -0,0 +1,31 @@ +#!/usr/bin/env node +const { apiPost, pathResume } = require('../utils/apiClient'); + +async function main() { + const args = process.argv.slice(2); + let id = undefined; + let prompt = ''; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--id') id = Number(args[++i]); + else if (args[i] === '--prompt') prompt = args[++i] || ''; + } + + if (!id) { + console.error('Usage: resume_agent_run.js --id 123 [--prompt "..."]'); + process.exit(2); + } + + const body = { agent_run_id: id }; + if (prompt) body.prompt = prompt; + + const data = await apiPost(pathResume(), body); + console.log(JSON.stringify(data, null, 2)); +} + +if (require.main === module) { + main().catch((e) => { console.error(e.message || e); process.exit(1); }); +} + +module.exports = main; + diff --git a/CodegenRestDashboard/commands/runCommand.js b/CodegenRestDashboard/commands/runCommand.js new file mode 100644 index 000000000..9b90a4afc --- /dev/null +++ b/CodegenRestDashboard/commands/runCommand.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node +const cmds = require('./index'); + +async function main() { + const [,, name, ...rest] = process.argv; + if (!name || !cmds[name]) { + console.error('Usage: runCommand.js [args...]'); + process.exit(2); + } + // Re-dispatch by spawning module main + await cmds[name](); +} + +main().catch((e)=>{ console.error(e.message || e); process.exit(1); }); + diff --git a/CodegenRestDashboard/dashboard/api/client.js b/CodegenRestDashboard/dashboard/api/client.js new file mode 100644 index 000000000..284a9a040 --- /dev/null +++ b/CodegenRestDashboard/dashboard/api/client.js @@ -0,0 +1,23 @@ +// Browser-side client calls the local proxy server under /api +(function(){ + const api = {}; + const base = '/api'; + + function get(path, params={}){ + const qs = new URLSearchParams(params).toString(); + return fetch(`${base}${path}${qs?`?${qs}`:''}`).then(r=>r.json()); + } + function post(path, body={}){ + return fetch(`${base}${path}`, { method: 'POST', headers: { 'Content-Type':'application/json' }, body: JSON.stringify(body) }).then(r=>r.json()); + } + + // Helpers mirroring Node client paths + function createAgentRun(payload){ return post(`/v1/organizations/${CG_ENV.ORG_ID}/agent/run`, payload); } + function listAgentRuns(params){ return get(`/v1/organizations/${CG_ENV.ORG_ID}/agent/runs`, params); } + function getAgentRun(id){ return get(`/v1/organizations/${CG_ENV.ORG_ID}/agent/run/${id}`); } + function getAgentLogs(id, params){ return get(`/v1/alpha/organizations/${CG_ENV.ORG_ID}/agent/run/${id}/logs`, params); } + function resumeAgentRun(payload){ return post(`/v1/organizations/${CG_ENV.ORG_ID}/agent/run/resume`, payload); } + + window.CGApi = { createAgentRun, listAgentRuns, getAgentRun, getAgentLogs, resumeAgentRun }; +})(); + diff --git a/CodegenRestDashboard/dashboard/automation/followUpManager.js b/CodegenRestDashboard/dashboard/automation/followUpManager.js new file mode 100644 index 000000000..1a202db9a --- /dev/null +++ b/CodegenRestDashboard/dashboard/automation/followUpManager.js @@ -0,0 +1,28 @@ +(function(){ + // Monitors watched runs; when a run moves to COMPLETED, automatically send a resume request + // using the first template (if any). You can extend to map templates per run later. + let lastStatuses = new Map(); + + async function check(){ + const watched = Object.entries(CGStore.state.watched).filter(([,v])=>v).map(([k])=>Number(k)); + for (const id of watched){ + try { + const cur = await CGApi.getAgentRun(id); + const prev = lastStatuses.get(id); + if (prev && prev!=='COMPLETED' && cur.status==='COMPLETED'){ + const tpls = CGStore.state.templates||[]; + if (tpls.length){ + const text = tpls[0].text; + await CGApi.resumeAgentRun({ agent_run_id: id, prompt: text }); + CGToast.toast(`Auto-follow-up sent for #${id}`); + } + } + lastStatuses.set(id, cur.status); + } catch (e){ /* ignore */ } + } + } + + function start(){ setInterval(check, 4000); } + window.CGFollowUp = { start }; +})(); + diff --git a/CodegenRestDashboard/dashboard/components/header.js b/CodegenRestDashboard/dashboard/components/header.js new file mode 100644 index 000000000..c25a50464 --- /dev/null +++ b/CodegenRestDashboard/dashboard/components/header.js @@ -0,0 +1,44 @@ +(function(){ + const header = document.getElementById('app-header'); + + function render(state){ + header.innerHTML = ''; + + // Left: App title + const title = document.createElement('div'); + title.textContent = 'Codegen REST Dashboard'; + header.appendChild(title); + + // Active count with hover list + const activeWrap = document.createElement('div'); + activeWrap.className = 'header-item'; + const label = document.createElement('span'); + label.textContent = 'Active Runs'; + const badge = document.createElement('span'); + badge.className = 'badge'; + badge.textContent = String(state.activeCount); + activeWrap.appendChild(label); + activeWrap.appendChild(badge); + + const hover = document.createElement('div'); + hover.className = 'hover-card'; + state.runs.filter(r=>r.status==='ACTIVE'||r.status==='PENDING').slice(0,10).forEach((r)=>{ + const row = document.createElement('div'); + row.className = 'run-row'; + row.textContent = `#${r.id} ${r.title || ''}`; + row.onclick = ()=> window.CGRunDialog.open(r.id); + hover.appendChild(row); + }); + if (!hover.childElementCount) { const e=document.createElement('div'); e.className='run-row'; e.textContent='No active runs'; hover.appendChild(e); } + activeWrap.appendChild(hover); + + header.appendChild(activeWrap); + + // Right: Tabs (Runs / Templates) + const tabs = window.CGTabControl.render(); + header.appendChild(tabs); + } + + window.CGHeader = { render }; +})(); + diff --git a/CodegenRestDashboard/dashboard/components/notifications.js b/CodegenRestDashboard/dashboard/components/notifications.js new file mode 100644 index 000000000..c83751b23 --- /dev/null +++ b/CodegenRestDashboard/dashboard/components/notifications.js @@ -0,0 +1,12 @@ +(function(){ + const root = document.getElementById('toasts'); + function toast(msg, timeout=3000){ + const el = document.createElement('div'); + el.className = 'toast'; + el.textContent = msg; + root.appendChild(el); + setTimeout(()=>{ root.removeChild(el); }, timeout); + } + window.CGToast = { toast }; +})(); + diff --git a/CodegenRestDashboard/dashboard/components/pinnedRuns.js b/CodegenRestDashboard/dashboard/components/pinnedRuns.js new file mode 100644 index 000000000..349969178 --- /dev/null +++ b/CodegenRestDashboard/dashboard/components/pinnedRuns.js @@ -0,0 +1,30 @@ +(function(){ + const root = document.getElementById('pinned'); + + function render(state){ + root.innerHTML = ''; + if (!state.pinned.length) return; + const wrap = document.createElement('div'); + const title = document.createElement('div'); + title.textContent = 'Pinned'; title.style.marginBottom='6px'; + wrap.appendChild(title); + + state.pinned.forEach(id=>{ + const run = state.runs.find(r=>r.id===id) || { id, status: 'UNKNOWN' }; + const card = document.createElement('div'); card.className='pin-card'; + const head = document.createElement('div'); head.textContent = `#${id} ${run.title||''}`; head.style.fontWeight='600'; + const status = document.createElement('div'); status.textContent = `Status: ${run.status}`; + const controls = document.createElement('div'); + const unpin = document.createElement('button'); unpin.className='btn'; unpin.textContent='Unpin'; unpin.onclick=()=> CGStore.unpin(id); + const open = document.createElement('button'); open.className='btn'; open.textContent='Open'; open.onclick=()=> CGRunDialog.open(id); + controls.appendChild(unpin); controls.appendChild(open); + card.appendChild(head); card.appendChild(status); card.appendChild(controls); + wrap.appendChild(card); + }); + + root.appendChild(wrap); + } + + window.CGPinnedRuns = { render }; +})(); + diff --git a/CodegenRestDashboard/dashboard/components/runDialog.js b/CodegenRestDashboard/dashboard/components/runDialog.js new file mode 100644 index 000000000..ad86aa8e4 --- /dev/null +++ b/CodegenRestDashboard/dashboard/components/runDialog.js @@ -0,0 +1,75 @@ +(function(){ + const dialogs = document.getElementById('dialogs'); + + function fmtLog(l){ return `[${l.timestamp}] ${l.level||'INFO'} - ${l.message}`; } + + function open(id){ + const wrapper = document.createElement('div'); + wrapper.className = 'dialog'; + wrapper.innerHTML = `
+
+
Run #${id}
+
+ + +
+
+
+
+
+
+
`; + + function close(){ clearInterval(t); dialogs.removeChild(wrapper); } + wrapper.querySelector('#closeBtn').onclick = close; + + wrapper.querySelector('#resumeBtn').onclick = async ()=>{ + // Use template if selected in Templates tab, or prompt user + const tpls = CGStore.state.templates || []; + let prompt = ''; + if (tpls.length) { + const names = tpls.map((t,i)=>`${i+1}) ${t.name}`).join('\n'); + const pick = promptWindow(`Pick template index (1..${tpls.length}) or leave empty to type: \n${names}`); + if (pick) { + const idx = Number(pick)-1; if (idx>=0 && idx{ + const line = document.createElement('div'); + line.textContent = fmtLog(l); + box.appendChild(line); + }); + skip += (logs.logs || []).length; + if (meta.status === 'COMPLETED' || meta.status === 'FAILED' || meta.status === 'CANCELLED') { + clearInterval(t); + } + } catch (e) { console.error(e); } + } + refresh(); + const t = setInterval(refresh, 2000); + } + + function promptWindow(msg){ + // Avoid blocking native prompt for better UX, but spec asks no dependencies; use window.prompt + return window.prompt(msg || ''); + } + + window.CGRunDialog = { open }; +})(); + diff --git a/CodegenRestDashboard/dashboard/components/runList.js b/CodegenRestDashboard/dashboard/components/runList.js new file mode 100644 index 000000000..fbc3a3812 --- /dev/null +++ b/CodegenRestDashboard/dashboard/components/runList.js @@ -0,0 +1,76 @@ +(function(){ + const runsRoot = document.getElementById('runs'); + const controls = document.getElementById('controls'); + + const MODELS = [ + 'Sonnet 4.5', 'GPT-5', 'GPT 5 Codex', 'Claude opus 4.5', 'Grok 4', 'Grok 4 Fast reasoning', 'Grok Code Fast 1' + ]; + + function renderControls(state){ + controls.innerHTML = ''; + const wrap = document.createElement('div'); + wrap.className = 'controls'; + + // Filter toggle (Active/Past) + const sel = document.createElement('select'); + ;['active','past'].forEach(v=>{ + const o = document.createElement('option'); o.value = v; o.textContent = v==='active'?'Active':'Past'; + if (state.filter===v) o.selected = true; sel.appendChild(o); + }); + sel.onchange = ()=> CGStore.setFilter(sel.value); + wrap.appendChild(sel); + + // Create run prompt input + const prompt = document.createElement('input'); + prompt.placeholder = 'New agent prompt...'; prompt.style.minWidth = '260px'; + wrap.appendChild(prompt); + + // Model dropdown + const model = document.createElement('select'); + MODELS.forEach(m=>{ const o=document.createElement('option'); o.value=m; o.textContent=m; model.appendChild(o); }); + wrap.appendChild(model); + + // Repo id (optional) + const repo = document.createElement('input'); repo.placeholder='repo_id (optional)'; repo.type='number'; repo.style.width='140px'; + wrap.appendChild(repo); + + // Create button + const btn = document.createElement('button'); + btn.className = 'btn primary'; btn.textContent = 'Create Agent Run'; + btn.onclick = async ()=>{ + const body = { prompt: prompt.value, model: model.value }; + if (!body.prompt) { CGToast.toast('Prompt required'); return; } + if (repo.value) body.repo_id = Number(repo.value); + await CGApi.createAgentRun(body); + CGToast.toast('Agent run created'); + // No manual refresh button: watcher will auto-refresh the list shortly + prompt.value = ''; + }; + wrap.appendChild(btn); + + controls.appendChild(wrap); + } + + function renderList(state){ + runsRoot.innerHTML = ''; + const list = document.createElement('div'); + const filtered = state.runs.filter(r=> state.filter==='active' ? (r.status==='ACTIVE'||r.status==='PENDING') : (r.status==='COMPLETED'||r.status==='FAILED'||r.status==='CANCELLED')); + filtered.forEach(r=>{ + const row = document.createElement('div'); row.className='run-row'; + const title = document.createElement('div'); title.textContent=`#${r.id} ${r.title||''}`; title.style.flex='1'; + const status = document.createElement('div'); status.textContent=r.status; status.className = r.status==='ACTIVE'?'status-active': (r.status==='COMPLETED'?'status-completed':''); + const pinBtn = document.createElement('button'); pinBtn.className='btn'; pinBtn.textContent = CGStore.state.pinned.includes(r.id)?'Unpin':'Pin'; + pinBtn.onclick = ()=> CGStore.state.pinned.includes(r.id) ? CGStore.unpin(r.id) : CGStore.pin(r.id); + const watchBtn = document.createElement('button'); watchBtn.className='btn'; watchBtn.textContent = CGStore.state.watched[r.id]?'Unwatch':'Watch'; + watchBtn.onclick = ()=> CGStore.setWatched(r.id, !CGStore.state.watched[r.id]); + const openBtn = document.createElement('button'); openBtn.className='btn'; openBtn.textContent='Open'; openBtn.onclick=()=> CGRunDialog.open(r.id); + row.appendChild(title); row.appendChild(status); row.appendChild(pinBtn); row.appendChild(watchBtn); row.appendChild(openBtn); + list.appendChild(row); + }); + runsRoot.appendChild(list); + } + + function render(state){ renderControls(state); renderList(state); } + window.CGRunList = { render }; +})(); + diff --git a/CodegenRestDashboard/dashboard/components/tabControl.js b/CodegenRestDashboard/dashboard/components/tabControl.js new file mode 100644 index 000000000..742b662fa --- /dev/null +++ b/CodegenRestDashboard/dashboard/components/tabControl.js @@ -0,0 +1,31 @@ +(function(){ + function render(){ + const wrap = document.createElement('div'); + const runsBtn = document.createElement('button'); runsBtn.className='btn'; runsBtn.textContent='Runs'; + const tplBtn = document.createElement('button'); tplBtn.className='btn'; tplBtn.textContent='Templates'; + + function sel(which){ + window.CGTabControl.active = which; + document.getElementById('controls').style.display = which==='runs'?'block':'none'; + document.getElementById('runs').style.display = which==='runs'?'block':'none'; + document.getElementById('pinned').style.display = which==='runs'?'block':'none'; + const main = document.getElementById('app-main'); + const existing = document.getElementById('tplView'); + if (which==='templates') { + if (!existing) { + const v = document.createElement('div'); v.id='tplView'; v.appendChild(CGTemplates.render()); main.appendChild(v); + } + } else { + if (existing) existing.remove(); + } + } + + runsBtn.onclick = ()=> sel('runs'); + tplBtn.onclick = ()=> sel('templates'); + + wrap.appendChild(runsBtn); wrap.appendChild(tplBtn); + return wrap; + } + window.CGTabControl = { render, active: 'runs' }; +})(); + diff --git a/CodegenRestDashboard/dashboard/components/templateManager.js b/CodegenRestDashboard/dashboard/components/templateManager.js new file mode 100644 index 000000000..829f536fb --- /dev/null +++ b/CodegenRestDashboard/dashboard/components/templateManager.js @@ -0,0 +1,35 @@ +(function(){ + function render(){ + const c = document.createElement('div'); + c.style.padding = '10px 0'; + + const list = document.createElement('div'); + (CGStore.state.templates||[]).forEach((t, i)=>{ + const row = document.createElement('div'); row.className='run-row'; + const name = document.createElement('div'); name.textContent = t.name; name.style.flex='1'; + const edit = document.createElement('button'); edit.className='btn'; edit.textContent='Edit'; edit.onclick=()=> editTpl(i); + const del = document.createElement('button'); del.className='btn'; del.textContent='Delete'; del.onclick=()=> CGStore.deleteTemplate(i); + row.appendChild(name); row.appendChild(edit); row.appendChild(del); list.appendChild(row); + }); + + const add = document.createElement('button'); add.className='btn primary'; add.textContent='Add Template'; add.onclick=()=> addTpl(); + + c.appendChild(list); c.appendChild(add); + return c; + } + + function addTpl(){ + const name = window.prompt('Template name:'); if (!name) return; + const text = window.prompt('Template text:'); if (text==null) return; + CGStore.addTemplate({ name, text }); + } + function editTpl(idx){ + const cur = CGStore.state.templates[idx]; + const name = window.prompt('Template name:', cur.name); if (!name) return; + const text = window.prompt('Template text:', cur.text); if (text==null) return; + CGStore.updateTemplate(idx, { name, text }); + } + + window.CGTemplates = { render }; +})(); + diff --git a/CodegenRestDashboard/dashboard/index.html b/CodegenRestDashboard/dashboard/index.html new file mode 100644 index 000000000..36e5a9cf6 --- /dev/null +++ b/CodegenRestDashboard/dashboard/index.html @@ -0,0 +1,33 @@ + + + + + + Codegen REST Dashboard + + + +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + diff --git a/CodegenRestDashboard/dashboard/main.js b/CodegenRestDashboard/dashboard/main.js new file mode 100644 index 000000000..e26d99d05 --- /dev/null +++ b/CodegenRestDashboard/dashboard/main.js @@ -0,0 +1,16 @@ +(function(){ + // Inject CG_ENV for browser (org id only; token stays server-side!) + window.CG_ENV = { ORG_ID: (new URLSearchParams(location.search).get('org') || '323') }; + + // Render reactive UI + CGStore.subscribe((state)=>{ + CGHeader.render(state); + CGPinnedRuns.render(state); + if (window.CGTabControl.active==='runs') CGRunList.render(state); + }); + + // Start background watchers (auto-refresh; no manual refresh button) + CGWatcher.start(); + CGFollowUp.start(); +})(); + diff --git a/CodegenRestDashboard/dashboard/services/watcher.js b/CodegenRestDashboard/dashboard/services/watcher.js new file mode 100644 index 000000000..ee50d4c43 --- /dev/null +++ b/CodegenRestDashboard/dashboard/services/watcher.js @@ -0,0 +1,27 @@ +(function(){ + let interval = null; + + async function tick(){ + try { + const data = await CGApi.listAgentRuns({ page: 1, limit: 50 }); + const runs = (data.data||data.runs||[]); + CGStore.setRuns(runs); + + // Watch pinned/watched runs for status transitions + const watchedIds = Object.entries(CGStore.state.watched).filter(([,v])=>v).map(([k])=>Number(k)); + for (const id of new Set([ ...watchedIds, ...CGStore.state.pinned ])){ + try { + const r = await CGApi.getAgentRun(id); + const old = CGStore.state.runs.find(x=>x.id===id); + if (old && old.status!==r.status && (r.status==='COMPLETED'||r.status==='FAILED'||r.status==='CANCELLED')){ + CGToast.toast(`Run #${id} ${r.status}`); + } + } catch (e){ /* ignore individual errors */ } + } + } catch (e) { console.error('watcher error', e); } + } + + function start(){ if (interval) clearInterval(interval); tick(); interval = setInterval(tick, 3000); } + window.CGWatcher = { start }; +})(); + diff --git a/CodegenRestDashboard/dashboard/state/store.js b/CodegenRestDashboard/dashboard/state/store.js new file mode 100644 index 000000000..ea3ded88b --- /dev/null +++ b/CodegenRestDashboard/dashboard/state/store.js @@ -0,0 +1,27 @@ +// Tiny store with pub/sub (no deps) +(function(){ + const state = { + activeCount: 0, + runs: [], + filter: 'active', // 'active' | 'past' + pinned: [], // array of run ids + watched: {}, // id -> boolean + templates: JSON.parse(localStorage.getItem('cg_templates')||'[]'), + }; + const subs = []; + function notify(){ subs.forEach(fn=>fn(state)); save(); } + function save(){ localStorage.setItem('cg_pins', JSON.stringify(state.pinned)); localStorage.setItem('cg_templates', JSON.stringify(state.templates)); } + function init(){ try { state.pinned = JSON.parse(localStorage.getItem('cg_pins')||'[]'); } catch(_){} } + function subscribe(fn){ subs.push(fn); fn(state); return ()=>{ const i=subs.indexOf(fn); if(i>=0) subs.splice(i,1); } } + function setRuns(runs){ state.runs = runs; state.activeCount = runs.filter(r=>r.status==='ACTIVE' || r.status==='PENDING' ).length; notify(); } + function setFilter(f){ state.filter = f; notify(); } + function pin(id){ if(!state.pinned.includes(id)) { state.pinned.unshift(id); notify(); } } + function unpin(id){ state.pinned = state.pinned.filter(x=>x!==id); notify(); } + function setWatched(id, v){ state.watched[id] = !!v; notify(); } + function addTemplate(t){ state.templates.push(t); notify(); } + function updateTemplate(i, t){ state.templates[i]=t; notify(); } + function deleteTemplate(i){ state.templates.splice(i,1); notify(); } + init(); + window.CGStore = { state, subscribe, setRuns, setFilter, pin, unpin, setWatched, addTemplate, updateTemplate, deleteTemplate }; +})(); + diff --git a/CodegenRestDashboard/dashboard/styles.css b/CodegenRestDashboard/dashboard/styles.css new file mode 100644 index 000000000..4a8604e14 --- /dev/null +++ b/CodegenRestDashboard/dashboard/styles.css @@ -0,0 +1,25 @@ +* { box-sizing: border-box; } +body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; color: #1f2937; } +header { position: sticky; top: 0; background: #0f172a; color: white; padding: 10px 16px; display: flex; align-items: center; gap: 16px; z-index: 10; } +#app-main { padding: 16px; } +.badge { background: #10b981; color: white; border-radius: 999px; padding: 2px 8px; font-size: 12px; } +.header-item { position: relative; } +.hover-card { position: absolute; top: 30px; left: 0; background: white; border: 1px solid #e5e7eb; box-shadow: 0 10px 20px rgba(0,0,0,0.1); border-radius: 8px; min-width: 240px; display: none; } +.header-item:hover .hover-card { display: block; } +.run-row { display: flex; gap: 8px; padding: 6px 10px; border-bottom: 1px solid #f3f4f6; align-items: center; } +.run-row:last-child { border-bottom: none; } +.btn { border: 1px solid #d1d5db; background: white; padding: 6px 10px; border-radius: 6px; cursor: pointer; } +.btn.primary { background: #2563eb; color: white; border-color: #1d4ed8; } +.controls { display: flex; gap: 12px; align-items: center; margin: 10px 0 16px; } +#pinned { margin-bottom: 16px; } +.pin-card { border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px 10px; margin-bottom: 8px; } +.status-active { color: #0891b2; } +.status-completed { color: #059669; } +.status-failed { color: #dc2626; } +.toast { position: fixed; right: 16px; bottom: 16px; background: #111827; color: white; padding: 10px 14px; border-radius: 8px; opacity: 0.96; } +.dialog { position: fixed; inset: 0; background: rgba(0,0,0,.4); display: flex; align-items: center; justify-content: center; } +.dialog .panel { width: min(900px, 92vw); max-height: 80vh; background: white; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,0.2); display: flex; flex-direction: column; } +.dialog-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid #e5e7eb; } +.dialog-body { padding: 10px 14px; overflow: auto; } +.logs { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px; } + diff --git a/CodegenRestDashboard/mock/mockData.json b/CodegenRestDashboard/mock/mockData.json new file mode 100644 index 000000000..ad5d89c7f --- /dev/null +++ b/CodegenRestDashboard/mock/mockData.json @@ -0,0 +1,7 @@ +{ + "runs": [ + { "id": 1, "status": "ACTIVE", "title": "Demo active" }, + { "id": 2, "status": "COMPLETED", "title": "Demo past" } + ] +} + diff --git a/CodegenRestDashboard/server.js b/CodegenRestDashboard/server.js new file mode 100644 index 000000000..277714af3 --- /dev/null +++ b/CodegenRestDashboard/server.js @@ -0,0 +1,74 @@ +// Simple static server + proxy without external deps +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const { loadEnv } = require('./utils/env'); +loadEnv(path.join(__dirname, '.env')); + +const PORT = Number(process.env.PORT || 8787); +const API_BASE = (process.env.CODEGEN_API_BASE || 'https://api.codegen.com').replace(/\/$/, ''); +const TOKEN = process.env.CODEGEN_TOKEN || ''; +const ORG_ID = process.env.CODEGEN_ORG_ID || ''; +const OFFLINE = String(process.env.CODEGEN_OFFLINE || '0') === '1'; + +function contentType(filePath) { + if (filePath.endsWith('.html')) return 'text/html; charset=utf-8'; + if (filePath.endsWith('.css')) return 'text/css; charset=utf-8'; + if (filePath.endsWith('.js')) return 'application/javascript; charset=utf-8'; + if (filePath.endsWith('.json')) return 'application/json; charset=utf-8'; + return 'text/plain; charset=utf-8'; +} + +function serveStatic(req, res) { + let file = req.url.split('?')[0]; + if (file === '/' || file === '') file = '/index.html'; + const p = path.join(__dirname, 'dashboard', file); + if (!p.startsWith(path.join(__dirname, 'dashboard'))) { + res.writeHead(403); return res.end('Forbidden'); + } + fs.readFile(p, (err, data) => { + if (err) { res.writeHead(404); return res.end('Not found'); } + res.writeHead(200, { 'Content-Type': contentType(p) }); + res.end(data); + }); +} + +async function proxyApi(req, res) { + if (!TOKEN || !ORG_ID) { res.writeHead(500); return res.end('Missing CODEGEN_TOKEN or CODEGEN_ORG_ID'); } + let body = ''; + req.on('data', (c) => body += c); + req.on('end', async () => { + try { + // Map /api/* to real endpoints; the incoming path already contains /v1/... + const target = `${API_BASE}${req.url.replace(/^\/api/, '')}`; + if (OFFLINE) { + // Minimal offline mock + res.writeHead(200, { 'Content-Type': 'application/json' }); + return res.end(JSON.stringify({ ok: true, offline: true, url: target })); + } + const r = await fetch(target, { + method: req.method, + headers: { + 'Authorization': `Bearer ${TOKEN}`, + 'Content-Type': req.headers['content-type'] || 'application/json' + }, + body: ['POST','PUT','PATCH'].includes(req.method) ? body : undefined, + }); + const txt = await r.text(); + res.writeHead(r.status, { 'Content-Type': r.headers.get('content-type') || 'application/json' }); + res.end(txt); + } catch (e) { + res.writeHead(500); res.end(e.message || 'Proxy error'); + } + }); +} + +const server = http.createServer((req, res) => { + if (req.url.startsWith('/api/')) return proxyApi(req, res); + return serveStatic(req, res); +}); + +server.listen(PORT, () => { + console.log(`[CodegenRestDashboard] Server listening on http://localhost:${PORT}`); +}); + diff --git a/CodegenRestDashboard/utils/apiClient.js b/CodegenRestDashboard/utils/apiClient.js new file mode 100644 index 000000000..e13927d11 --- /dev/null +++ b/CodegenRestDashboard/utils/apiClient.js @@ -0,0 +1,124 @@ +// Minimal API client (Node-only) +const { loadEnv } = require('./env'); +const https = require('https'); + +loadEnv(); + +const API_BASE = process.env.CODEGEN_API_BASE?.replace(/\/$/, '') || 'https://api.codegen.com'; +const ORG_ID = process.env.CODEGEN_ORG_ID; +const TOKEN = process.env.CODEGEN_TOKEN; +const OFFLINE = String(process.env.CODEGEN_OFFLINE || '0') === '1'; + +function nodeFetch(url, options) { + // Prefer global fetch if available (Node 18+), else fallback to https + if (typeof fetch === 'function') { + return fetch(url, options); + } + return new Promise((resolve, reject) => { + const u = new URL(url); + const req = https.request({ + method: options?.method || 'GET', + hostname: u.hostname, + path: u.pathname + u.search, + headers: options?.headers || {}, + }, (res) => { + let data = ''; + res.on('data', (chunk) => (data += chunk)); + res.on('end', () => { + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + json: async () => JSON.parse(data || '{}'), + text: async () => data, + }); + }); + }); + req.on('error', reject); + if (options?.body) req.write(options.body); + req.end(); + }); +} + +function authHeaders() { + if (!TOKEN || !ORG_ID) throw new Error('Missing CODEGEN_TOKEN or CODEGEN_ORG_ID'); + return { + 'Authorization': `Bearer ${TOKEN}`, + 'Content-Type': 'application/json', + }; +} + +async function apiGet(path, params = {}) { + if (OFFLINE) return mockResponse(path, params, 'GET'); + const qs = new URLSearchParams(params).toString(); + const url = `${API_BASE}${path}${qs ? `?${qs}` : ''}`; + const res = await nodeFetch(url, { method: 'GET', headers: authHeaders() }); + if (!res.ok) throw new Error(`GET ${path} failed: ${res.status}`); + return res.json(); +} + +async function apiPost(path, body = {}) { + if (OFFLINE) return mockResponse(path, body, 'POST'); + const url = `${API_BASE}${path}`; + const res = await nodeFetch(url, { method: 'POST', headers: authHeaders(), body: JSON.stringify(body) }); + if (!res.ok) { + const txt = await res.text().catch(() => ''); + throw new Error(`POST ${path} failed: ${res.status} ${txt}`); + } + return res.json(); +} + +// Mock data for offline mode +function mockResponse(path, payload, method) { + if (path.includes('/agent/runs')) { + return Promise.resolve({ + data: [ + { id: 101, status: 'ACTIVE', title: 'Active run A', created_at: new Date().toISOString() }, + { id: 99, status: 'COMPLETED', title: 'Past run X', created_at: new Date(Date.now()-86400000).toISOString() }, + ], + total: 2, + page: 1, + limit: 50, + }); + } + if (path.includes('/agent/run/resume')) { + return Promise.resolve({ success: true, resumed: true, ...payload }); + } + if (path.includes('/agent/run/') && method === 'GET' && path.match(/\/agent\/run\/(\d+)/)) { + const id = Number(path.match(/\/(\d+)$/)?.[1] || 0); + return Promise.resolve({ id, status: id % 2 ? 'ACTIVE' : 'COMPLETED', title: `Run ${id}` }); + } + if (path.includes('/agent/run') && method === 'POST') { + return Promise.resolve({ id: Math.floor(Math.random()*1000)+200, status: 'PENDING', ...payload }); + } + if (path.includes('/setup-commands/generate')) { + return Promise.resolve({ status: 'QUEUED', repo_id: payload.repo_id, agent_run_id: 555 }); + } + if (path.includes('/logs')) { + return Promise.resolve({ + agent_run: { id: payload.id || 999, status: 'ACTIVE' }, + logs: [ { id: 1, message: 'Mock log line 1', level: 'INFO', timestamp: new Date().toISOString() } ], + pagination: { skip: 0, limit: 50, total: 1 } + }); + } + return Promise.resolve({ ok: true, echo: { path, payload, method } }); +} + +// High-level API wrappers +function pathCreate() { return `/v1/organizations/${ORG_ID}/agent/run`; } +function pathGet(id) { return `/v1/organizations/${ORG_ID}/agent/run/${id}`; } +function pathList() { return `/v1/organizations/${ORG_ID}/agent/runs`; } +function pathResume() { return `/v1/organizations/${ORG_ID}/agent/run/resume`; } +function pathLogs(id) { return `/v1/alpha/organizations/${ORG_ID}/agent/run/${id}/logs`; } +function pathGenerateSetup() { return `/v1/organizations/${ORG_ID}/setup-commands/generate`; } + +module.exports = { + apiGet, + apiPost, + pathCreate, + pathGet, + pathList, + pathResume, + pathLogs, + pathGenerateSetup, +}; + diff --git a/CodegenRestDashboard/utils/env.js b/CodegenRestDashboard/utils/env.js new file mode 100644 index 000000000..15753d7c6 --- /dev/null +++ b/CodegenRestDashboard/utils/env.js @@ -0,0 +1,25 @@ +// Minimal .env loader without dependencies +// Usage: loadEnv(path.join(__dirname, '..', '.env')) +const fs = require('fs'); +const path = require('path'); + +function loadEnv(envPath) { + try { + if (!envPath) envPath = path.join(process.cwd(), 'CodegenRestDashboard', '.env'); + if (!fs.existsSync(envPath)) return; + const content = fs.readFileSync(envPath, 'utf8'); + content.split(/\r?\n/).forEach((line) => { + if (!line || line.trim().startsWith('#')) return; + const idx = line.indexOf('='); + if (idx === -1) return; + const key = line.slice(0, idx).trim(); + const val = line.slice(idx + 1).trim(); + if (key && !(key in process.env)) process.env[key] = val; + }); + } catch (e) { + console.error('[env] Failed to load .env:', e.message); + } +} + +module.exports = { loadEnv }; + diff --git a/CodegenRestDashboard/webhook_server.js b/CodegenRestDashboard/webhook_server.js new file mode 100644 index 000000000..89d6a50c1 --- /dev/null +++ b/CodegenRestDashboard/webhook_server.js @@ -0,0 +1,42 @@ +// Cloudflare Worker: handle Codegen webhook callbacks at /webhook +// Deploy with Wrangler or Cloudflare dashboard. No external deps. +// Optionally set CODEGEN_WEBHOOK_SECRET in Worker secrets to verify HMAC. + +export default { + async fetch(request, env, ctx) { + const url = new URL(request.url); + if (url.pathname !== '/webhook') + return new Response('Not Found', { status: 404 }); + + if (request.method !== 'POST') + return new Response('Method Not Allowed', { status: 405 }); + + // Read body + const bodyText = await request.text(); + + // Optional signature check + const sig = request.headers.get('X-Codegen-Signature'); + const secret = env.CODEGEN_WEBHOOK_SECRET || ''; + if (secret) { + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'] + ); + const mac = await crypto.subtle.sign('HMAC', key, encoder.encode(bodyText)); + const expected = [...new Uint8Array(mac)].map(b => b.toString(16).padStart(2,'0')).join(''); + if (!sig || sig !== expected) { + return new Response('Invalid signature', { status: 401 }); + } + } + + // Persist minimal event in KV (optional) or forward to your backend + // Here, we simply log and return 200. + console.log('Codegen webhook:', bodyText.slice(0, 500)); + + return new Response(JSON.stringify({ ok: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } +}; + From 1773d1f0ee6599b2bcfa3cac723774b916455c4b Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 18:53:52 +0000 Subject: [PATCH 2/4] feat(dashboard): chaining UI + persistence; per-run template selection; compact cards; notifications; new CLI (ban/unban/logs) - Persist watched + chain selections in localStorage - Inline chain editor for active runs (list + pinned) - Run dialog gains template selector and template-driven resume - Follow-up manager executes chained templates sequentially per completion - Compact UI styling and status dots - Add ban_agent_run.js, unban_agent_run.js, get_agent_run_logs.js; update index - README updates Co-authored-by: Zeeeepa --- CodegenRestDashboard/README.md | 14 ++++++++ .../commands/ban_agent_run.js | 28 +++++++++++++++ .../commands/get_agent_run_logs.js | 19 ++++++++++ CodegenRestDashboard/commands/index.js | 4 ++- .../commands/unban_agent_run.js | 28 +++++++++++++++ .../dashboard/automation/followUpManager.js | 21 +++++++---- .../dashboard/components/pinnedRuns.js | 27 ++++++++++++-- .../dashboard/components/runDialog.js | 36 +++++++++++++------ .../dashboard/components/runList.js | 36 ++++++++++++++++--- CodegenRestDashboard/dashboard/main.js | 6 +++- .../dashboard/services/watcher.js | 4 ++- CodegenRestDashboard/dashboard/state/store.js | 25 ++++++++++--- CodegenRestDashboard/dashboard/styles.css | 7 ++++ 13 files changed, 224 insertions(+), 31 deletions(-) create mode 100644 CodegenRestDashboard/commands/ban_agent_run.js create mode 100644 CodegenRestDashboard/commands/get_agent_run_logs.js create mode 100644 CodegenRestDashboard/commands/unban_agent_run.js diff --git a/CodegenRestDashboard/README.md b/CodegenRestDashboard/README.md index 0ece7ba0d..31905bec4 100644 --- a/CodegenRestDashboard/README.md +++ b/CodegenRestDashboard/README.md @@ -52,4 +52,18 @@ CODEGEN_OFFLINE=1 node CodegenRestDashboard/server.js - Configure your DNS so https://www.pixelium.uk/webhook points to the Worker - Optionally set CODEGEN_WEBHOOK_SECRET and verify HMAC in the worker +## New features +- Auto-refresh only UI (no manual refresh button) +- Header shows only Active count (hover reveals top active runs) +- Compact run cards with status dots; click a completed run to open logs/resume dialog; click an active run’s “Chain” to configure multi-template chaining +- Per-run template selection and chaining (Templates tab manages templates) +- Follow-up automation sends templates in sequence on each completion cycle +- Desktop notifications (optional, via browser permission) + in-app toasts + +## Extra commands +``` +node CodegenRestDashboard/commands/get_agent_run_logs.js --id 123 --skip 0 --limit 100 +node CodegenRestDashboard/commands/ban_agent_run.js --id 123 [--before ] [--after ] +node CodegenRestDashboard/commands/unban_agent_run.js --id 123 [--before ] [--after ] +``` diff --git a/CodegenRestDashboard/commands/ban_agent_run.js b/CodegenRestDashboard/commands/ban_agent_run.js new file mode 100644 index 000000000..abec4dafa --- /dev/null +++ b/CodegenRestDashboard/commands/ban_agent_run.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +const { apiPost } = require('../utils/apiClient'); +const { loadEnv } = require('../utils/env'); +const path = require('path'); +loadEnv(path.join(__dirname, '..', '.env')); + +function pathBan(org){ return `/v1/organizations/${org}/agent/run/ban`; } + +async function main(){ + const args = process.argv.slice(2); + let id; let before=null; let after=null; + for (let i=0;i [--before ] [--after ]'); process.exit(2); } + const org = process.env.CODEGEN_ORG_ID; + const body = { agent_run_id: id }; + if (before!==null) body.before_card_order_id = before; + if (after!==null) body.after_card_order_id = after; + const data = await apiPost(pathBan(org), body); + console.log(JSON.stringify(data, null, 2)); +} + +if (require.main===module){ main().catch(e=>{ console.error(e.message||e); process.exit(1); }); } +module.exports = main; + diff --git a/CodegenRestDashboard/commands/get_agent_run_logs.js b/CodegenRestDashboard/commands/get_agent_run_logs.js new file mode 100644 index 000000000..d806b4e51 --- /dev/null +++ b/CodegenRestDashboard/commands/get_agent_run_logs.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +const { apiGet, pathLogs } = require('../utils/apiClient'); + +async function main(){ + const args = process.argv.slice(2); + let id; let skip=0; let limit=100; + for (let i=0;i [--skip 0] [--limit 100]'); process.exit(2); } + const data = await apiGet(pathLogs(id), { skip, limit }); + console.log(JSON.stringify(data, null, 2)); +} + +if (require.main===module){ main().catch(e=>{ console.error(e.message||e); process.exit(1); }); } +module.exports = main; + diff --git a/CodegenRestDashboard/commands/index.js b/CodegenRestDashboard/commands/index.js index 2f24662d3..86c4121ee 100644 --- a/CodegenRestDashboard/commands/index.js +++ b/CodegenRestDashboard/commands/index.js @@ -8,6 +8,8 @@ module.exports = { resume: require('./resume_agent_run'), list: require('./list_agent_runs'), get: require('./get_agent_run'), + logs: require('./get_agent_run_logs'), genSetup: require('./generate_setup_commands'), + ban: require('./ban_agent_run'), + unban: require('./unban_agent_run'), }; - diff --git a/CodegenRestDashboard/commands/unban_agent_run.js b/CodegenRestDashboard/commands/unban_agent_run.js new file mode 100644 index 000000000..a07cb8808 --- /dev/null +++ b/CodegenRestDashboard/commands/unban_agent_run.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +const { apiPost } = require('../utils/apiClient'); +const { loadEnv } = require('../utils/env'); +const path = require('path'); +loadEnv(path.join(__dirname, '..', '.env')); + +function pathUnban(org){ return `/v1/organizations/${org}/agent/run/unban`; } + +async function main(){ + const args = process.argv.slice(2); + let id; let before=null; let after=null; + for (let i=0;i [--before ] [--after ]'); process.exit(2); } + const org = process.env.CODEGEN_ORG_ID; + const body = { agent_run_id: id }; + if (before!==null) body.before_card_order_id = before; + if (after!==null) body.after_card_order_id = after; + const data = await apiPost(pathUnban(org), body); + console.log(JSON.stringify(data, null, 2)); +} + +if (require.main===module){ main().catch(e=>{ console.error(e.message||e); process.exit(1); }); } +module.exports = main; + diff --git a/CodegenRestDashboard/dashboard/automation/followUpManager.js b/CodegenRestDashboard/dashboard/automation/followUpManager.js index 1a202db9a..2f001d8e8 100644 --- a/CodegenRestDashboard/dashboard/automation/followUpManager.js +++ b/CodegenRestDashboard/dashboard/automation/followUpManager.js @@ -1,6 +1,6 @@ (function(){ // Monitors watched runs; when a run moves to COMPLETED, automatically send a resume request - // using the first template (if any). You can extend to map templates per run later. + // using the run's configured chain of templates (if any), one-by-one per completion cycle. let lastStatuses = new Map(); async function check(){ @@ -10,11 +10,19 @@ const cur = await CGApi.getAgentRun(id); const prev = lastStatuses.get(id); if (prev && prev!=='COMPLETED' && cur.status==='COMPLETED'){ - const tpls = CGStore.state.templates||[]; - if (tpls.length){ - const text = tpls[0].text; - await CGApi.resumeAgentRun({ agent_run_id: id, prompt: text }); - CGToast.toast(`Auto-follow-up sent for #${id}`); + const chain = CGStore.getChain(id) || []; + const prog = CGStore.getChainProgress(id) || 0; + if (prog < chain.length){ + const tplIdx = chain[prog]; + const tpls = CGStore.state.templates||[]; + if (tpls[tplIdx]){ + const text = tpls[tplIdx].text || ''; + if (text){ + await CGApi.resumeAgentRun({ agent_run_id: id, prompt: text }); + CGStore.setChainProgress(id, prog+1); + CGToast.toast(`Auto-follow-up ${prog+1}/${chain.length} sent for #${id}`); + } + } } } lastStatuses.set(id, cur.status); @@ -25,4 +33,3 @@ function start(){ setInterval(check, 4000); } window.CGFollowUp = { start }; })(); - diff --git a/CodegenRestDashboard/dashboard/components/pinnedRuns.js b/CodegenRestDashboard/dashboard/components/pinnedRuns.js index 349969178..96a60c5c3 100644 --- a/CodegenRestDashboard/dashboard/components/pinnedRuns.js +++ b/CodegenRestDashboard/dashboard/components/pinnedRuns.js @@ -13,10 +13,32 @@ const run = state.runs.find(r=>r.id===id) || { id, status: 'UNKNOWN' }; const card = document.createElement('div'); card.className='pin-card'; const head = document.createElement('div'); head.textContent = `#${id} ${run.title||''}`; head.style.fontWeight='600'; - const status = document.createElement('div'); status.textContent = `Status: ${run.status}`; - const controls = document.createElement('div'); + const status = document.createElement('div'); status.textContent = ''; status.className = run.status==='ACTIVE'?'status-active': (run.status==='COMPLETED'?'status-completed':''); status.title = run.status; + const controls = document.createElement('div'); controls.style.display='flex'; controls.style.gap='6px'; const unpin = document.createElement('button'); unpin.className='btn'; unpin.textContent='Unpin'; unpin.onclick=()=> CGStore.unpin(id); const open = document.createElement('button'); open.className='btn'; open.textContent='Open'; open.onclick=()=> CGRunDialog.open(id); + + // Inline chain selector for active runs + if (run.status==='ACTIVE' || run.status==='PENDING'){ + const chain = document.createElement('button'); chain.className='btn'; chain.textContent='Chain'; + const panel = document.createElement('div'); panel.style.display='none'; panel.style.marginTop='6px'; + const listBox = document.createElement('div'); + const tpls = CGStore.state.templates || []; + const selected = new Set((CGStore.getChain(id)||[]).map(Number)); + tpls.forEach((t,i)=>{ + const lbl = document.createElement('label'); lbl.style.display='flex'; lbl.style.alignItems='center'; lbl.style.gap='6px'; lbl.style.fontSize='12px'; + const cb = document.createElement('input'); cb.type='checkbox'; cb.checked=selected.has(i); + cb.onchange = ()=>{ if (cb.checked) selected.add(i); else selected.delete(i); }; + const span = document.createElement('span'); span.textContent=t.name||`Template ${i+1}`; + lbl.appendChild(cb); lbl.appendChild(span); listBox.appendChild(lbl); + }); + const save = document.createElement('button'); save.className='btn primary'; save.textContent='Save'; save.style.marginTop='6px'; + save.onclick = ()=> { CGStore.setChain(id, Array.from(selected)); panel.style.display='none'; CGToast.toast('Chain updated'); }; + panel.appendChild(listBox); panel.appendChild(save); + chain.onclick = ()=>{ panel.style.display = panel.style.display==='none'?'block':'none'; }; + controls.appendChild(chain); controls.appendChild(panel); + } + controls.appendChild(unpin); controls.appendChild(open); card.appendChild(head); card.appendChild(status); card.appendChild(controls); wrap.appendChild(card); @@ -27,4 +49,3 @@ window.CGPinnedRuns = { render }; })(); - diff --git a/CodegenRestDashboard/dashboard/components/runDialog.js b/CodegenRestDashboard/dashboard/components/runDialog.js index ad86aa8e4..2af8a5556 100644 --- a/CodegenRestDashboard/dashboard/components/runDialog.js +++ b/CodegenRestDashboard/dashboard/components/runDialog.js @@ -16,6 +16,11 @@
+
+ + + +
`; @@ -24,18 +29,30 @@ wrapper.querySelector('#closeBtn').onclick = close; wrapper.querySelector('#resumeBtn').onclick = async ()=>{ - // Use template if selected in Templates tab, or prompt user const tpls = CGStore.state.templates || []; + const chain = CGStore.getChain(id) || []; let prompt = ''; - if (tpls.length) { - const names = tpls.map((t,i)=>`${i+1}) ${t.name}`).join('\n'); - const pick = promptWindow(`Pick template index (1..${tpls.length}) or leave empty to type: \n${names}`); - if (pick) { - const idx = Number(pick)-1; if (idx>=0 && idx0 && tpls[chain[0]]) { + prompt = tpls[chain[0]].text || ''; + } else { + const pick = promptWindow('Follow-up prompt (leave empty to cancel):'); + if (!pick) return; + prompt = pick; } - if (!prompt) prompt = promptWindow('Follow-up prompt:'); - if (!prompt) return; + // Populate template selector and persist default single-template selection + const sel = wrapper.querySelector('#tplSel'); + const tpls = CGStore.state.templates || []; + sel.innerHTML = ''; + const noneOpt = document.createElement('option'); noneOpt.value = ''; noneOpt.textContent = 'None'; sel.appendChild(noneOpt); + tpls.forEach((t,i)=>{ const o=document.createElement('option'); o.value=String(i); o.textContent=t.name||`Template ${i+1}`; sel.appendChild(o); }); + const existing = (CGStore.getChain(id)||[]); + if (existing.length>0) sel.value = String(existing[0]); + wrapper.querySelector('#applyTplBtn').onclick = ()=>{ + const v = sel.value; const arr = v===''? [] : [Number(v)]; + CGStore.setChain(id, arr); + CGToast.toast('Default template updated'); + }; + await CGApi.resumeAgentRun({ agent_run_id: id, prompt }); CGToast.toast('Resume requested'); }; @@ -72,4 +89,3 @@ window.CGRunDialog = { open }; })(); - diff --git a/CodegenRestDashboard/dashboard/components/runList.js b/CodegenRestDashboard/dashboard/components/runList.js index fbc3a3812..db90936a8 100644 --- a/CodegenRestDashboard/dashboard/components/runList.js +++ b/CodegenRestDashboard/dashboard/components/runList.js @@ -58,13 +58,42 @@ filtered.forEach(r=>{ const row = document.createElement('div'); row.className='run-row'; const title = document.createElement('div'); title.textContent=`#${r.id} ${r.title||''}`; title.style.flex='1'; - const status = document.createElement('div'); status.textContent=r.status; status.className = r.status==='ACTIVE'?'status-active': (r.status==='COMPLETED'?'status-completed':''); + const status = document.createElement('div'); status.textContent=''; status.className = r.status==='ACTIVE'?'status-active': (r.status==='COMPLETED'?'status-completed':''); status.title=r.status; const pinBtn = document.createElement('button'); pinBtn.className='btn'; pinBtn.textContent = CGStore.state.pinned.includes(r.id)?'Unpin':'Pin'; pinBtn.onclick = ()=> CGStore.state.pinned.includes(r.id) ? CGStore.unpin(r.id) : CGStore.pin(r.id); const watchBtn = document.createElement('button'); watchBtn.className='btn'; watchBtn.textContent = CGStore.state.watched[r.id]?'Unwatch':'Watch'; watchBtn.onclick = ()=> CGStore.setWatched(r.id, !CGStore.state.watched[r.id]); - const openBtn = document.createElement('button'); openBtn.className='btn'; openBtn.textContent='Open'; openBtn.onclick=()=> CGRunDialog.open(r.id); - row.appendChild(title); row.appendChild(status); row.appendChild(pinBtn); row.appendChild(watchBtn); row.appendChild(openBtn); + + // If ACTIVE: show inline chain selector; if not: clicking row opens dialog for resume/logs + const actions = document.createElement('div'); actions.style.display='flex'; actions.style.gap='6px'; + if (r.status==='ACTIVE' || r.status==='PENDING') { + const chainWrap = document.createElement('div'); chainWrap.style.position='relative'; + const chainBtn = document.createElement('button'); chainBtn.className='btn'; chainBtn.textContent='Chain'; + const panel = document.createElement('div'); panel.style.display='none'; panel.style.position='absolute'; panel.style.top='28px'; panel.style.right='0'; panel.style.background='white'; panel.style.border='1px solid #e5e7eb'; panel.style.padding='8px'; panel.style.borderRadius='6px'; panel.style.zIndex='5'; + const listBox = document.createElement('div'); listBox.style.maxHeight='200px'; listBox.style.overflow='auto'; + const tpls = CGStore.state.templates || []; + const selected = new Set((CGStore.getChain(r.id)||[]).map(Number)); + tpls.forEach((t,i)=>{ + const lbl = document.createElement('label'); lbl.style.display='flex'; lbl.style.alignItems='center'; lbl.style.gap='6px'; lbl.style.fontSize='12px'; + const cb = document.createElement('input'); cb.type='checkbox'; cb.checked=selected.has(i); + cb.onchange = ()=>{ if (cb.checked) selected.add(i); else selected.delete(i); }; + const span = document.createElement('span'); span.textContent=t.name||`Template ${i+1}`; + lbl.appendChild(cb); lbl.appendChild(span); listBox.appendChild(lbl); + }); + const saveBtn = document.createElement('button'); saveBtn.className='btn primary'; saveBtn.textContent='Save'; saveBtn.style.marginTop='6px'; + saveBtn.onclick = ()=>{ CGStore.setChain(r.id, Array.from(selected)); panel.style.display='none'; CGToast.toast('Chain updated'); }; + panel.appendChild(listBox); panel.appendChild(saveBtn); + chainBtn.onclick = ()=>{ panel.style.display = panel.style.display==='none'?'block':'none'; }; + chainWrap.appendChild(chainBtn); chainWrap.appendChild(panel); + actions.appendChild(chainWrap); + // Clicking title opens dialog too for logs + title.style.cursor='pointer'; title.onclick=()=> CGRunDialog.open(r.id); + } else { + // Completed or failed: clicking title resumes via dialog + title.style.cursor='pointer'; title.onclick=()=> CGRunDialog.open(r.id); + } + actions.appendChild(pinBtn); actions.appendChild(watchBtn); + row.appendChild(title); row.appendChild(status); row.appendChild(actions); list.appendChild(row); }); runsRoot.appendChild(list); @@ -73,4 +102,3 @@ function render(state){ renderControls(state); renderList(state); } window.CGRunList = { render }; })(); - diff --git a/CodegenRestDashboard/dashboard/main.js b/CodegenRestDashboard/dashboard/main.js index e26d99d05..47cd96858 100644 --- a/CodegenRestDashboard/dashboard/main.js +++ b/CodegenRestDashboard/dashboard/main.js @@ -9,8 +9,12 @@ if (window.CGTabControl.active==='runs') CGRunList.render(state); }); + // Request Notification permission (optional) + if ('Notification' in window && Notification.permission==='default'){ + try { Notification.requestPermission().catch(()=>{}); } catch(_){} + } + // Start background watchers (auto-refresh; no manual refresh button) CGWatcher.start(); CGFollowUp.start(); })(); - diff --git a/CodegenRestDashboard/dashboard/services/watcher.js b/CodegenRestDashboard/dashboard/services/watcher.js index ee50d4c43..114f011a9 100644 --- a/CodegenRestDashboard/dashboard/services/watcher.js +++ b/CodegenRestDashboard/dashboard/services/watcher.js @@ -15,6 +15,9 @@ const old = CGStore.state.runs.find(x=>x.id===id); if (old && old.status!==r.status && (r.status==='COMPLETED'||r.status==='FAILED'||r.status==='CANCELLED')){ CGToast.toast(`Run #${id} ${r.status}`); + if ('Notification' in window && Notification.permission==='granted'){ + try { new Notification(`Run #${id} ${r.status}`); } catch(_){} + } } } catch (e){ /* ignore individual errors */ } } @@ -24,4 +27,3 @@ function start(){ if (interval) clearInterval(interval); tick(); interval = setInterval(tick, 3000); } window.CGWatcher = { start }; })(); - diff --git a/CodegenRestDashboard/dashboard/state/store.js b/CodegenRestDashboard/dashboard/state/store.js index ea3ded88b..da120b47e 100644 --- a/CodegenRestDashboard/dashboard/state/store.js +++ b/CodegenRestDashboard/dashboard/state/store.js @@ -7,11 +7,29 @@ pinned: [], // array of run ids watched: {}, // id -> boolean templates: JSON.parse(localStorage.getItem('cg_templates')||'[]'), + followUpTemplateMap: {}, // runId -> array of template indices (chain) + chainProgressMap: {}, // runId -> integer pointer into chain }; const subs = []; function notify(){ subs.forEach(fn=>fn(state)); save(); } - function save(){ localStorage.setItem('cg_pins', JSON.stringify(state.pinned)); localStorage.setItem('cg_templates', JSON.stringify(state.templates)); } - function init(){ try { state.pinned = JSON.parse(localStorage.getItem('cg_pins')||'[]'); } catch(_){} } + function save(){ + localStorage.setItem('cg_pins', JSON.stringify(state.pinned)); + localStorage.setItem('cg_templates', JSON.stringify(state.templates)); + localStorage.setItem('cg_watched', JSON.stringify(state.watched)); + localStorage.setItem('cg_chain_map', JSON.stringify(state.followUpTemplateMap)); + localStorage.setItem('cg_chain_prog', JSON.stringify(state.chainProgressMap)); + } + function init(){ + try { state.pinned = JSON.parse(localStorage.getItem('cg_pins')||'[]'); } catch(_){ } + function setChain(runId, tplIdxArr){ state.followUpTemplateMap[runId] = Array.isArray(tplIdxArr)? tplIdxArr.slice(0) : []; notify(); } + function setChainProgress(runId, n){ state.chainProgressMap[runId] = n|0; notify(); } + function getChain(runId){ return state.followUpTemplateMap[runId] || []; } + function getChainProgress(runId){ return (state.chainProgressMap[runId] | 0); } + + try { state.watched = JSON.parse(localStorage.getItem('cg_watched')||'{}'); } catch(_){ } + try { state.followUpTemplateMap = JSON.parse(localStorage.getItem('cg_chain_map')||'{}'); } catch(_){ } + try { state.chainProgressMap = JSON.parse(localStorage.getItem('cg_chain_prog')||'{}'); } catch(_){ } + } function subscribe(fn){ subs.push(fn); fn(state); return ()=>{ const i=subs.indexOf(fn); if(i>=0) subs.splice(i,1); } } function setRuns(runs){ state.runs = runs; state.activeCount = runs.filter(r=>r.status==='ACTIVE' || r.status==='PENDING' ).length; notify(); } function setFilter(f){ state.filter = f; notify(); } @@ -22,6 +40,5 @@ function updateTemplate(i, t){ state.templates[i]=t; notify(); } function deleteTemplate(i){ state.templates.splice(i,1); notify(); } init(); - window.CGStore = { state, subscribe, setRuns, setFilter, pin, unpin, setWatched, addTemplate, updateTemplate, deleteTemplate }; + window.CGStore = { state, subscribe, setRuns, setFilter, pin, unpin, setWatched, addTemplate, updateTemplate, deleteTemplate, setChain, setChainProgress, getChain, getChainProgress }; })(); - diff --git a/CodegenRestDashboard/dashboard/styles.css b/CodegenRestDashboard/dashboard/styles.css index 4a8604e14..8969a1bef 100644 --- a/CodegenRestDashboard/dashboard/styles.css +++ b/CodegenRestDashboard/dashboard/styles.css @@ -22,4 +22,11 @@ header { position: sticky; top: 0; background: #0f172a; color: white; padding: 1 .dialog-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 14px; border-bottom: 1px solid #e5e7eb; } .dialog-body { padding: 10px 14px; overflow: auto; } .logs { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 8px; } +.status-active::before, .status-completed::before, .status-failed::before { content: ''; display: inline-block; width: 8px; height: 8px; border-radius: 999px; margin-right: 6px; vertical-align: middle; } +.status-active::before { background: #06b6d4; } +.status-completed::before { background: #10b981; } +.status-failed::before { background: #ef4444; } +.run-row { padding: 4px 8px; gap: 6px; } +.btn { padding: 4px 8px; font-size: 12px; } +.controls { gap: 8px; } From bc22b3bb182a5aa4fc432d2ec898d977bb432184 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:17:19 +0000 Subject: [PATCH 3/4] feat(webhook+watcher): add local /webhook receiver + /api/events; optional CF forward; watcher consumes events for faster updates - server.js: POST /webhook stores events in-memory; GET /api/events returns+clears - webhook_server.js: forward to WEBHOOK_FORWARD_URL if set - watcher.js: poll /api/events and refresh affected runs immediately Co-authored-by: Zeeeepa --- .../dashboard/services/watcher.js | 20 +++++++++++++ CodegenRestDashboard/server.js | 29 ++++++++++++++++++- CodegenRestDashboard/webhook_server.js | 12 +++++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/CodegenRestDashboard/dashboard/services/watcher.js b/CodegenRestDashboard/dashboard/services/watcher.js index 114f011a9..4610266ae 100644 --- a/CodegenRestDashboard/dashboard/services/watcher.js +++ b/CodegenRestDashboard/dashboard/services/watcher.js @@ -19,6 +19,26 @@ try { new Notification(`Run #${id} ${r.status}`); } catch(_){} } } + // Consume webhook events if available (optimistic fast-path) + try { + const ev = await fetch('/api/events').then(r=>r.json()).catch(()=>({events:[]})); + (ev.events||[]).forEach(async (e)=>{ + const id = e?.body?.agent_run_id || e?.body?.id; + if (id){ + try { + const r = await CGApi.getAgentRun(id); + const old = CGStore.state.runs.find(x=>x.id===id); + if (old && old.status!==r.status){ + CGToast.toast(`Run #${id} ${r.status}`); + if ('Notification' in window && Notification.permission==='granted'){ + try { new Notification(`Run #${id} ${r.status}`); } catch(_){ } + } + } + } catch(_){} + } + }); + } catch(_){} + } catch (e){ /* ignore individual errors */ } } } catch (e) { console.error('watcher error', e); } diff --git a/CodegenRestDashboard/server.js b/CodegenRestDashboard/server.js index 277714af3..dc5921ccb 100644 --- a/CodegenRestDashboard/server.js +++ b/CodegenRestDashboard/server.js @@ -11,6 +11,32 @@ const TOKEN = process.env.CODEGEN_TOKEN || ''; const ORG_ID = process.env.CODEGEN_ORG_ID || ''; const OFFLINE = String(process.env.CODEGEN_OFFLINE || '0') === '1'; +// In-memory webhook events cache (best-effort; dev only) +const webhookEvents = []; + +function serveWebhook(req, res){ + let body = ''; + req.on('data', c => body += c); + req.on('end', ()=>{ + try { + const json = JSON.parse(body || '{}'); + webhookEvents.push({ ts: Date.now(), body: json }); + } catch(_) { + webhookEvents.push({ ts: Date.now(), raw: body }); + } + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true })); + }); +} + +function serveEvents(req, res){ + // Return and clear (so each event is processed once). Client can poll /api/events frequently. + const copy = webhookEvents.splice(0, webhookEvents.length); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ events: copy })); +} + + function contentType(filePath) { if (filePath.endsWith('.html')) return 'text/html; charset=utf-8'; if (filePath.endsWith('.css')) return 'text/css; charset=utf-8'; @@ -64,6 +90,8 @@ async function proxyApi(req, res) { } const server = http.createServer((req, res) => { + if (req.url.startsWith('/api/events') && req.method==='GET') return serveEvents(req, res); + if (req.url === '/webhook' && req.method==='POST') return serveWebhook(req, res); if (req.url.startsWith('/api/')) return proxyApi(req, res); return serveStatic(req, res); }); @@ -71,4 +99,3 @@ const server = http.createServer((req, res) => { server.listen(PORT, () => { console.log(`[CodegenRestDashboard] Server listening on http://localhost:${PORT}`); }); - diff --git a/CodegenRestDashboard/webhook_server.js b/CodegenRestDashboard/webhook_server.js index 89d6a50c1..7513401da 100644 --- a/CodegenRestDashboard/webhook_server.js +++ b/CodegenRestDashboard/webhook_server.js @@ -33,10 +33,20 @@ export default { // Here, we simply log and return 200. console.log('Codegen webhook:', bodyText.slice(0, 500)); + // Optional forward to another endpoint (e.g., your local proxy /webhook) + const forwardUrl = env.WEBHOOK_FORWARD_URL || ''; + if (forwardUrl) { + try { + await fetch(forwardUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: bodyText }); + } catch (e) { + console.log('Forward failed:', e.message || e); + } + } + + return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'Content-Type': 'application/json' } }); } }; - From 46e900ad654ebd6ced64846882de59169ab6e873 Mon Sep 17 00:00:00 2001 From: "codegen-sh[bot]" <131295404+codegen-sh[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 08:30:19 +0000 Subject: [PATCH 4/4] Dashboard fixes and templating: - Fix store.js init() braces and state load - Move webhook polling outside per-run loop; clean watcher - Harden static file serving (path normalize, traversal guard) - Make API client honor http/https and custom ports - Add browser templating engine and integrate in auto follow-ups + run dialog - Populate template selector correctly; resolve variables on resume - Add Node CLI render_template for variable testing Co-authored-by: Zeeeepa --- .../commands/render_template.js | 30 ++++++++++ .../dashboard/automation/followUpManager.js | 15 ++++- .../dashboard/components/runDialog.js | 56 +++++++++++++------ CodegenRestDashboard/dashboard/index.html | 2 +- .../dashboard/services/watcher.js | 40 +++++++------ CodegenRestDashboard/dashboard/state/store.js | 9 ++- CodegenRestDashboard/server.js | 9 ++- CodegenRestDashboard/utils/apiClient.js | 6 +- CodegenRestDashboard/utils/templateEngine.js | 22 ++++++++ 9 files changed, 142 insertions(+), 47 deletions(-) create mode 100644 CodegenRestDashboard/commands/render_template.js create mode 100644 CodegenRestDashboard/utils/templateEngine.js diff --git a/CodegenRestDashboard/commands/render_template.js b/CodegenRestDashboard/commands/render_template.js new file mode 100644 index 000000000..095d944cc --- /dev/null +++ b/CodegenRestDashboard/commands/render_template.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node +const path = require('path'); +const { loadEnv } = require('../utils/env'); +loadEnv(path.join(__dirname, '..', '.env')); + +// Minimal inlined renderer to avoid depending on dashboard file +function getPath(obj, p){ try { return p.split('.').reduce((a,k)=>(a&&a[k]!=null?a[k]:undefined), obj);} catch(_){ return undefined; } } +function renderTemplate(tpl, vars){ return String(tpl||'').replace(/\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g,(_,k)=>{ const v=getPath(vars||{},k); return v==null? '': String(v); }); } + +async function main(){ + const args = process.argv.slice(2); + let template = ''; + let varsJson = '{}'; + for (let i=0;i{ console.error(e.message||e); process.exit(1); }); } +module.exports = main; + diff --git a/CodegenRestDashboard/dashboard/automation/followUpManager.js b/CodegenRestDashboard/dashboard/automation/followUpManager.js index 2f001d8e8..bf20246e5 100644 --- a/CodegenRestDashboard/dashboard/automation/followUpManager.js +++ b/CodegenRestDashboard/dashboard/automation/followUpManager.js @@ -18,7 +18,20 @@ if (tpls[tplIdx]){ const text = tpls[tplIdx].text || ''; if (text){ - await CGApi.resumeAgentRun({ agent_run_id: id, prompt: text }); + const vars = { + run_id: id, + status: cur.status, + title: cur.title, + summary: cur.summary, + result: cur.result, + created_at: cur.created_at, + now: new Date().toISOString(), + agent_run: cur, + }; + const prompt = (window.CGTemplate && CGTemplate.renderTemplate) + ? CGTemplate.renderTemplate(text, vars) + : text; + await CGApi.resumeAgentRun({ agent_run_id: id, prompt }); CGStore.setChainProgress(id, prog+1); CGToast.toast(`Auto-follow-up ${prog+1}/${chain.length} sent for #${id}`); } diff --git a/CodegenRestDashboard/dashboard/components/runDialog.js b/CodegenRestDashboard/dashboard/components/runDialog.js index 2af8a5556..d145af269 100644 --- a/CodegenRestDashboard/dashboard/components/runDialog.js +++ b/CodegenRestDashboard/dashboard/components/runDialog.js @@ -28,32 +28,53 @@ function close(){ clearInterval(t); dialogs.removeChild(wrapper); } wrapper.querySelector('#closeBtn').onclick = close; + // Populate template selector and persist default single-template selection + const sel = wrapper.querySelector('#tplSel'); + function refreshTemplateSelector(){ + const tpls = CGStore.state.templates || []; + sel.innerHTML = ''; + const noneOpt = document.createElement('option'); noneOpt.value = ''; noneOpt.textContent = 'None'; sel.appendChild(noneOpt); + tpls.forEach((t,i)=>{ const o=document.createElement('option'); o.value=String(i); o.textContent=t.name||`Template ${i+1}`; sel.appendChild(o); }); + const existing = (CGStore.getChain(id)||[]); + if (existing.length>0) sel.value = String(existing[0]); + } + refreshTemplateSelector(); + + wrapper.querySelector('#applyTplBtn').onclick = ()=>{ + const v = sel.value; const arr = v===''? [] : [Number(v)]; + CGStore.setChain(id, arr); + CGToast.toast('Default template updated'); + }; + wrapper.querySelector('#resumeBtn').onclick = async ()=>{ const tpls = CGStore.state.templates || []; const chain = CGStore.getChain(id) || []; - let prompt = ''; + let promptText = ''; if (chain.length>0 && tpls[chain[0]]) { - prompt = tpls[chain[0]].text || ''; + promptText = tpls[chain[0]].text || ''; } else { const pick = promptWindow('Follow-up prompt (leave empty to cancel):'); if (!pick) return; - prompt = pick; + promptText = pick; } - // Populate template selector and persist default single-template selection - const sel = wrapper.querySelector('#tplSel'); - const tpls = CGStore.state.templates || []; - sel.innerHTML = ''; - const noneOpt = document.createElement('option'); noneOpt.value = ''; noneOpt.textContent = 'None'; sel.appendChild(noneOpt); - tpls.forEach((t,i)=>{ const o=document.createElement('option'); o.value=String(i); o.textContent=t.name||`Template ${i+1}`; sel.appendChild(o); }); - const existing = (CGStore.getChain(id)||[]); - if (existing.length>0) sel.value = String(existing[0]); - wrapper.querySelector('#applyTplBtn').onclick = ()=>{ - const v = sel.value; const arr = v===''? [] : [Number(v)]; - CGStore.setChain(id, arr); - CGToast.toast('Default template updated'); - }; + // Resolve template variables using current run meta + let meta = {}; + try { meta = await CGApi.getAgentRun(id); } catch(_){} + const vars = { + run_id: id, + status: meta.status, + title: meta.title, + summary: meta.summary, + result: meta.result, + created_at: meta.created_at, + now: new Date().toISOString(), + agent_run: meta, + }; + const finalPrompt = (window.CGTemplate && CGTemplate.renderTemplate) + ? CGTemplate.renderTemplate(promptText, vars) + : promptText; - await CGApi.resumeAgentRun({ agent_run_id: id, prompt }); + await CGApi.resumeAgentRun({ agent_run_id: id, prompt: finalPrompt }); CGToast.toast('Resume requested'); }; @@ -89,3 +110,4 @@ window.CGRunDialog = { open }; })(); + diff --git a/CodegenRestDashboard/dashboard/index.html b/CodegenRestDashboard/dashboard/index.html index 36e5a9cf6..2382e332f 100644 --- a/CodegenRestDashboard/dashboard/index.html +++ b/CodegenRestDashboard/dashboard/index.html @@ -25,9 +25,9 @@ + - diff --git a/CodegenRestDashboard/dashboard/services/watcher.js b/CodegenRestDashboard/dashboard/services/watcher.js index 4610266ae..c8a7e3646 100644 --- a/CodegenRestDashboard/dashboard/services/watcher.js +++ b/CodegenRestDashboard/dashboard/services/watcher.js @@ -7,9 +7,12 @@ const runs = (data.data||data.runs||[]); CGStore.setRuns(runs); - // Watch pinned/watched runs for status transitions - const watchedIds = Object.entries(CGStore.state.watched).filter(([,v])=>v).map(([k])=>Number(k)); - for (const id of new Set([ ...watchedIds, ...CGStore.state.pinned ])){ + // Watch pinned/watched runs for status transitions (poll individually) + const watchedIds = Object.entries(CGStore.state.watched) + .filter(([,v])=>v) + .map(([k])=>Number(k)); + const ids = new Set([ ...watchedIds, ...CGStore.state.pinned ]); + for (const id of ids){ try { const r = await CGApi.getAgentRun(id); const old = CGStore.state.runs.find(x=>x.id===id); @@ -19,31 +22,32 @@ try { new Notification(`Run #${id} ${r.status}`); } catch(_){} } } - // Consume webhook events if available (optimistic fast-path) + } catch (e){ /* ignore individual errors */ } + } + + // Consume webhook events once per tick (outside per-run loop) try { const ev = await fetch('/api/events').then(r=>r.json()).catch(()=>({events:[]})); (ev.events||[]).forEach(async (e)=>{ const id = e?.body?.agent_run_id || e?.body?.id; - if (id){ - try { - const r = await CGApi.getAgentRun(id); - const old = CGStore.state.runs.find(x=>x.id===id); - if (old && old.status!==r.status){ - CGToast.toast(`Run #${id} ${r.status}`); - if ('Notification' in window && Notification.permission==='granted'){ - try { new Notification(`Run #${id} ${r.status}`); } catch(_){ } - } + if (!id) return; + try { + const r = await CGApi.getAgentRun(id); + const old = CGStore.state.runs.find(x=>x.id===id); + if (old && old.status!==r.status){ + CGToast.toast(`Run #${id} ${r.status}`); + if ('Notification' in window && Notification.permission==='granted'){ + try { new Notification(`Run #${id} ${r.status}`); } catch(_){ } } - } catch(_){} - } + } + } catch(_){ /* ignore */ } }); - } catch(_){} + } catch(_){ /* ignore webhook errors */ } - } catch (e){ /* ignore individual errors */ } - } } catch (e) { console.error('watcher error', e); } } function start(){ if (interval) clearInterval(interval); tick(); interval = setInterval(tick, 3000); } window.CGWatcher = { start }; })(); + diff --git a/CodegenRestDashboard/dashboard/state/store.js b/CodegenRestDashboard/dashboard/state/store.js index da120b47e..4a87d8eef 100644 --- a/CodegenRestDashboard/dashboard/state/store.js +++ b/CodegenRestDashboard/dashboard/state/store.js @@ -21,15 +21,14 @@ } function init(){ try { state.pinned = JSON.parse(localStorage.getItem('cg_pins')||'[]'); } catch(_){ } - function setChain(runId, tplIdxArr){ state.followUpTemplateMap[runId] = Array.isArray(tplIdxArr)? tplIdxArr.slice(0) : []; notify(); } - function setChainProgress(runId, n){ state.chainProgressMap[runId] = n|0; notify(); } - function getChain(runId){ return state.followUpTemplateMap[runId] || []; } - function getChainProgress(runId){ return (state.chainProgressMap[runId] | 0); } - try { state.watched = JSON.parse(localStorage.getItem('cg_watched')||'{}'); } catch(_){ } try { state.followUpTemplateMap = JSON.parse(localStorage.getItem('cg_chain_map')||'{}'); } catch(_){ } try { state.chainProgressMap = JSON.parse(localStorage.getItem('cg_chain_prog')||'{}'); } catch(_){ } } + function setChain(runId, tplIdxArr){ state.followUpTemplateMap[runId] = Array.isArray(tplIdxArr)? tplIdxArr.slice(0) : []; notify(); } + function setChainProgress(runId, n){ state.chainProgressMap[runId] = n|0; notify(); } + function getChain(runId){ return state.followUpTemplateMap[runId] || []; } + function getChainProgress(runId){ return (state.chainProgressMap[runId] | 0); } function subscribe(fn){ subs.push(fn); fn(state); return ()=>{ const i=subs.indexOf(fn); if(i>=0) subs.splice(i,1); } } function setRuns(runs){ state.runs = runs; state.activeCount = runs.filter(r=>r.status==='ACTIVE' || r.status==='PENDING' ).length; notify(); } function setFilter(f){ state.filter = f; notify(); } diff --git a/CodegenRestDashboard/server.js b/CodegenRestDashboard/server.js index dc5921ccb..f786cb309 100644 --- a/CodegenRestDashboard/server.js +++ b/CodegenRestDashboard/server.js @@ -46,10 +46,13 @@ function contentType(filePath) { } function serveStatic(req, res) { - let file = req.url.split('?')[0]; + let file = req.url.split('?')[0] || '/index.html'; if (file === '/' || file === '') file = '/index.html'; - const p = path.join(__dirname, 'dashboard', file); - if (!p.startsWith(path.join(__dirname, 'dashboard'))) { + // Prevent path traversal and fix leading slash + const baseDir = path.join(__dirname, 'dashboard'); + const rel = file.replace(/^\/+/, ''); + const p = path.normalize(path.join(baseDir, rel)); + if (!p.startsWith(baseDir)) { res.writeHead(403); return res.end('Forbidden'); } fs.readFile(p, (err, data) => { diff --git a/CodegenRestDashboard/utils/apiClient.js b/CodegenRestDashboard/utils/apiClient.js index e13927d11..04eee146e 100644 --- a/CodegenRestDashboard/utils/apiClient.js +++ b/CodegenRestDashboard/utils/apiClient.js @@ -1,5 +1,6 @@ // Minimal API client (Node-only) const { loadEnv } = require('./env'); +const http = require('http'); const https = require('https'); loadEnv(); @@ -16,9 +17,11 @@ function nodeFetch(url, options) { } return new Promise((resolve, reject) => { const u = new URL(url); - const req = https.request({ + const mod = u.protocol === 'http:' ? http : https; + const req = mod.request({ method: options?.method || 'GET', hostname: u.hostname, + port: u.port || (u.protocol === 'http:' ? 80 : 443), path: u.pathname + u.search, headers: options?.headers || {}, }, (res) => { @@ -121,4 +124,3 @@ module.exports = { pathLogs, pathGenerateSetup, }; - diff --git a/CodegenRestDashboard/utils/templateEngine.js b/CodegenRestDashboard/utils/templateEngine.js new file mode 100644 index 000000000..1520a4ff9 --- /dev/null +++ b/CodegenRestDashboard/utils/templateEngine.js @@ -0,0 +1,22 @@ +// Simple, safe templating: replaces {{var}} with values from vars (supports dot paths) +(function(){ + function getPath(obj, path) { + try { + return path.split('.').reduce((acc, key) => (acc && acc[key] != null ? acc[key] : undefined), obj); + } catch (_) { return undefined; } + } + + function renderTemplate(template, vars) { + if (typeof template !== 'string') return ''; + return template.replace(/\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/g, (m, key) => { + const v = getPath(vars || {}, key); + if (v == null) return ''; + return String(v); + }); + } + + // Expose for browser and Node + if (typeof window !== 'undefined') window.CGTemplate = { renderTemplate }; + module.exports = { renderTemplate }; +})(); +