diff --git a/packaging/mother/colibri-mcp-ssh b/packaging/mother/colibri-mcp-ssh index 9417e7c..754a130 100755 --- a/packaging/mother/colibri-mcp-ssh +++ b/packaging/mother/colibri-mcp-ssh @@ -30,12 +30,14 @@ case "${SSH_ORIGINAL_COMMAND:-}" in # Input: {"node_hostname":"debby","task_id":"abc","provider":"deepseek", # "model":"deepseek-chat","input_tokens":150,"output_tokens":80, # "cache_read_tokens":200,"cache_write_tokens":50, - # "cost_usd":0.0042,"success":true,"proof_text":"a1b2c3d4e5f6", + # "cost_usd":0.0042,"success":true, + # "proof_text":"agent:hermes|cost:0.0042|tokens:150/80", + # "screenshot_uuid":"a1b2c3d4e5f6", # "finished_at":"2026-06-27T12:00:00Z"} psql -d mother_hive -tA -v ON_ERROR_STOP=1 <<'PSQL' INSERT INTO task_costs (node_id, task_id, provider, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, - cost_usd, success, proof_text, finished_at) + cost_usd, success, proof_text, screenshot_uuid, finished_at) SELECT (SELECT id FROM hive_nodes WHERE hostname = j->>'node_hostname'), j->>'task_id', @@ -48,6 +50,7 @@ SELECT COALESCE((j->>'cost_usd')::DOUBLE PRECISION, 0.0), COALESCE((j->>'success')::BOOLEAN, false), NULLIF(j->>'proof_text', ''), + NULLIF(j->>'screenshot_uuid', ''), COALESCE((j->>'finished_at')::TIMESTAMPTZ, now()) FROM (SELECT (pg_read_file('/dev/stdin')::JSONB) AS j) AS _; PSQL diff --git a/packaging/mother/dashboard/deploy.sh b/packaging/mother/dashboard/deploy.sh index 1536919..79f25e1 100755 --- a/packaging/mother/dashboard/deploy.sh +++ b/packaging/mother/dashboard/deploy.sh @@ -5,7 +5,7 @@ # Places: # /usr/local/www/clawdie/dashboard/index.html — dashboard page # /usr/local/www/clawdie/dashboard/export-costs.sh — JSON export (cron) -# /usr/local/etc/cron.d/clawdie-dashboard — cron job +# /etc/crontab entry — cron job (FreeBSD) # # The dashboard reads task_costs.json (exported every 60s by cron) and # links screenshots from ../screenshots/ (tmux-screenshot publish dir). @@ -22,7 +22,7 @@ set -eu SRC="$(dirname "$0")" WEBROOT="/usr/local/www/clawdie/dashboard" -CRON_FILE="/usr/local/etc/cron.d/clawdie-dashboard" +CRON_MARKER="# clawdie-dashboard — auto-managed by deploy.sh" echo "=== deploy cost dashboard ===" @@ -32,18 +32,24 @@ cp "$SRC/index.html" "$WEBROOT/index.html" cp "$SRC/export-costs.sh" "$WEBROOT/export-costs.sh" chmod +x "$WEBROOT/export-costs.sh" -# Idempotent cron entry: export every 60s -cat > "$CRON_FILE" <<'CRON' -# clawdie cost dashboard — export task_costs to JSON every 60s -* * * * * root /usr/local/www/clawdie/dashboard/export-costs.sh -CRON +# FreeBSD cron: add to /etc/crontab (idempotent via marker line). +CRON_ENTRY="* * * * * root /usr/local/www/clawdie/dashboard/export-costs.sh ${CRON_MARKER}" +if ! grep -qF "${CRON_MARKER}" /etc/crontab 2>/dev/null; then + echo "${CRON_ENTRY}" >> /etc/crontab + echo " cron → /etc/crontab (added)" +else + echo " cron → /etc/crontab (already present)" +fi echo " dashboard → $WEBROOT/index.html" -echo " cron → $CRON_FILE" echo "" # Run once immediately to seed the data echo "=== initial export ===" -"$WEBROOT/export-costs.sh" || echo " (no data yet — tasks will appear as agents complete)" +if "$WEBROOT/export-costs.sh"; then + echo "" +else + echo " (no data yet — tasks will appear as agents complete)" +fi echo "" -echo "Done. Dashboard at: https://mother.clawdie.si/dashboard/" +echo "Done. Dashboard at: https://osa.smilepowered.org/dashboard/" diff --git a/packaging/mother/dashboard/export-costs.sh b/packaging/mother/dashboard/export-costs.sh index d704b18..d63bfce 100755 --- a/packaging/mother/dashboard/export-costs.sh +++ b/packaging/mother/dashboard/export-costs.sh @@ -7,7 +7,7 @@ set -eu OUTDIR="/usr/local/www/clawdie/dashboard" mkdir -p "$OUTDIR" -psql -d mother_hive -tA <<'SQL' > "${OUTDIR}/task_costs.json" +sudo -u postgres psql -d mother_hive -tA <<'SQL' > "${OUTDIR}/task_costs.json" SELECT json_build_object( 'updated_at', now(), 'summary', json_build_object( @@ -63,14 +63,15 @@ SELECT json_build_object( tc.cost_usd, tc.success, tc.finished_at, - tc.proof_text + tc.proof_text, + tc.screenshot_uuid FROM task_costs tc LEFT JOIN hive_nodes hn ON hn.id = tc.node_id ORDER BY tc.finished_at DESC LIMIT 200 ) AS t ) -) AS result; +) AS result FROM task_costs; SQL echo "dashboard: $(date -Iseconds) — $(jq '.summary.total_tasks' "${OUTDIR}/task_costs.json") tasks" >&2 diff --git a/packaging/mother/dashboard/index.html b/packaging/mother/dashboard/index.html index a23eec9..dc109de 100644 --- a/packaging/mother/dashboard/index.html +++ b/packaging/mother/dashboard/index.html @@ -114,6 +114,7 @@ h1 .dot{display:inline-block; width:8px; height:8px; border-radius:50%; margin-r .lightbox{display:none; position:fixed; inset:0; background:rgba(0,0,0,.94); z-index:1000; align-items:center; justify-content:center; padding:1.5rem} .lightbox.open{display:flex} +.lightbox img{max-width:94vw; max-height:88vh; border-radius:4px; box-shadow:0 0 40px rgba(0,180,216,.15)} .lightbox pre{max-width:94vw; max-height:88vh; border-radius:4px; box-shadow:0 0 40px rgba(0,180,216,.15)} .lightbox-close{position:fixed; top:1rem; right:1.5rem; background:var(--surface); color:var(--fg2); border:1px solid var(--border); border-radius:4px; padding:.4rem 1rem; font-family:inherit; @@ -189,6 +190,7 @@ h1 .dot{display:inline-block; width:8px; height:8px; border-radius:50%; margin-r @@ -304,7 +306,9 @@ function renderCard(t) { const total = (t.input_tokens||0) + (t.cache_read_tokens||0); const cachePct = total > 0 ? Math.round((t.cache_read_tokens||0) / total * 100) : 0; const freshPct = 100 - cachePct; - const hasProof = !!t.proof_text; + const hasProofText = !!t.proof_text; + const hasScreenshot = !!t.screenshot_uuid; + const hasProof = hasProofText || hasScreenshot; const cls = hasProof ? 'card has-proof' : 'card'; const onClick = hasProof ? `onclick="openProof(this,'${esc(t.task_id||'')}')"` @@ -323,21 +327,33 @@ function renderCard(t) {
${fmtTokens(t.input_tokens||0)} in · ${fmtTokens(t.output_tokens||0)} out ${cachePct>0?` · ${cachePct}% cache` : ''}
- ${hasProof ? '
▸ proof
' : ''} + ${hasScreenshot ? '
▸ screenshot
' : hasProofText ? '
▸ text
' : ''} `; } // ══ lightbox ════════════════════════════════════════════════════════════ function openProof(_el, taskId) { const t = (DATA.tasks || []).find(t => t.task_id === taskId); - const raw = t ? (t.proof_text || '{}') : '{}'; - let display; - try { display = JSON.stringify(JSON.parse(raw), null, 2); } - catch(_) { display = raw; } const lb = document.getElementById('lightbox'); - document.getElementById('lb-text').textContent = display; - document.getElementById('lightbox-meta').innerHTML = - `${esc(taskId)} ${esc(t?.provider||'')} · $${(t?.cost_usd||0).toFixed(4)}`; + const meta = document.getElementById('lightbox-meta'); + const img = document.getElementById('lb-img'); + const pre = document.getElementById('lb-text'); + meta.innerHTML = `${esc(taskId)} ${esc(t?.provider||'')} · $${(t?.cost_usd||0).toFixed(4)}`; + + // Hide both, then show the appropriate one. + img.style.display = 'none'; + pre.style.display = 'none'; + + if (t?.screenshot_uuid) { + img.src = `../screenshots/${t.screenshot_uuid}.png`; + img.style.display = 'block'; + } else if (t?.proof_text) { + let display; + try { display = JSON.stringify(JSON.parse(t.proof_text), null, 2); } + catch(_) { display = t.proof_text; } + pre.textContent = display; + pre.style.display = 'block'; + } lb.classList.add('open'); } function closeLb() { document.getElementById('lightbox').classList.remove('open'); } @@ -357,7 +373,7 @@ function showJSON() { byNode[n].push({task_id:t.task_id,provider:t.provider,model:t.model, cost:t.cost_usd,success:t.success,finished_at:t.finished_at, tokens:{in:t.input_tokens,out:t.output_tokens,cache_read:t.cache_read_tokens}, - proof_text:t.proof_text||null}); + proof_text:t.proof_text||null,screenshot_uuid:t.screenshot_uuid||null}); } const output = {updated_at:DATA.updated_at, summary:DATA.summary, nodes:byNode}; document.getElementById('json-output').textContent = JSON.stringify(output, null, 2); diff --git a/packaging/mother/mother_schema.sql b/packaging/mother/mother_schema.sql index bcc6276..a4ec06d 100644 --- a/packaging/mother/mother_schema.sql +++ b/packaging/mother/mother_schema.sql @@ -65,15 +65,16 @@ CREATE TABLE IF NOT EXISTS task_costs ( cache_write_tokens BIGINT NOT NULL DEFAULT 0, cost_usd DOUBLE PRECISION NOT NULL DEFAULT 0.0, success BOOLEAN NOT NULL DEFAULT false, - proof_text TEXT, -- tmux-screenshot content hash (12-char UUID) + proof_text TEXT, -- glasspane evidence at task exit (agent, state, tokens) + screenshot_uuid TEXT, -- visual terminal proof (PNG capture UUID) finished_at TIMESTAMPTZ NOT NULL DEFAULT now(), - reported_at TIMESTAMPTZ NOT NULL DEFAULT now(), - proof_text TEXT -- optional; links to tmux-screenshot capture at task completion + reported_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_task_costs_node ON task_costs (node_id); CREATE INDEX IF NOT EXISTS idx_task_costs_finished ON task_costs (finished_at DESC); CREATE INDEX IF NOT EXISTS idx_task_costs_provider ON task_costs (provider, model); ALTER TABLE task_costs ADD COLUMN IF NOT EXISTS proof_text TEXT; +ALTER TABLE task_costs ADD COLUMN IF NOT EXISTS screenshot_uuid TEXT; CREATE TABLE IF NOT EXISTS build_queue ( id SERIAL PRIMARY KEY,