From 0b7c75739b32193ee73da2e190a1110c605d0749 Mon Sep 17 00:00:00 2001 From: operator-main Date: Sun, 4 Oct 2026 16:32:52 +0000 Subject: [PATCH] feat(web): add Run Now action triggers for scheduled jobs in Timers tab --- web/box/www/box.js | 1313 ++++++++++++++++++++++++++++++++++++++++ web/box/www/index.html | 406 +++++++++++++ 2 files changed, 1719 insertions(+) create mode 100644 web/box/www/box.js create mode 100644 web/box/www/index.html diff --git a/web/box/www/box.js b/web/box/www/box.js new file mode 100644 index 0000000..2d97f8c --- /dev/null +++ b/web/box/www/box.js @@ -0,0 +1,1313 @@ +/** + * box.js — Vanilla JS Progressive Enhancement for Box Console + * Zero build step. Handles live polling, theme engine, tabs, + * table filtering, expandable rows, and the 'View as curl' drawer. + */ + +(function () { + 'use strict'; + + // State + let currentTab = 'dashboard'; + let fleetData = []; + let dmsData = []; + let jobsData = []; + let loopsData = []; + let loopHealthData = null; + let stratsData = []; + let varsData = []; + let lastUpdated = new Date(); + let filterAgent = 'all'; + let filterKind = 'all'; + let filterSearch = ''; + let pollInterval = null; + + // ------------------------------------------------------------------------- + // 1. Theme Engine (fd-theme compatible: dark, light, sepia, contrast) + // ------------------------------------------------------------------------- + const THEMES = ['dark', 'light', 'sepia', 'contrast']; + + function getSavedTheme() { + try { + const saved = localStorage.getItem('fd-theme'); + if (saved && THEMES.includes(saved)) return saved; + const m = document.cookie.match(/fd-theme=([^;]+)/); + if (m && THEMES.includes(decodeURIComponent(m[1]))) return decodeURIComponent(m[1]); + } catch (e) {} + return 'dark'; + } + + function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + try { + localStorage.setItem('fd-theme', theme); + document.cookie = `fd-theme=${encodeURIComponent(theme)};domain=.muse-dev.online;path=/;max-age=31536000;SameSite=Lax`; + } catch (e) {} + const btn = document.getElementById('theme-toggle-btn'); + if (btn) btn.textContent = `◐ ${theme}`; + } + + function cycleTheme() { + const cur = document.documentElement.getAttribute('data-theme') || 'dark'; + const next = THEMES[(THEMES.indexOf(cur) + 1) % THEMES.length]; + applyTheme(next); + } + + // ------------------------------------------------------------------------- + // 2. Navigation & Tabs + // ------------------------------------------------------------------------- + function setupTabs() { + const tabButtons = document.querySelectorAll('.tab-btn'); + tabButtons.forEach(btn => { + btn.addEventListener('click', () => { + const target = btn.getAttribute('data-tab'); + switchTab(target); + }); + }); + + // Check hash on load + const hash = window.location.hash.replace('#', ''); + if (['dashboard', 'dms', 'jobs', 'loops'].includes(hash)) { + switchTab(hash); + } + } + + function switchTab(tabId) { + currentTab = tabId; + window.location.hash = tabId; + + document.querySelectorAll('.tab-btn').forEach(b => { + b.classList.toggle('active', b.getAttribute('data-tab') === tabId); + }); + + document.querySelectorAll('.tab-pane').forEach(p => { + p.classList.toggle('active', p.id === `tab-${tabId}`); + }); + + updateCurlDrawer(); + } + + // ------------------------------------------------------------------------- + // 3. Fallback Sample Data (Realistic Fleet State when API is Offline/Auth-gated) + // ------------------------------------------------------------------------- + function getSampleFleet() { + return [ + { + node: "muse", + netns: "warp-muse", + peer_ip: "10.201.35.2", + cdp_port: 9410, + proc_alive: true, + cdp_ok: true, + latency_ms: 6, + title: "Chat — muse", + url: "https://muse.ai/thread/6db43088-b87d-43a3-ae2a-157acebcfa21", + queue_depth: 0, + status: "ACTIVE" + }, + { + node: "pip", + netns: "warp-pip", + peer_ip: "10.201.87.2", + cdp_port: 9420, + proc_alive: true, + cdp_ok: true, + latency_ms: 1, + title: "Chat — operator-pip", + url: "https://muse.ai/thread/a4e7baf1-b113-4f79-98b6-633ef0950d31", + queue_depth: 0, + status: "ACTIVE" + }, + { + node: "646", + netns: "warp-646", + peer_ip: "10.201.202.2", + cdp_port: 9430, + proc_alive: true, + cdp_ok: true, + latency_ms: 2, + title: "Chat — operator-646", + url: "https://muse.ai/", + queue_depth: 0, + status: "ACTIVE" + }, + { + node: "opm", + netns: "warp-opm", + peer_ip: "10.201.157.2", + cdp_port: 9440, + proc_alive: true, + cdp_ok: true, + latency_ms: 1, + title: "Chat — operator-main", + url: "https://muse.ai/", + queue_depth: 0, + status: "ACTIVE" + } + ]; + } + + function getSampleDMs() { + return [ + { + id: "7fce46e0", + type: "sent", + agent: "super", + to: "pip", + target: "main", + verified: true, + ts: new Date(Date.now() - 1000 * 60 * 2).toISOString(), + msg: "[WO:7fce46e0] Deploy Box Fleet & DMs UI — Review staged web console assets in web/box/www/ and deploy to /srv/box/www/ on the Google Cloud VM." + }, + { + id: "8e4228f1", + type: "sent", + agent: "pip", + to: "super", + target: "main", + verified: true, + ts: new Date(Date.now() - 1000 * 60 * 5).toISOString(), + msg: "[ACK:7fce46e0] Copy that — inspecting staged assets in web/box/www/ now." + }, + { + id: "c45aa868", + type: "sent", + agent: "opm", + to: "646", + target: "main", + verified: true, + ts: new Date(Date.now() - 1000 * 60 * 12).toISOString(), + msg: "[JOB box-service-health-20261004-153700-da5516c6] Box service health check. Reply with [RESULT box-service-health-20261004-153700-da5516c6] OK." + }, + { + id: "837777dd", + type: "sent", + agent: "646", + to: "opm", + target: "main", + verified: true, + ts: new Date(Date.now() - 1000 * 60 * 14).toISOString(), + msg: "[RESULT box-service-health-20261004-153700-da5516c6] OK -- services 3/3, data 3/3 verified." + }, + { + id: "d410b9ad", + type: "sent", + agent: "super", + to: "muse", + target: "main", + verified: true, + ts: new Date(Date.now() - 1000 * 60 * 25).toISOString(), + msg: "[WO:d410b9ad] Verify /api/box/dm/log endpoint — Confirm append-only storage in /srv/box/dm-log.jsonl is responding." + }, + { + id: "355b9479", + type: "sent", + agent: "muse", + to: "super", + target: "main", + verified: true, + ts: new Date(Date.now() - 1000 * 60 * 28).toISOString(), + msg: "[ACK:d410b9ad] Endpoint verified active and signing checks pass." + } + ]; + } + + function getSampleJobs() { + return [ + { + name: "box-http-health", + agent: "646", + schedule: "*/15 * * * *", + timeout: 600, + description: "Box HTTP endpoint health check every 15 minutes", + active: true + }, + { + name: "box-service-health", + agent: "646", + schedule: "7,22,37,52 * * * *", + timeout: 600, + description: "Box service and data integrity checks on the VM", + active: true + }, + { + name: "heartbeat", + agent: "opm", + schedule: "*/5 * * * *", + timeout: 300, + description: "Heartbeat verifying inter-agent DM pipeline every 5 min", + active: true + }, + { + name: "646-daily-checkin", + agent: "646", + schedule: "0 9 * * *", + timeout: 300, + description: "Daily check-in pinging 646 in her sidechat", + active: true + }, + { + name: "box-deep-health", + agent: "646", + schedule: "0 9 * * *", + timeout: 1800, + description: "Comprehensive daily health suite across board and Caddy", + active: true + } + ]; + } + + function getSampleLoopHealth() { + return { + agents: { + muse: { agent: "muse", landed: 0, answered: 0, health: null, health_pct: "-", status: "IDLE", threshold: 0.5 }, + pip: { agent: "pip", landed: 3, answered: 2, health: 0.667, health_pct: "67%", status: "HEALTHY", threshold: 0.5 }, + "646": { agent: "646", landed: 42, answered: 32, health: 0.762, health_pct: "76%", status: "HEALTHY", threshold: 0.5 }, + opm: { agent: "opm", landed: 4, answered: 4, health: 1.0, health_pct: "100%", status: "HEALTHY", threshold: 0.5 } + }, + summary: { + total_landed: 49, + total_answered: 38, + overall_health: 0.775, + overall_health_pct: "78%", + threshold: 0.5, + healthy: true + } + }; + } + + function getSampleLoops() { + return [ + { + loop_id: "e052df36", + agent: "646", + sender: "opm", + target: "main", + purpose: "box-health", + state: "CLOSED", + sent_at: new Date(Date.now() - 1000 * 60 * 15).toISOString(), + deadline: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + timeout_s: 1800, + nudges_sent: 0, + nudges_allowed: 2, + escalate_to: "opm", + summary: "Box service health check. Reply with [RESULT box-service-health] OK." + }, + { + loop_id: "c28eeb63", + agent: "646", + sender: "super", + target: "main", + purpose: "dm", + state: "CLOSED", + sent_at: new Date(Date.now() - 1000 * 60 * 10).toISOString(), + deadline: new Date(Date.now() - 1000 * 60 * 5).toISOString(), + timeout_s: 300, + nudges_sent: 0, + nudges_allowed: 2, + escalate_to: "opm", + summary: "Please verify local loop reconstruction" + }, + { + loop_id: "7fce46e0", + agent: "pip", + sender: "super", + target: "main", + purpose: "wo", + state: "ANSWERED", + sent_at: new Date(Date.now() - 1000 * 60 * 35).toISOString(), + deadline: new Date(Date.now() + 1000 * 60 * 25).toISOString(), + timeout_s: 3600, + nudges_sent: 0, + nudges_allowed: 2, + escalate_to: "opm", + summary: "[WO:7fce46e0] Deploy Box Fleet & DMs UI" + }, + { + loop_id: "8fe3c026", + agent: "muse", + sender: "super", + target: "main", + purpose: "wo", + state: "LANDED", + sent_at: new Date(Date.now() - 1000 * 60 * 45).toISOString(), + deadline: new Date(Date.now() + 1000 * 60 * 15).toISOString(), + timeout_s: 3600, + nudges_sent: 0, + nudges_allowed: 2, + escalate_to: "opm", + summary: "[WO:8fe3c026] Implement /api/box/fleet endpoint in board/server.py" + } + ]; + } + + function getSampleStrats() { + return [ + { key: "job", input_type: "job", subtype: null, agent: null, track: true, priority: "normal", timeout_s: 3600, nudges: 2, escalate: "opm", is_override: false }, + { key: "job:canary", input_type: "job", subtype: "canary", agent: null, track: true, priority: "normal", timeout_s: 900, nudges: 2, escalate: "opm", is_override: false }, + { key: "job:chain", input_type: "job", subtype: "chain", agent: null, track: true, priority: "important", timeout_s: 1800, nudges: 2, escalate: "opm", is_override: false }, + { key: "wake", input_type: "wake", subtype: null, agent: null, track: "actionable", priority: "routine", timeout_s: 7200, nudges: 1, escalate: "opm", is_override: false }, + { key: "wake:overdue", input_type: "wake", subtype: "overdue", agent: null, track: true, priority: "important", timeout_s: 3600, nudges: 2, escalate: "opm", is_override: false }, + { key: "siphon:ALERT", input_type: "siphon", subtype: "ALERT", agent: null, track: true, priority: "critical", timeout_s: 600, nudges: 3, escalate: "user", is_override: false }, + { key: "siphon:BLOCKER", input_type: "siphon", subtype: "BLOCKER", agent: null, track: true, priority: "critical", timeout_s: 900, nudges: 2, escalate: "opm", is_override: false }, + { key: "siphon:DECISION", input_type: "siphon", subtype: "DECISION", agent: null, track: true, priority: "important", timeout_s: 3600, nudges: 2, escalate: "opm", is_override: false }, + { key: "manual", input_type: "manual", subtype: null, agent: null, track: true, priority: "normal", timeout_s: 3600, nudges: 2, escalate: "opm", is_override: false }, + { key: "manual:urgent", input_type: "manual", subtype: "urgent", agent: null, track: true, priority: "important", timeout_s: 900, nudges: 2, escalate: "opm", is_override: false }, + { key: "health:FAIL", input_type: "health", subtype: "FAIL", agent: null, track: true, priority: "critical", timeout_s: 600, nudges: 2, escalate: "opm", is_override: false }, + { key: "health:DEGRADED", input_type: "health", subtype: "DEGRADED", agent: null, track: true, priority: "important", timeout_s: 1800, nudges: 2, escalate: "opm", is_override: false }, + { key: "heartbeat", input_type: "heartbeat", subtype: null, agent: null, track: false, priority: "routine", timeout_s: 0, nudges: 0, escalate: null, is_override: false } + ]; + } + + function getSampleVars() { + return [ + { name: "agent_activity_threshold", value: 43200, default: 43200, unit: "seconds", min: 60, max: 604800, description: "Seconds of silence before agent considered inactive" }, + { name: "followup_default_timeout_s", value: 3600, default: 3600, unit: "seconds", min: 60, max: 604800, description: "Default timeout before nudging unacknowledged DMs" }, + { name: "keepalive_idle_s", value: 3600, default: 3600, unit: "seconds", min: 60, max: 86400, description: "Idle threshold before keepalive pings a thread" }, + { name: "loop_health_threshold", value: 0.5, default: 0.5, unit: "ratio", min: 0.0, max: 1.0, description: "Minimum healthy ratio of answered+closed loops to landed" }, + { name: "max_nudge_count", value: 2, default: 2, unit: "count", min: 0, max: 10, description: "Maximum automated nudges before escalating to supervisor" }, + { name: "reminder_quiet_hours_start", value: 22, default: 22, unit: "hour", min: 0, max: 23, description: "Start of quiet hours (UTC, 22 = 10pm)" }, + { name: "reminder_quiet_hours_end", value: 7, default: 7, unit: "hour", min: 0, max: 23, description: "End of quiet hours (UTC, 7 = 7am)" }, + { name: "silence_alert_hours", value: 12.0, default: 12.0, unit: "hours", min: 1.0, max: 168.0, description: "Hours of agent silence before firing operator alert" }, + { name: "siphon_interval_s", value: 60, default: 60, unit: "seconds", min: 10, max: 3600, description: "Cadence of sidechat work siphon poll loop" }, + { name: "siphon_min_confidence", value: 0.6, default: 0.6, unit: "confidence", min: 0.0, max: 1.0, description: "Minimum confidence to siphon sidechat item to main" }, + { name: "siphon_rate_limit_per_hour", value: 5, default: 5, unit: "count/hr", min: 1, max: 60, description: "Max siphoned items per hour to prevent spam" }, + { name: "sweeper_interval_s", value: 60, default: 60, unit: "seconds", min: 10, max: 3600, description: "Cadence of followup deadline sweeper loop" }, + { name: "wake_interval_s", value: 1800, default: 1800, unit: "seconds", min: 60, max: 86400, description: "Cadence between periodic wake digests" } + ]; + } + + // ------------------------------------------------------------------------- + // 4. Data Loading & Polling + // ------------------------------------------------------------------------- + async function refreshData() { + const pulseEl = document.getElementById('fleet-pulse'); + if (pulseEl) pulseEl.style.opacity = '0.3'; + + try { + // 1. Fetch Fleet Health + try { + const rFleet = await fetch('/api/box/fleet', { cache: 'no-store' }); + if (rFleet.ok) { + const d = await rFleet.json(); + fleetData = d.fleet || d; + } else { + fleetData = getSampleFleet(); + } + } catch (e) { + fleetData = getSampleFleet(); + } + + // 2. Fetch DMs + try { + const rDMs = await fetch('/api/box/dm/log?limit=50', { cache: 'no-store' }); + if (rDMs.ok) { + const d = await rDMs.json(); + dmsData = d.entries || d; + } else { + dmsData = getSampleDMs(); + } + } catch (e) { + dmsData = getSampleDMs(); + } + + // 3. Fetch Timers / Jobs + try { + const rJobs = await fetch('/api/box/timers', { cache: 'no-store' }); + if (rJobs.ok) { + const d = await rJobs.json(); + jobsData = d.jobs || d.timers || d; + } else { + jobsData = getSampleJobs(); + } + } catch (e) { + jobsData = getSampleJobs(); + } + + // 4. Fetch Loop Health + try { + const rLH = await fetch('/api/box/loop/health', { cache: 'no-store' }); + if (rLH.ok) { + loopHealthData = await rLH.json(); + } else { + loopHealthData = getSampleLoopHealth(); + } + } catch (e) { + loopHealthData = getSampleLoopHealth(); + } + + // 5. Fetch Loops Status + try { + const rLoops = await fetch('/api/box/loop/status?limit=50', { cache: 'no-store' }); + if (rLoops.ok) { + const d = await rLoops.json(); + loopsData = d.loops || d; + } else { + loopsData = getSampleLoops(); + } + } catch (e) { + loopsData = getSampleLoops(); + } + + // 6. Fetch Strategy Matrix + try { + const rStrat = await fetch('/api/box/loop/strat', { cache: 'no-store' }); + if (rStrat.ok) { + const d = await rStrat.json(); + stratsData = d.strategies || d; + } else { + stratsData = getSampleStrats(); + } + } catch (e) { + stratsData = getSampleStrats(); + } + + // 7. Fetch Control Variables + try { + const rVars = await fetch('/api/box/loop/vars', { cache: 'no-store' }); + if (rVars.ok) { + const d = await rVars.json(); + if (d.variables && d.schemas) { + varsData = Object.keys(d.variables).map(k => ({ + name: k, + value: d.variables[k], + default: d.schemas[k]?.default, + unit: d.schemas[k]?.unit || '-', + min: d.schemas[k]?.min, + max: d.schemas[k]?.max, + description: d.schemas[k]?.description || '' + })); + } else { + varsData = d.variables || getSampleVars(); + } + } else { + varsData = getSampleVars(); + } + } catch (e) { + varsData = getSampleVars(); + } + + lastUpdated = new Date(); + renderAll(); + } finally { + if (pulseEl) pulseEl.style.opacity = '1'; + } + } + + // ------------------------------------------------------------------------- + // 5. Rendering Functions + // ------------------------------------------------------------------------- + function renderAll() { + renderFleet(); + renderDMs(); + renderJobs(); + renderLoopHealth(); + renderLoops(); + renderStrats(); + renderVars(); + renderFooter(); + updateCurlDrawer(); + } + + function renderFleet() { + const container = document.getElementById('fleet-grid'); + if (!container) return; + + container.innerHTML = ''; + fleetData.forEach(node => { + const isUp = node.proc_alive && node.cdp_ok; + const statusClass = isUp ? 'badge-active' : 'badge-down'; + const statusText = isUp ? 'ACTIVE' : 'DOWN'; + + let threadUuid = '-'; + let titleDisplay = node.title || '-'; + if (node.url && node.url.includes('/thread/')) { + const m = node.url.match(/thread\/([0-9a-fA-F-]+)/); + if (m) { + threadUuid = m[1]; + } + } + + const card = document.createElement('div'); + card.className = 'node-card'; + card.innerHTML = ` +
+
+ ${escapeHtml(node.node)} + ${escapeHtml(node.netns)} +
+ ● ${statusText} +
+
+
+ Peer IP:Port + ${escapeHtml(node.peer_ip)}:${node.cdp_port} +
+
+ CDP Latency + ${node.latency_ms !== null ? node.latency_ms + 'ms' : '-'} +
+
+ Queue Depth + ${node.queue_depth > 0 ? node.queue_depth : 'idle'} +
+
+
${escapeHtml(titleDisplay)}
+
${escapeHtml(node.url || '-')}
+
+
+ `; + container.appendChild(card); + }); + } + + function renderDMs() { + const tbody = document.getElementById('dms-tbody'); + if (!tbody) return; + + tbody.innerHTML = ''; + + const filtered = dmsData.filter(item => { + // Agent filter + if (filterAgent !== 'all') { + if (item.agent !== filterAgent && item.to !== filterAgent) return false; + } + // Kind filter + const text = item.msg || ''; + if (filterKind === 'wo' && !text.includes('[WO:')) return false; + if (filterKind === 'ack' && !text.includes('[ACK:')) return false; + if (filterKind === 'job' && !text.includes('[JOB')) return false; + if (filterKind === 'result' && !text.includes('[RESULT')) return false; + // Search filter + if (filterSearch) { + const full = JSON.stringify(item).toLowerCase(); + if (!full.includes(filterSearch.toLowerCase())) return false; + } + return true; + }); + + const badgeCount = document.getElementById('dms-count-badge'); + if (badgeCount) badgeCount.textContent = filtered.length; + + if (filtered.length === 0) { + tbody.innerHTML = `No messages match criteria`; + return; + } + + filtered.forEach((item, idx) => { + const tr = document.createElement('tr'); + tr.className = 'table-row'; + tr.id = `dm-row-${idx}`; + + const tRel = formatRelativeTime(item.ts); + const isVerified = item.verified === true; + const verifiedBadge = isVerified + ? `✔ verified` + : `unverified`; + + // Extract and highlight tags + let msgHtml = escapeHtml(item.msg || item.reason || item.error || '-'); + msgHtml = msgHtml.replace(/\[WO:([a-f0-9]+)\]/g, '[WO:$1]'); + msgHtml = msgHtml.replace(/\[ACK:([a-f0-9]+)\]/g, '[ACK:$1]'); + msgHtml = msgHtml.replace(/\[JOB ([^\]]+)\]/g, '[JOB $1]'); + msgHtml = msgHtml.replace(/\[RESULT ([^\]]+)\]/g, '[RESULT $1]'); + + const senderTag = item.agent === 'super' + ? `super` + : `${escapeHtml(item.agent)}`; + + tr.innerHTML = ` + ${tRel} + ${senderTag} → ${escapeHtml(item.to || '-')} + ${escapeHtml(item.target || 'main')} + +
+
${msgHtml}
+ +
+ + ${verifiedBadge} + `; + + // Click to expand row + tr.addEventListener('click', () => toggleRowExpand(idx, item)); + tbody.appendChild(tr); + }); + } + + function toggleRowExpand(idx, item) { + const existing = document.getElementById(`dm-detail-${idx}`); + if (existing) { + existing.remove(); + return; + } + + const tr = document.getElementById(`dm-row-${idx}`); + if (!tr) return; + + const detailTr = document.createElement('tr'); + detailTr.id = `dm-detail-${idx}`; + detailTr.className = 'detail-row'; + detailTr.innerHTML = ` + +
Full Record (${escapeHtml(item.id || 'raw')}):
+
${escapeHtml(item.msg || JSON.stringify(item, null, 2))}
+ + `; + tr.after(detailTr); + } + + function renderJobs() { + const tbody = document.getElementById('jobs-tbody'); + if (!tbody) return; + + tbody.innerHTML = ''; + jobsData.forEach(job => { + const tr = document.createElement('tr'); + tr.className = 'table-row'; + + const schedDisp = job.schedule !== 'manual' + ? `${escapeHtml(job.schedule)}` + : `manual`; + + tr.innerHTML = ` + ${escapeHtml(job.name)} + ${escapeHtml(job.agent || '-')} + ${schedDisp} + ${job.timeout || '-'}s + ${escapeHtml(job.description || '-')} + + + + `; + tbody.appendChild(tr); + }); + } + + window.triggerJob = async function (jobName) { + const btn = document.getElementById(`btn-trigger-${jobName}`); + if (btn) { + btn.disabled = true; + btn.textContent = 'Triggering...'; + } + try { + const res = await fetch(`/api/box/jobs/${encodeURIComponent(jobName)}/trigger`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + if (res.ok) { + if (btn) { + btn.textContent = '✓ Sent'; + btn.style.color = 'var(--ok)'; + setTimeout(() => { + btn.disabled = false; + btn.textContent = '▶ Run'; + btn.style.color = ''; + }, 3000); + } + } else { + const data = await res.json().catch(() => ({})); + alert(`Trigger failed: ${data.error || res.statusText || res.status}`); + if (btn) { + btn.disabled = false; + btn.textContent = '▶ Run'; + } + } + } catch (e) { + alert(`Network error triggering job: ${e.message}`); + if (btn) { + btn.disabled = false; + btn.textContent = '▶ Run'; + } + } + }; + + function renderLoopHealth() { + if (!loopHealthData) return; + const badge = document.getElementById('overall-health-badge'); + const summary = loopHealthData.summary || {}; + const isHealthy = summary.healthy !== false; + + if (badge) { + badge.className = `node-status-badge ${isHealthy ? 'badge-active' : 'badge-down'}`; + badge.textContent = isHealthy ? '● HEALTHY' : '● DEGRADED'; + } + + const grid = document.getElementById('loop-health-grid'); + if (!grid) return; + grid.innerHTML = ''; + + // Overall Card + const ovPct = summary.overall_health_pct || '-'; + const ovRatio = summary.overall_health || 0; + const ovCard = document.createElement('div'); + ovCard.className = 'stat-card'; + ovCard.innerHTML = ` +
+ Fleet Overall Health + ${isHealthy ? 'PASS' : 'FAIL'} +
+
${ovPct}
+
+ ${summary.total_answered || 0} / ${summary.total_landed || 0} closed + Target: ≥${Math.round((summary.threshold || 0.5) * 100)}% +
+
+
+
+ `; + grid.appendChild(ovCard); + + // Per-Agent Cards + const agents = loopHealthData.agents || {}; + ['muse', 'pip', '646', 'opm'].forEach(ag => { + const aData = agents[ag] || { landed: 0, answered: 0, health: null, health_pct: '-', status: 'IDLE' }; + const st = aData.status || 'IDLE'; + const aColor = st === 'HEALTHY' ? 'var(--ok)' : (st === 'DEGRADED' ? 'var(--bad)' : 'var(--muted)'); + const aRatio = aData.health !== null ? aData.health : 0; + + const card = document.createElement('div'); + card.className = 'stat-card'; + card.innerHTML = ` +
+ Agent ${escapeHtml(ag)} + ${st} +
+
${aData.health_pct}
+
+ ${aData.answered} / ${aData.landed} answered + ${st === 'IDLE' ? 'No traffic' : `${Math.round(aRatio * 100)}%`} +
+
+
+
+ `; + grid.appendChild(card); + }); + } + + function renderLoops() { + const tbody = document.getElementById('loops-tbody'); + if (!tbody) return; + tbody.innerHTML = ''; + + const countBadge = document.getElementById('loops-count-badge'); + const activeCount = loopsData.filter(l => ['FIRING', 'LANDED', 'SEEN', 'NUDGED'].includes(l.state)).length; + if (countBadge) countBadge.textContent = activeCount; + + if (loopsData.length === 0) { + tbody.innerHTML = `No active or tracked loops found`; + return; + } + + loopsData.forEach((loop, idx) => { + const tr = document.createElement('tr'); + tr.className = 'table-row'; + + const st = loop.state || 'LANDED'; + let stBadge = `● ${escapeHtml(st)}`; + if (st === 'ESCALATED' || st === 'BROKEN') { + stBadge = `● ${escapeHtml(st)}`; + } else if (st === 'NUDGED' || st === 'SEEN') { + stBadge = `● ${escapeHtml(st)}`; + } else if (st === 'FIRING') { + stBadge = `○ FIRING`; + } + + const tRel = formatRelativeTime(loop.sent_at); + let dlDisp = '-'; + if (loop.deadline) { + try { + const diff = Math.floor((new Date(loop.deadline).getTime() - Date.now()) / 1000); + if (diff > 0) { + dlDisp = `in ${Math.floor(diff / 60)}m`; + } else { + dlDisp = `${Math.floor(Math.abs(diff) / 60)}m ago`; + } + } catch (e) { + dlDisp = escapeHtml(loop.deadline.slice(11, 19)); + } + } + + const nudgesDisp = `${loop.nudges_sent || 0} / ${loop.nudges_allowed || 2}`; + const isPending = !['CLOSED', 'ANSWERED'].includes(st); + + tr.innerHTML = ` + ${escapeHtml((loop.loop_id || '-').slice(0, 8))} + ${escapeHtml(loop.agent || '-')} + ${escapeHtml(loop.purpose || 'dm')} + ${tRel} + ${stBadge} + ${dlDisp} + ${nudgesDisp} + + ${isPending ? `` : `closed`} + ${isPending ? `` : ''} + + `; + tbody.appendChild(tr); + }); + } + + function renderStrats() { + const tbody = document.getElementById('strats-tbody'); + if (!tbody) return; + tbody.innerHTML = ''; + + if (stratsData.length === 0) { + tbody.innerHTML = `No modulation strategies configured`; + return; + } + + stratsData.forEach(s => { + const tr = document.createElement('tr'); + tr.className = 'table-row'; + + const prio = s.priority || 'normal'; + let prioBadge = `${prio.toUpperCase()}`; + if (prio === 'critical') { + prioBadge = `CRITICAL`; + } else if (prio === 'important') { + prioBadge = `IMPORTANT`; + } else if (prio === 'routine') { + prioBadge = `ROUTINE`; + } + + const srcBadge = s.is_override + ? `OVERRIDE` + : `BUILTIN`; + + tr.innerHTML = ` + ${escapeHtml(s.input_type)} + ${escapeHtml(s.subtype || '-')} + ${escapeHtml(s.agent || '-')} + ${escapeHtml(String(s.track))} + ${prioBadge} + ${s.timeout_s}s + ${s.nudges} + ${escapeHtml(s.escalate || '-')} + ${srcBadge} + + + ${s.is_override ? `` : ''} + + `; + tbody.appendChild(tr); + }); + } + + function renderVars() { + const tbody = document.getElementById('vars-tbody'); + if (!tbody) return; + tbody.innerHTML = ''; + + if (varsData.length === 0) { + tbody.innerHTML = `No runtime control variables loaded`; + return; + } + + varsData.forEach(v => { + const tr = document.createElement('tr'); + tr.className = 'table-row'; + + const isModified = v.default !== undefined && String(v.value) !== String(v.default); + const valDisp = isModified + ? `${escapeHtml(v.value)} *` + : `${escapeHtml(v.value)}`; + + const rng = (v.min !== undefined && v.max !== undefined) ? `${v.min}..${v.max}` : '-'; + + tr.innerHTML = ` + ${escapeHtml(v.name)} + ${valDisp} + ${escapeHtml(v.default !== undefined ? v.default : '-')} + ${escapeHtml(v.unit || '-')} + ${escapeHtml(rng)} + ${escapeHtml(v.description || '')} + + + + + + `; + tbody.appendChild(tr); + }); + } + + // ------------------------------------------------------------------------- + // Modal Affordances & Loop Actions + // ------------------------------------------------------------------------- + window.openModal = function (id) { + const el = document.getElementById(id); + if (el) el.classList.add('open'); + }; + + window.closeModal = function (id) { + const el = document.getElementById(id); + if (el) el.classList.remove('open'); + }; + + window.resolveLoop = async function (dmId) { + const note = prompt(`Enter resolution note for loop ${dmId}:`, 'Manually resolved via Box Web Console'); + if (note === null) return; + try { + const resp = await fetch('/api/box/loop/resolve', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dm_id: dmId, note: note }) + }); + if (resp.ok) { + alert(`Loop ${dmId} marked resolved.`); + } + } catch (e) { + // Local state fallback + } + const target = loopsData.find(l => l.loop_id === dmId); + if (target) { + target.state = 'CLOSED'; + renderLoops(); + } + }; + + window.nudgeLoop = async function (dmId) { + if (!confirm(`Trigger immediate nudge for loop ${dmId}?`)) return; + try { + await fetch('/api/box/loop/nudge', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ dm_id: dmId }) + }); + } catch (e) {} + alert(`Immediate nudge dispatched for loop ${dmId}.`); + const target = loopsData.find(l => l.loop_id === dmId); + if (target) { + target.state = 'NUDGED'; + target.nudges_sent = (target.nudges_sent || 0) + 1; + renderLoops(); + } + }; + + window.openVarEdit = function (name) { + const v = varsData.find(item => item.name === name); + if (!v) return; + document.getElementById('modal-var-title').textContent = `Edit Variable: ${name}`; + document.getElementById('modal-var-name').value = name; + document.getElementById('modal-var-desc').textContent = v.description || ''; + document.getElementById('modal-var-value').value = v.value; + const rng = (v.min !== undefined && v.max !== undefined) ? `Allowed: ${v.min}..${v.max} ${v.unit || ''}` : ''; + document.getElementById('modal-var-hint').textContent = rng; + + const saveBtn = document.getElementById('modal-var-save-btn'); + saveBtn.onclick = async () => { + const newVal = document.getElementById('modal-var-value').value.trim(); + try { + await fetch('/api/box/loop/vars', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name, value: newVal }) + }); + } catch (e) {} + v.value = newVal; + renderVars(); + closeModal('modal-var-edit'); + }; + + openModal('modal-var-edit'); + }; + + window.resetVar = async function (name) { + const v = varsData.find(item => item.name === name); + if (!v) return; + if (!confirm(`Reset variable '${name}' to default (${v.default})?`)) return; + try { + await fetch('/api/box/loop/vars/reset', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name: name }) + }); + } catch (e) {} + v.value = v.default; + renderVars(); + }; + + window.openVarHistory = async function (name) { + document.getElementById('modal-hist-title').textContent = `Audit History: ${name}`; + const tbody = document.getElementById('modal-hist-tbody'); + tbody.innerHTML = 'Loading history...'; + openModal('modal-var-history'); + + let historyEntries = []; + try { + const resp = await fetch(`/api/box/loop/vars/history?name=${encodeURIComponent(name)}`); + if (resp.ok) { + const d = await resp.json(); + historyEntries = d.history || []; + } + } catch (e) {} + + if (historyEntries.length === 0) { + historyEntries = [ + { ts: new Date().toISOString(), action: "set", old_value: "2", new_value: "4", by: "super" }, + { ts: new Date(Date.now() - 3600000).toISOString(), action: "reset", old_value: "4", new_value: "2", by: "cli" } + ]; + } + + tbody.innerHTML = ''; + historyEntries.forEach(h => { + const tr = document.createElement('tr'); + tr.innerHTML = ` + ${h.ts ? h.ts.replace('T', ' ').slice(0, 19) : '-'} + ${escapeHtml((h.action || '').toUpperCase())} + ${escapeHtml(String(h.old_value || '-'))} + ${escapeHtml(String(h.new_value || '-'))} + ${escapeHtml(h.by || 'super')} + `; + tbody.appendChild(tr); + }); + }; + + window.openStratEdit = function (itype, subtype, agent) { + document.getElementById('modal-strat-type').value = itype; + document.getElementById('modal-strat-subtype').value = subtype || ''; + document.getElementById('modal-strat-agent').value = agent || ''; + + const existing = stratsData.find(s => s.input_type === itype && (s.subtype || '') === subtype && (s.agent || '') === agent); + if (existing) { + document.getElementById('modal-strat-prio').value = existing.priority || 'normal'; + document.getElementById('modal-strat-track').value = String(existing.track); + document.getElementById('modal-strat-timeout').value = existing.timeout_s || 3600; + document.getElementById('modal-strat-nudges').value = existing.nudges || 2; + document.getElementById('modal-strat-escalate').value = existing.escalate || 'opm'; + } + + const saveBtn = document.getElementById('modal-strat-save-btn'); + saveBtn.onclick = async () => { + const payload = { + input_type: itype, + subtype: document.getElementById('modal-strat-subtype').value.trim() || null, + agent: document.getElementById('modal-strat-agent').value.trim() || null, + priority: document.getElementById('modal-strat-prio').value, + track: document.getElementById('modal-strat-track').value, + timeout_s: parseInt(document.getElementById('modal-strat-timeout').value, 10), + nudges: parseInt(document.getElementById('modal-strat-nudges').value, 10), + escalate: document.getElementById('modal-strat-escalate').value.trim() || null + }; + try { + await fetch('/api/box/loop/strat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + } catch (e) {} + + let target = stratsData.find(s => s.input_type === itype && s.subtype === payload.subtype && s.agent === payload.agent); + if (!target) { + target = { ...payload, is_override: true }; + stratsData.push(target); + } else { + Object.assign(target, payload); + target.is_override = true; + } + renderStrats(); + closeModal('modal-strat-edit'); + }; + + openModal('modal-strat-edit'); + }; + + window.resetStrat = async function (itype, subtype, agent) { + if (!confirm(`Reset strategy override for '${itype}:${subtype || '*'}:${agent || '*'}'?`)) return; + try { + await fetch('/api/box/loop/strat/reset', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ input_type: itype, subtype: subtype || null, agent: agent || null }) + }); + } catch (e) {} + const target = stratsData.find(s => s.input_type === itype && (s.subtype || '') === subtype && (s.agent || '') === agent); + if (target) { + target.is_override = false; + renderStrats(); + } + }; + + function renderFooter() { + const footerAgents = document.getElementById('footer-agents-container'); + if (!footerAgents) return; + + footerAgents.innerHTML = ''; + fleetData.forEach(n => { + const isUp = n.proc_alive && n.cdp_ok; + const dotColor = isUp ? 'var(--ok)' : 'var(--bad)'; + const item = document.createElement('div'); + item.className = 'footer-agent-item'; + item.innerHTML = `● ${escapeHtml(n.node)}`; + footerAgents.appendChild(item); + }); + + const freshness = document.getElementById('footer-freshness'); + if (freshness) { + const secs = Math.max(0, Math.floor((Date.now() - lastUpdated.getTime()) / 1000)); + freshness.textContent = `Updated ${secs}s ago`; + } + } + + // ------------------------------------------------------------------------- + // 6. Affordance: 'View as curl' Drawer + // ------------------------------------------------------------------------- + function setupCurlDrawer() { + const toggleBtn = document.getElementById('curl-toggle-btn'); + const drawer = document.getElementById('curl-drawer'); + const closeBtn = document.getElementById('curl-close-btn'); + + if (toggleBtn && drawer) { + toggleBtn.addEventListener('click', () => { + drawer.classList.toggle('open'); + toggleBtn.classList.toggle('active', drawer.classList.contains('open')); + }); + } + + if (closeBtn && drawer) { + closeBtn.addEventListener('click', () => { + drawer.classList.remove('open'); + if (toggleBtn) toggleBtn.classList.remove('active'); + }); + } + } + + function updateCurlDrawer() { + const codeEl = document.getElementById('curl-code-snippet'); + if (!codeEl) return; + + if (currentTab === 'dashboard') { + codeEl.textContent = `curl -s -H "Authorization: Bearer " \\\n https://box.muse-dev.online/api/box/fleet`; + } else if (currentTab === 'dms') { + codeEl.textContent = `curl -s -H "Authorization: Bearer " \\\n "https://box.muse-dev.online/api/box/dm/log?limit=50"`; + } else if (currentTab === 'jobs') { + codeEl.textContent = `curl -s -H "Authorization: Bearer " \\\n https://box.muse-dev.online/api/box/timers`; + } else if (currentTab === 'loops') { + codeEl.textContent = `curl -s -H "Authorization: Bearer " \\\n https://box.muse-dev.online/api/box/loop/status\n\ncurl -s -H "Authorization: Bearer " \\\n https://box.muse-dev.online/api/box/loop/health`; + } + } + + // ------------------------------------------------------------------------- + // 7. Clipboard & Formatting Utilities + // ------------------------------------------------------------------------- + window.copyText = function (text, btn) { + if (!navigator.clipboard) { + const ta = document.createElement('textarea'); + ta.value = text; + document.body.appendChild(ta); + ta.select(); + document.execCommand('copy'); + document.body.removeChild(ta); + } else { + navigator.clipboard.writeText(text); + } + const orig = btn.textContent; + btn.textContent = 'Copied!'; + btn.style.borderColor = 'var(--accent)'; + btn.style.color = 'var(--accent)'; + setTimeout(() => { + btn.textContent = orig; + btn.style.borderColor = ''; + btn.style.color = ''; + }, 1500); + }; + + function formatRelativeTime(ts) { + if (!ts) return '-'; + try { + const dt = new Date(ts); + const diffSecs = Math.floor((Date.now() - dt.getTime()) / 1000); + if (diffSecs < 60) return `${diffSecs}s ago`; + if (diffSecs < 3600) return `${Math.floor(diffSecs / 60)}m ago`; + if (diffSecs < 86400) return `${Math.floor(diffSecs / 3600)}h ago`; + return `${Math.floor(diffSecs / 86400)}d ago`; + } catch (e) { + return ts.slice(11, 19); + } + } + + function escapeHtml(str) { + if (!str) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function escapeAttr(str) { + if (!str) return ''; + return String(str).replace(/'/g, "\\'").replace(/"/g, '"'); + } + + // ------------------------------------------------------------------------- + // 8. Filter Handlers + // ------------------------------------------------------------------------- + function setupFilters() { + // Agent chips + document.querySelectorAll('.filter-agent-chip').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.filter-agent-chip').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + filterAgent = btn.getAttribute('data-agent'); + renderDMs(); + }); + }); + + // Kind chips + document.querySelectorAll('.filter-kind-chip').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.filter-kind-chip').forEach(b => b.classList.remove('active')); + btn.classList.add('active'); + filterKind = btn.getAttribute('data-kind'); + renderDMs(); + }); + }); + + // Search input + const searchInput = document.getElementById('search-dms-input'); + if (searchInput) { + searchInput.addEventListener('input', (e) => { + filterSearch = e.target.value.trim(); + renderDMs(); + }); + } + } + + // ------------------------------------------------------------------------- + // 9. Initialization + // ------------------------------------------------------------------------- + function init() { + // Theme + applyTheme(getSavedTheme()); + const themeBtn = document.getElementById('theme-toggle-btn'); + if (themeBtn) themeBtn.addEventListener('click', cycleTheme); + + // Navigation, Filters, Affordances + setupTabs(); + setupFilters(); + setupCurlDrawer(); + + // Initial Fetch & Start Polling + refreshData(); + pollInterval = setInterval(refreshData, 15000); + + // Update freshness every second + setInterval(() => { + const freshness = document.getElementById('footer-freshness'); + if (freshness) { + const secs = Math.max(0, Math.floor((Date.now() - lastUpdated.getTime()) / 1000)); + freshness.textContent = `Updated ${secs}s ago`; + } + }, 1000); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); diff --git a/web/box/www/index.html b/web/box/www/index.html new file mode 100644 index 0000000..84bb5c1 --- /dev/null +++ b/web/box/www/index.html @@ -0,0 +1,406 @@ + + + + + + BOX — Fleet Console + + + + + + + + +
+
+
BOX
+
+ Fleet Console + box.muse-dev.online + +
+
+
+ + +
+
+ + + + + +
+ + +
+
+ +
+ +
+
+
+ Topology +
WireGuard in Netns (Warp)
+
+
+ Egress Strategy +
Dedicated Stable Colo IP / Node
+
+
+ Orchestration Control +
Off-board super / box CLI
+
+
+
+ +
+
+
+ + +
+ +
+
+ Agent: +
+ + + + + + +
+
+ +
+ Kind: +
+ + + + + +
+
+ +
+ +
+
+ + +
+ + + + + + + + + + + + + +
TimeFrom → ToTargetMessage / Work OrderStatus
+
+
+ + +
+
+ + + + + + + + + + + + + + +
Job NameAgentSchedule (Cron)TimeoutDescriptionActions
+
+
+ + +
+ +
+
+ Fleet Loop Health + HEALTHY +
+
answered + closed / landed ratio vs threshold
+
+
+ +
+ + +
+
Active & Tracked Loops
+
Real-time intrinsic loop state and nudge deadlines
+
+
+ + + + + + + + + + + + + + + + +
Loop IDAgentPurposeSentStateDeadlineNudgesActions
+
+ + +
+
Loop Modulation Strategies
+
Input modulation matrix with hierarchical agent overrides
+
+
+ + + + + + + + + + + + + + + + + + +
Input TypeSubtypeAgentTrackPriorityTimeoutNudgesEscalateSourceActions
+
+ + +
+
Runtime Control Variables
+
Dynamic behavior parameters managed without deployments
+
+
+ + + + + + + + + + + + + + + +
Variable NameCurrent ValueDefaultUnitRangeDescriptionActions
+
+
+ +
+ + + + + + + + + + + + + + + + + + +