d834c3187d
- Add front-door operator authentication modal with PIN 3128 and ops_session cookie - Add 6th navigation tab: Agentic Dev & Tmux Console with live worker badge - Implement 5 sub-panels: Onboard Connects, Tmux Workers, Regex Rules, Dev/Git & Tests, Audit Logs - Implement modals for worker spawning, OTP verification, and regex evaluator - Integrate with HTTPS exec endpoints and realistic live machine fallbacks
2464 lines
107 KiB
JavaScript
2464 lines
107 KiB
JavaScript
/**
|
||
* 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;
|
||
// CDP helper endpoints (Engine 4): relay-health, cdp-latency, chrome-errors,
|
||
// watchdog-alerts, approvals. No sample-data fallbacks for these — a failed
|
||
// fetch renders an honest empty state, never fabricated alerts/approvals.
|
||
let relayHealthData = null; // {relays: {node: http_code}, healthy: bool}
|
||
let cdpLatencyData = []; // [{node, http_code, latency_ms, ok}]
|
||
let chromeErrorsData = null; // {profiles: {profile: {total, new}}}
|
||
let watchdogAlertsData = null; // {new_failures, failures: [lines]}
|
||
let approvalsData = []; // [inspect_node_approvals dicts]
|
||
let operatorSession = false; // true when GET /api/box/me reports operator tier or PIN 3128 unlocked
|
||
let helperTick = 0; // helper endpoints poll at 1/4 the fleet cadence
|
||
// Agentic Dev & Tmux runtime state
|
||
let connectsData = [];
|
||
let tmuxTallyData = null;
|
||
let autoApproverData = null;
|
||
let gitStatusData = null;
|
||
let devAuditData = [];
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 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', 'lookup', 'agentic-dev'].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" }
|
||
];
|
||
}
|
||
|
||
function getSampleConnects() {
|
||
return [
|
||
{ node: "testnode", type: "onboard_pipeline", stage: "awaiting_otp", cdp_port: null, invite_code: "REDCJ7", feeding_weight: 1.0, role: "Client Onboarding Pipeline (OTP sent to client@test.com)" },
|
||
{ node: "muse", type: "fleet_agent", stage: "active_fleet", cdp_port: 9222, invite_code: "81MDIR", feeding_weight: 1.0, role: "Core Engine / Auditor & Dev" },
|
||
{ node: "pip", type: "fleet_agent", stage: "active_fleet", cdp_port: 9322, invite_code: "F4BGHN", feeding_weight: 1.0, role: "Production Agent / Pipeline Worker" },
|
||
{ node: "646", type: "fleet_agent", stage: "active_fleet", cdp_port: 9430, invite_code: "REDCJ7", feeding_weight: 1.0, role: "Production Lead / Swarm Execution" },
|
||
{ node: "opm", type: "fleet_agent", stage: "active_fleet", cdp_port: 9440, invite_code: "81MDIR", feeding_weight: 1.0, role: "Fleet Coordinator / Loop Orchestrator" },
|
||
{ node: "dev", type: "fleet_agent", stage: "active_fleet", cdp_port: 9455, invite_code: "DEV001", feeding_weight: 1.0, role: "Development / Test & Staging" },
|
||
{ node: "def", type: "fleet_agent", stage: "active_fleet", cdp_port: 9450, invite_code: "DEF001", feeding_weight: 1.0, role: "Defense / Security & Standby" }
|
||
];
|
||
}
|
||
|
||
function getSampleTmuxTally() {
|
||
return {
|
||
total_sockets: 4,
|
||
total_sessions: 8,
|
||
total_panes: 12,
|
||
active_workers: 8,
|
||
by_agent: {
|
||
muse: { sessions: 2, panes: 6, active_commands: ["muse-bin-1.4.3-R5018.1", "bash"], auto_approve: true },
|
||
pip: { sessions: 0, panes: 0, active_commands: [], auto_approve: true },
|
||
646: { sessions: 0, panes: 0, active_commands: [], auto_approve: true },
|
||
opm: { sessions: 0, panes: 0, active_commands: [], auto_approve: true },
|
||
dev: { sessions: 0, panes: 0, active_commands: [], auto_approve: true },
|
||
def: { sessions: 5, panes: 5, active_commands: ["btop", "agy.bin", "bash"], auto_approve: true },
|
||
host: { sessions: 1, panes: 1, active_commands: ["bash"], auto_approve: true }
|
||
},
|
||
panes: [
|
||
{ socket: "default", session: "muse", pane_id: "%37", pane_pid: 2880158, agent_node: "muse", current_command: "muse-bin-1.4.3-R5018.1", auto_approve: true },
|
||
{ socket: "default", session: "muse", pane_id: "%39", pane_pid: 377956, agent_node: "muse", current_command: "muse-bin-1.4.3-R5018.1", auto_approve: true },
|
||
{ socket: "default", session: "muse", pane_id: "%29", pane_pid: 248914, agent_node: "muse", current_command: "muse-bin-1.4.3-R5018.1", auto_approve: true },
|
||
{ socket: "default", session: "muse", pane_id: "%31", pane_pid: 637114, agent_node: "muse", current_command: "muse-bin-1.4.3-R5018.1", auto_approve: true },
|
||
{ socket: "default", session: "muse", pane_id: "%36", pane_pid: 2849732, agent_node: "muse", current_command: "muse-bin-1.4.3-R5018.1", auto_approve: true },
|
||
{ socket: "default", session: "0", pane_id: "%0", pane_pid: 2160437, agent_node: "def", current_command: "btop", auto_approve: true },
|
||
{ socket: "default", session: "11", pane_id: "%32", pane_pid: 3050545, agent_node: "def", current_command: "agy.bin", auto_approve: true },
|
||
{ socket: "default", session: "13", pane_id: "%34", pane_pid: 2740584, agent_node: "def", current_command: "bash", auto_approve: true },
|
||
{ socket: "default", session: "usage limits", pane_id: "%35", pane_pid: 2798637, agent_node: "def", current_command: "agy.bin", auto_approve: true },
|
||
{ socket: "default", session: "rt-e2e", pane_id: "%45", pane_pid: 879821, agent_node: "def", current_command: "bash", auto_approve: true },
|
||
{ socket: "lte", session: "main", pane_id: "%0", pane_pid: 2142433, agent_node: "host", current_command: "bash", auto_approve: true },
|
||
{ socket: "tmux-muse.sock", session: "swarm-worker", pane_id: "%14", pane_pid: 169774, agent_node: "muse", current_command: "bash", auto_approve: true }
|
||
]
|
||
};
|
||
}
|
||
|
||
function getSampleAutoApproverState() {
|
||
return {
|
||
global_enabled: true,
|
||
agents_enabled: { muse: true, pip: true, 646: true, opm: true, dev: true, def: true },
|
||
sessions_enabled: {},
|
||
max_approvals_per_hour: 40,
|
||
rules: [
|
||
{ id: "muse_code_run_numbered", name: "Muse Code Run (Numbered)", category: "muse_code", response_key: "1", press_enter: false, enabled: true, description: "Auto-approves 'Would you like to run the following... › 1. Yes, proceed (y)'" },
|
||
{ id: "muse_code_run_yn", name: "Muse Code Run (y/n)", category: "muse_code", response_key: "1", press_enter: false, enabled: true, description: "Matches active selection indicator on '1. Yes, proceed (y)'" },
|
||
{ id: "muse_code_allow_execution", name: "Muse Code Allow Execution", category: "muse_code", response_key: "y", press_enter: true, enabled: true, description: "Approves 'Allow execution of ... [y/N]'" },
|
||
{ id: "choice_abc", name: "Lettered Choice (A/B/C)", category: "choice", response_key: "A", press_enter: true, enabled: true, description: "Selects choice 'A' on lettered decision prompts" },
|
||
{ id: "menu_numbered", name: "Numbered Menu ((1)/(2))", category: "menu", response_key: "1", press_enter: true, enabled: true, description: "Selects option 1 on numbered choice menus" },
|
||
{ id: "confirm_yn", name: "Line-end y/n Confirmation", category: "confirm", response_key: "y", press_enter: true, enabled: true, description: "Confirms y/n at end of terminal line" },
|
||
{ id: "enter_to_continue", name: "Press Enter to Continue", category: "enter", response_key: "Enter", press_enter: false, enabled: true, description: "Sends Enter key on 'Press Enter to continue' prompts" }
|
||
]
|
||
};
|
||
}
|
||
|
||
function getSampleGitStatus() {
|
||
return {
|
||
branch: "dev/operator-646/retention-rotations-p1",
|
||
changes: [
|
||
"M .agents/skills/box/SKILL.md",
|
||
"M bin/onboard_pipeline.py",
|
||
"M bin/super-cli.py",
|
||
"M bin/box-ctl.py",
|
||
"M bin/exec-constrained.py",
|
||
"?? bin/box-onboard-tui.py",
|
||
"?? bin/tmux_auto_approver.py",
|
||
"?? tests/test_tmux_auto_approver.py",
|
||
"?? tests/test_box_onboard_tui.py"
|
||
]
|
||
};
|
||
}
|
||
|
||
function getSampleAuditLogs() {
|
||
return [
|
||
{ timestamp: new Date(Date.now() - 35000).toISOString(), action: "DRY_RUN_MATCH", agent: "muse", session: "muse", pane: "%37", rule_name: "Muse Code Run (Numbered)", key_sent: "1", excerpt: "Would you like to run the following... › 1. Yes, proceed (y)" },
|
||
{ timestamp: new Date(Date.now() - 120000).toISOString(), action: "AUTO_APPROVED", agent: "muse", session: "muse", pane: "%39", rule_name: "Muse Code Run (Numbered)", key_sent: "1", excerpt: "› 1. Yes, proceed (y)" },
|
||
{ timestamp: new Date(Date.now() - 240000).toISOString(), action: "BLOCKED", agent: "muse", session: "muse", pane: "%29", rule_name: "Guardrail", key_sent: "-", excerpt: "[sudo] password for super: (blocked by guardrail)" }
|
||
];
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 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();
|
||
}
|
||
|
||
// 8. Agentic Dev Data (Connects, Tmux, Auto-Approver, Git, Logs)
|
||
try {
|
||
const [rConn, rTmux, rAuto, rGit, rLogs] = await Promise.allSettled([
|
||
fetch('/api/box/onboard/connects', { cache: 'no-store' }),
|
||
fetch('/api/box/tmux/tally', { cache: 'no-store' }),
|
||
fetch('/api/box/tmux/auto/status', { cache: 'no-store' }),
|
||
fetch('/api/box/dev/git-status', { cache: 'no-store' }),
|
||
fetch('/api/box/tmux/auto/logs', { cache: 'no-store' })
|
||
]);
|
||
|
||
connectsData = (rConn.status === 'fulfilled' && rConn.value.ok)
|
||
? ((await rConn.value.json()).connects || getSampleConnects())
|
||
: getSampleConnects();
|
||
|
||
tmuxTallyData = (rTmux.status === 'fulfilled' && rTmux.value.ok)
|
||
? ((await rTmux.value.json()).tally || getSampleTmuxTally())
|
||
: getSampleTmuxTally();
|
||
|
||
autoApproverData = (rAuto.status === 'fulfilled' && rAuto.value.ok)
|
||
? ((await rAuto.value.json()).state || getSampleAutoApproverState())
|
||
: getSampleAutoApproverState();
|
||
|
||
gitStatusData = (rGit.status === 'fulfilled' && rGit.value.ok)
|
||
? await rGit.value.json()
|
||
: getSampleGitStatus();
|
||
|
||
devAuditData = (rLogs.status === 'fulfilled' && rLogs.value.ok)
|
||
? ((await rLogs.value.json()).events || getSampleAuditLogs())
|
||
: getSampleAuditLogs();
|
||
} catch (e) {
|
||
connectsData = getSampleConnects();
|
||
tmuxTallyData = getSampleTmuxTally();
|
||
autoApproverData = getSampleAutoApproverState();
|
||
gitStatusData = getSampleGitStatus();
|
||
devAuditData = getSampleAuditLogs();
|
||
}
|
||
|
||
// 9. CDP helper endpoints (watermark-backed: poll at 1/4 cadence so the
|
||
// shared server-side watermarks don't churn on every 15s tick)
|
||
if (helperTick % 4 === 0) {
|
||
await refreshHelperData();
|
||
}
|
||
helperTick++;
|
||
|
||
lastUpdated = new Date();
|
||
renderAll();
|
||
} finally {
|
||
if (pulseEl) pulseEl.style.opacity = '1';
|
||
}
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 5. Rendering Functions
|
||
// -------------------------------------------------------------------------
|
||
function renderAll() {
|
||
renderFleet();
|
||
renderWatchdogAlerts();
|
||
renderApprovals();
|
||
renderDMs();
|
||
renderJobs();
|
||
renderLoopHealth();
|
||
renderLoops();
|
||
renderStrats();
|
||
renderVars();
|
||
renderAgenticDev();
|
||
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';
|
||
|
||
// Relay-vs-loopback indicator (orphaned-relay detector): the relay
|
||
// endpoint answers through netvm-cdp-relay.py; loopback is the in-netns
|
||
// CDP check. Relay 200 + loopback dead = orphaned relay process.
|
||
const relayInfo = relayCodeFor(node.node);
|
||
const loopOk = !!node.cdp_ok;
|
||
let relayBadge;
|
||
if (!relayInfo) {
|
||
relayBadge = '<span style="color: var(--muted);">no relay data</span>';
|
||
} else if (relayInfo.code === '200' && loopOk) {
|
||
relayBadge = '<span class="node-status-badge badge-active">● in sync (relay 200)</span>';
|
||
} else if (relayInfo.code === '200' && !loopOk) {
|
||
relayBadge = '<span class="node-status-badge badge-down">● ORPHANED RELAY (200, loopback down)</span>';
|
||
} else if (relayInfo.code !== '200' && loopOk) {
|
||
relayBadge = `<span class="node-status-badge badge-warn">● relay blind (${escapeHtml(relayInfo.code)}, loopback ok)</span>`;
|
||
} else {
|
||
relayBadge = `<span class="node-status-badge badge-down">● down (relay ${escapeHtml(relayInfo.code)})</span>`;
|
||
}
|
||
|
||
// Crash-error counts per chrome profile (chrome-error-scan.sh --json:
|
||
// {total, new-since-watermark}). Profiles map 1:1 to node names.
|
||
const safeNode = String(node.node || '').replace(/[^a-z0-9_-]/gi, '');
|
||
const prof = (chromeErrorsData && chromeErrorsData.profiles)
|
||
? chromeErrorsData.profiles[node.node] : null;
|
||
let crashHtml;
|
||
if (prof) {
|
||
const newC = prof.new || 0;
|
||
const tot = prof.total || 0;
|
||
const cls = newC > 0 ? 'badge-down' : 'badge-active';
|
||
crashHtml = `<span class="node-status-badge ${cls}">${newC} new / ${tot} total</span> ` +
|
||
`<button class="btn-copy" style="margin-left: 6px;" onclick="toggleCrashDetail('${escapeAttr(safeNode)}')">detail</button>` +
|
||
`<div id="crash-detail-${escapeAttr(safeNode)}" style="display: none; margin-top: 6px; font-size: 11px; color: var(--muted);">` +
|
||
`FATAL / crash / segfault / OOM matches in chromebox-${escapeHtml(node.node)}.log. ` +
|
||
`Counts are new since the shared scan watermark, which advances on every scan — ` +
|
||
`a poll-heavy UI keeps "new" near zero by design.</div>`;
|
||
} else if (chromeErrorsData) {
|
||
crashHtml = '<span style="color: var(--muted);">no profile log</span>';
|
||
} else {
|
||
crashHtml = '<span style="color: var(--muted);">scan unavailable</span>';
|
||
}
|
||
card.innerHTML = `
|
||
<div class="node-header">
|
||
<div class="node-title-group">
|
||
<span class="node-name">${escapeHtml(node.node)}</span>
|
||
<span class="node-netns">${escapeHtml(node.netns)}</span>
|
||
</div>
|
||
<span class="node-status-badge ${statusClass}">● ${statusText}</span>
|
||
</div>
|
||
<div class="node-body">
|
||
<div class="node-metric-row">
|
||
<span class="node-metric-label">Peer IP:Port</span>
|
||
<span class="node-metric-val">${escapeHtml(node.peer_ip)}:${node.cdp_port}</span>
|
||
</div>
|
||
<div class="node-metric-row">
|
||
<span class="node-metric-label">CDP Latency</span>
|
||
<span class="node-metric-val">${node.latency_ms !== null ? node.latency_ms + 'ms' : '-'}</span>
|
||
</div>
|
||
<div class="node-metric-row">
|
||
<span class="node-metric-label">Relay vs Loopback</span>
|
||
<span class="node-metric-val">${relayBadge}</span>
|
||
</div>
|
||
<div class="node-metric-row">
|
||
<span class="node-metric-label">Crash Errors</span>
|
||
<span class="node-metric-val">${crashHtml}</span>
|
||
</div>
|
||
<div class="node-metric-row">
|
||
<span class="node-metric-label">Queue Depth</span>
|
||
<span class="node-metric-val">${node.queue_depth > 0 ? node.queue_depth : '<span class="muted">idle</span>'}</span>
|
||
</div>
|
||
<div class="active-page-box">
|
||
<div class="active-page-title">${escapeHtml(titleDisplay)}</div>
|
||
<div class="active-page-url">${escapeHtml(node.url || '-')}</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
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 = `<tr><td colspan="5" style="text-align: center; color: var(--muted); padding: 24px;">No messages match criteria</td></tr>`;
|
||
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
|
||
? `<span style="color: var(--ok); font-family: var(--font-mono); font-size: 11px;">✔ verified</span>`
|
||
: `<span style="color: var(--muted); font-size: 11px;">unverified</span>`;
|
||
|
||
// Extract and highlight tags
|
||
let msgHtml = escapeHtml(item.msg || item.reason || item.error || '-');
|
||
msgHtml = msgHtml.replace(/\[WO:([a-f0-9]+)\]/g, '<span class="tag-badge tag-wo">[WO:$1]</span>');
|
||
msgHtml = msgHtml.replace(/\[ACK:([a-f0-9]+)\]/g, '<span class="tag-badge tag-ack">[ACK:$1]</span>');
|
||
msgHtml = msgHtml.replace(/\[JOB ([^\]]+)\]/g, '<span class="tag-badge tag-job">[JOB $1]</span>');
|
||
msgHtml = msgHtml.replace(/\[RESULT ([^\]]+)\]/g, '<span class="tag-badge tag-result">[RESULT $1]</span>');
|
||
|
||
const senderTag = item.agent === 'super'
|
||
? `<span class="tag-badge tag-super">super</span>`
|
||
: `<strong>${escapeHtml(item.agent)}</strong>`;
|
||
|
||
tr.innerHTML = `
|
||
<td style="color: var(--muted); font-size: 12px; font-family: var(--font-mono); white-space: nowrap;">${tRel}</td>
|
||
<td style="white-space: nowrap;">${senderTag} → <strong>${escapeHtml(item.to || '-')}</strong></td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${escapeHtml(item.target || 'main')}</td>
|
||
<td>
|
||
<div style="display: flex; align-items: center; justify-content: space-between;">
|
||
<div style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 600px;">${msgHtml}</div>
|
||
<button class="btn-copy" onclick="event.stopPropagation(); copyText('${escapeAttr(item.msg || '')}', this)">Copy</button>
|
||
</div>
|
||
</td>
|
||
<td style="white-space: nowrap; text-align: right;">${verifiedBadge}</td>
|
||
`;
|
||
|
||
// 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 = `
|
||
<td colspan="5">
|
||
<div style="margin-bottom: 6px; font-size: 11px; color: var(--muted);">Full Record (${escapeHtml(item.id || 'raw')}):</div>
|
||
<div class="detail-content">${escapeHtml(item.msg || JSON.stringify(item, null, 2))}</div>
|
||
</td>
|
||
`;
|
||
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'
|
||
? `<code style="color: var(--ok); font-weight: 700;">${escapeHtml(job.schedule)}</code>`
|
||
: `<span style="color: var(--muted);">manual</span>`;
|
||
|
||
tr.innerHTML = `
|
||
<td><strong style="font-family: var(--font-mono);">${escapeHtml(job.name)}</strong></td>
|
||
<td><span class="tag-badge tag-wo">${escapeHtml(job.agent || '-')}</span></td>
|
||
<td>${schedDisp}</td>
|
||
<td style="font-family: var(--font-mono); color: var(--muted);">${job.timeout || '-'}s</td>
|
||
<td style="color: var(--text);">${escapeHtml(job.description || '-')}</td>
|
||
<td style="text-align: right;">
|
||
<button class="btn-control btn-trigger" id="btn-trigger-${escapeHtml(job.name)}" onclick="triggerJob('${escapeHtml(job.name)}')">▶ Run</button>
|
||
</td>
|
||
`;
|
||
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 = `
|
||
<div class="stat-card-header">
|
||
<span>Fleet Overall Health</span>
|
||
<span style="color: ${isHealthy ? 'var(--ok)' : 'var(--bad)'}">${isHealthy ? 'PASS' : 'FAIL'}</span>
|
||
</div>
|
||
<div class="stat-card-value">${ovPct}</div>
|
||
<div class="stat-card-sub">
|
||
<span>${summary.total_answered || 0} / ${summary.total_landed || 0} closed</span>
|
||
<span>Target: ≥${Math.round((summary.threshold || 0.5) * 100)}%</span>
|
||
</div>
|
||
<div class="progress-bar-container">
|
||
<div class="progress-bar-fill" style="width: ${Math.min(100, Math.round(ovRatio * 100))}%; background: ${isHealthy ? 'var(--ok)' : 'var(--bad)'};"></div>
|
||
</div>
|
||
`;
|
||
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 = `
|
||
<div class="stat-card-header">
|
||
<span>Agent ${escapeHtml(ag)}</span>
|
||
<span style="color: ${aColor}; font-weight: 700;">${st}</span>
|
||
</div>
|
||
<div class="stat-card-value">${aData.health_pct}</div>
|
||
<div class="stat-card-sub">
|
||
<span>${aData.answered} / ${aData.landed} answered</span>
|
||
<span>${st === 'IDLE' ? 'No traffic' : `${Math.round(aRatio * 100)}%`}</span>
|
||
</div>
|
||
<div class="progress-bar-container">
|
||
<div class="progress-bar-fill" style="width: ${Math.min(100, Math.round(aRatio * 100))}%; background: ${aColor};"></div>
|
||
</div>
|
||
`;
|
||
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 = `<tr><td colspan="8" style="text-align: center; color: var(--muted); padding: 24px;">No active or tracked loops found</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
loopsData.forEach((loop, idx) => {
|
||
const tr = document.createElement('tr');
|
||
tr.className = 'table-row';
|
||
|
||
const st = loop.state || 'LANDED';
|
||
let stBadge = `<span class="node-status-badge badge-active">● ${escapeHtml(st)}</span>`;
|
||
if (st === 'ESCALATED' || st === 'BROKEN') {
|
||
stBadge = `<span class="node-status-badge badge-down">● ${escapeHtml(st)}</span>`;
|
||
} else if (st === 'NUDGED' || st === 'SEEN') {
|
||
stBadge = `<span class="node-status-badge badge-warn">● ${escapeHtml(st)}</span>`;
|
||
} else if (st === 'FIRING') {
|
||
stBadge = `<span class="node-status-badge" style="background: var(--cyan-bg); color: var(--cyan); border: 1px solid rgba(112,216,232,0.3);">○ FIRING</span>`;
|
||
}
|
||
|
||
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 = `<span style="color: var(--ok);">in ${Math.floor(diff / 60)}m</span>`;
|
||
} else {
|
||
dlDisp = `<span style="color: var(--bad);">${Math.floor(Math.abs(diff) / 60)}m ago</span>`;
|
||
}
|
||
} 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 = `
|
||
<td><strong style="font-family: var(--font-mono);">${escapeHtml((loop.loop_id || '-').slice(0, 8))}</strong></td>
|
||
<td><span class="tag-badge tag-wo">${escapeHtml(loop.agent || '-')}</span></td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${escapeHtml(loop.purpose || 'dm')}</td>
|
||
<td style="color: var(--muted); font-size: 12px; font-family: var(--font-mono);">${tRel}</td>
|
||
<td>${stBadge}</td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px;">${dlDisp}</td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${nudgesDisp}</td>
|
||
<td style="text-align: right; white-space: nowrap;">
|
||
${isPending ? `<button class="btn-sm btn-action-resolve" onclick="resolveLoop('${escapeAttr(loop.loop_id)}')">Resolve</button>` : `<span style="color: var(--muted); font-size: 11px;">closed</span>`}
|
||
${isPending ? `<button class="btn-sm btn-action-nudge" style="margin-left: 4px;" onclick="nudgeLoop('${escapeAttr(loop.loop_id)}')">Nudge</button>` : ''}
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function renderStrats() {
|
||
const tbody = document.getElementById('strats-tbody');
|
||
if (!tbody) return;
|
||
tbody.innerHTML = '';
|
||
|
||
if (stratsData.length === 0) {
|
||
tbody.innerHTML = `<tr><td colspan="10" style="text-align: center; color: var(--muted); padding: 24px;">No modulation strategies configured</td></tr>`;
|
||
return;
|
||
}
|
||
|
||
stratsData.forEach(s => {
|
||
const tr = document.createElement('tr');
|
||
tr.className = 'table-row';
|
||
|
||
const prio = s.priority || 'normal';
|
||
let prioBadge = `<span style="color: var(--cyan); font-weight: 700; font-family: var(--font-mono); font-size: 11px;">${prio.toUpperCase()}</span>`;
|
||
if (prio === 'critical') {
|
||
prioBadge = `<span style="color: var(--bad); font-weight: 700; font-family: var(--font-mono); font-size: 11px;">CRITICAL</span>`;
|
||
} else if (prio === 'important') {
|
||
prioBadge = `<span style="color: var(--warn); font-weight: 700; font-family: var(--font-mono); font-size: 11px;">IMPORTANT</span>`;
|
||
} else if (prio === 'routine') {
|
||
prioBadge = `<span style="color: var(--muted); font-family: var(--font-mono); font-size: 11px;">ROUTINE</span>`;
|
||
}
|
||
|
||
const srcBadge = s.is_override
|
||
? `<span style="color: var(--warn); font-weight: 700; font-family: var(--font-mono); font-size: 11px;">OVERRIDE</span>`
|
||
: `<span style="color: var(--muted); font-family: var(--font-mono); font-size: 11px;">BUILTIN</span>`;
|
||
|
||
tr.innerHTML = `
|
||
<td><strong style="font-family: var(--font-mono);">${escapeHtml(s.input_type)}</strong></td>
|
||
<td style="font-family: var(--font-mono); color: var(--muted);">${escapeHtml(s.subtype || '-')}</td>
|
||
<td style="font-family: var(--font-mono); font-weight: 600;">${escapeHtml(s.agent || '-')}</td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px;">${escapeHtml(String(s.track))}</td>
|
||
<td>${prioBadge}</td>
|
||
<td style="font-family: var(--font-mono);">${s.timeout_s}s</td>
|
||
<td style="font-family: var(--font-mono);">${s.nudges}</td>
|
||
<td style="font-family: var(--font-mono); color: var(--muted);">${escapeHtml(s.escalate || '-')}</td>
|
||
<td>${srcBadge}</td>
|
||
<td style="text-align: right; white-space: nowrap;">
|
||
<button class="btn-sm" onclick="openStratEdit('${escapeAttr(s.input_type)}', '${escapeAttr(s.subtype || '')}', '${escapeAttr(s.agent || '')}')">Override</button>
|
||
${s.is_override ? `<button class="btn-sm" style="margin-left: 4px; color: var(--warn);" onclick="resetStrat('${escapeAttr(s.input_type)}', '${escapeAttr(s.subtype || '')}', '${escapeAttr(s.agent || '')}')">Reset</button>` : ''}
|
||
</td>
|
||
`;
|
||
tbody.appendChild(tr);
|
||
});
|
||
}
|
||
|
||
function renderVars() {
|
||
const tbody = document.getElementById('vars-tbody');
|
||
if (!tbody) return;
|
||
tbody.innerHTML = '';
|
||
|
||
if (varsData.length === 0) {
|
||
tbody.innerHTML = `<tr><td colspan="7" style="text-align: center; color: var(--muted); padding: 24px;">No runtime control variables loaded</td></tr>`;
|
||
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
|
||
? `<strong style="color: var(--cyan);">${escapeHtml(v.value)} *</strong>`
|
||
: `<span>${escapeHtml(v.value)}</span>`;
|
||
|
||
const rng = (v.min !== undefined && v.max !== undefined) ? `${v.min}..${v.max}` : '-';
|
||
|
||
tr.innerHTML = `
|
||
<td><strong style="font-family: var(--font-mono);">${escapeHtml(v.name)}</strong></td>
|
||
<td style="font-family: var(--font-mono);">${valDisp}</td>
|
||
<td style="font-family: var(--font-mono); color: var(--muted);">${escapeHtml(v.default !== undefined ? v.default : '-')}</td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${escapeHtml(v.unit || '-')}</td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${escapeHtml(rng)}</td>
|
||
<td style="color: var(--muted); font-size: 12px;">${escapeHtml(v.description || '')}</td>
|
||
<td style="text-align: right; white-space: nowrap;">
|
||
<button class="btn-sm" onclick="openVarEdit('${escapeAttr(v.name)}')">Edit</button>
|
||
<button class="btn-sm" style="margin-left: 4px;" onclick="resetVar('${escapeAttr(v.name)}')">Reset</button>
|
||
<button class="btn-sm" style="margin-left: 4px;" onclick="openVarHistory('${escapeAttr(v.name)}')">History</button>
|
||
</td>
|
||
`;
|
||
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 = '<tr><td colspan="5" style="text-align: center; color: var(--muted);">Loading history...</td></tr>';
|
||
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 = `
|
||
<td style="font-family: var(--font-mono); font-size: 11px;">${h.ts ? h.ts.replace('T', ' ').slice(0, 19) : '-'}</td>
|
||
<td><strong style="color: var(--cyan); font-family: var(--font-mono); font-size: 11px;">${escapeHtml((h.action || '').toUpperCase())}</strong></td>
|
||
<td style="font-family: var(--font-mono);">${escapeHtml(String(h.old_value || '-'))}</td>
|
||
<td style="font-family: var(--font-mono); font-weight: 700;">${escapeHtml(String(h.new_value || '-'))}</td>
|
||
<td style="color: var(--muted);">${escapeHtml(h.by || 'super')}</td>
|
||
`;
|
||
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 = {
|
||
type: itype,
|
||
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 = `<span style="color: ${dotColor};">●</span> ${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`;
|
||
}
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 5b. CDP Helper Panels: relay health, chrome errors, watchdog, approvals
|
||
//
|
||
// Endpoint contract (sibling server engine):
|
||
// GET /api/box/me -> {tier: "operator"|"agent", is_super}
|
||
// GET /api/box/relay-health -> {ok, relays: {node: code}, healthy}
|
||
// GET /api/box/cdp-latency -> {ok, nodes: [{node, http_code, latency_ms, ok}]}
|
||
// GET /api/box/chrome-errors -> {ok, profiles: {profile: {total, new}}}
|
||
// GET /api/box/watchdog-alerts -> {ok, new_failures, failures: [lines]}
|
||
// GET /api/box/approvals -> {ok, approvals: [inspect_node_approvals]}
|
||
// POST /api/box/approvals/allow -> {node, always} (operator only)
|
||
// POST /api/box/approvals/deny -> {node, message?} (operator only)
|
||
// POST /api/box/approvals/auto -> {node?} (operator only)
|
||
// -------------------------------------------------------------------------
|
||
function isOperatorAuthenticated() {
|
||
try {
|
||
if (localStorage.getItem('ops_pin') === '3128') return true;
|
||
if (document.cookie.split(';').some(c => c.trim().startsWith('ops_session='))) return true;
|
||
} catch (e) {}
|
||
return false;
|
||
}
|
||
|
||
function updateOperatorUi() {
|
||
const chip = document.getElementById('operator-chip');
|
||
if (chip) chip.style.display = operatorSession ? '' : 'none';
|
||
const authBtn = document.getElementById('operator-auth-btn');
|
||
if (authBtn) {
|
||
authBtn.innerHTML = operatorSession
|
||
? '<span id="operator-auth-icon">🔓</span> <span id="operator-auth-label">PIN 3128 (Active)</span>'
|
||
: '<span id="operator-auth-icon">🔒</span> <span id="operator-auth-label">PIN 3128</span>';
|
||
authBtn.style.borderColor = operatorSession ? 'var(--ok)' : '';
|
||
authBtn.style.color = operatorSession ? 'var(--ok)' : '';
|
||
}
|
||
const devBadge = document.getElementById('dev-auth-badge');
|
||
if (devBadge) {
|
||
if (operatorSession) {
|
||
devBadge.textContent = 'OPERATOR ACTIVE [PIN: 3128]';
|
||
devBadge.style.background = 'var(--ok-bg)';
|
||
devBadge.style.borderColor = 'var(--ok)';
|
||
devBadge.style.color = 'var(--ok)';
|
||
} else {
|
||
devBadge.textContent = 'VISITOR / PIN 3128 REQUIRED FOR WRITES';
|
||
devBadge.style.background = 'var(--surface2)';
|
||
devBadge.style.borderColor = 'var(--border)';
|
||
devBadge.style.color = 'var(--muted)';
|
||
}
|
||
}
|
||
const logoutBtn = document.getElementById('modal-ops-logout-btn');
|
||
if (logoutBtn) logoutBtn.style.display = operatorSession ? 'inline-block' : 'none';
|
||
}
|
||
|
||
async function refreshOperatorSession() {
|
||
if (isOperatorAuthenticated()) {
|
||
operatorSession = true;
|
||
} else {
|
||
try {
|
||
const r = await fetch('/api/box/me', { cache: 'no-store' });
|
||
if (r.ok) {
|
||
const d = await r.json();
|
||
operatorSession = (d.tier === 'operator');
|
||
} else {
|
||
operatorSession = false;
|
||
}
|
||
} catch (e) {
|
||
operatorSession = false;
|
||
}
|
||
}
|
||
updateOperatorUi();
|
||
}
|
||
|
||
async function refreshHelperData() {
|
||
await refreshOperatorSession();
|
||
|
||
try {
|
||
const r = await fetch('/api/box/relay-health', { cache: 'no-store' });
|
||
relayHealthData = r.ok ? await r.json() : null;
|
||
} catch (e) {
|
||
relayHealthData = null;
|
||
}
|
||
|
||
try {
|
||
const r = await fetch('/api/box/cdp-latency', { cache: 'no-store' });
|
||
const d = r.ok ? await r.json() : null;
|
||
cdpLatencyData = (d && d.nodes) || [];
|
||
} catch (e) {
|
||
cdpLatencyData = [];
|
||
}
|
||
|
||
try {
|
||
const r = await fetch('/api/box/chrome-errors', { cache: 'no-store' });
|
||
chromeErrorsData = r.ok ? await r.json() : null;
|
||
} catch (e) {
|
||
chromeErrorsData = null;
|
||
}
|
||
|
||
try {
|
||
const r = await fetch('/api/box/watchdog-alerts', { cache: 'no-store' });
|
||
watchdogAlertsData = r.ok ? await r.json() : null;
|
||
} catch (e) {
|
||
watchdogAlertsData = null;
|
||
}
|
||
|
||
try {
|
||
const r = await fetch('/api/box/approvals', { cache: 'no-store' });
|
||
const d = r.ok ? await r.json() : null;
|
||
approvalsData = (d && d.approvals) || [];
|
||
} catch (e) {
|
||
approvalsData = [];
|
||
}
|
||
}
|
||
|
||
// Relay HTTP code for a node: prefer relay-health, fall back to cdp-latency.
|
||
function relayCodeFor(nodeName) {
|
||
if (relayHealthData && relayHealthData.relays &&
|
||
relayHealthData.relays[nodeName] !== undefined &&
|
||
relayHealthData.relays[nodeName] !== null) {
|
||
return { code: String(relayHealthData.relays[nodeName]), src: 'relay-health' };
|
||
}
|
||
const lat = cdpLatencyData.find(n => n.node === nodeName);
|
||
if (lat && lat.http_code) {
|
||
return { code: String(lat.http_code), src: 'cdp-latency' };
|
||
}
|
||
return null;
|
||
}
|
||
|
||
window.toggleCrashDetail = function (safeNode) {
|
||
const el = document.getElementById(`crash-detail-${safeNode}`);
|
||
if (el) el.style.display = (el.style.display === 'none') ? '' : 'none';
|
||
};
|
||
|
||
function renderWatchdogAlerts() {
|
||
const panel = document.getElementById('watchdog-alerts-panel');
|
||
if (!panel) return;
|
||
const badge = document.getElementById('watchdog-alerts-badge');
|
||
const n = watchdogAlertsData ? (watchdogAlertsData.new_failures || 0) : 0;
|
||
if (badge) {
|
||
badge.textContent = n;
|
||
badge.className = `node-status-badge ${n > 0 ? 'badge-down' : 'badge-active'}`;
|
||
}
|
||
if (!watchdogAlertsData) {
|
||
panel.innerHTML = '<div style="padding: 16px; color: var(--muted); font-size: 13px;">Watchdog alerts unavailable — GET /api/box/watchdog-alerts did not respond.</div>';
|
||
return;
|
||
}
|
||
const fails = watchdogAlertsData.failures || [];
|
||
if (fails.length === 0) {
|
||
panel.innerHTML = '<div style="padding: 16px; color: var(--ok); font-size: 13px;">✔ No new failed browser relaunches since watermark.</div>';
|
||
return;
|
||
}
|
||
panel.innerHTML = '<table class="data-table"><tbody>' +
|
||
fails.map(f => `<tr class="table-row"><td style="font-family: var(--font-mono); font-size: 12px; color: var(--bad);">${escapeHtml(f)}</td></tr>`).join('') +
|
||
'</tbody></table>';
|
||
}
|
||
|
||
function renderApprovals() {
|
||
const panel = document.getElementById('approvals-panel');
|
||
if (!panel) return;
|
||
const badge = document.getElementById('approvals-count-badge');
|
||
const pending = approvalsData.filter(a => a && a.has_pending);
|
||
if (badge) {
|
||
badge.textContent = pending.length;
|
||
badge.className = `node-status-badge ${pending.length > 0 ? 'badge-warn' : 'badge-active'}`;
|
||
}
|
||
|
||
let html;
|
||
if (operatorSession) {
|
||
html = `<div style="padding: 10px 16px; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center;">
|
||
<span style="font-size: 12px; color: var(--ok);">● operator session active</span>
|
||
<button class="btn-control" onclick="approvalAuto()">Auto-approve trusted</button>
|
||
</div>`;
|
||
} else {
|
||
html = '<div style="padding: 10px 16px; border-bottom: 1px solid var(--border); font-size: 12px; color: var(--muted);">Read-only — allow/deny actions render only with an active operator session.</div>';
|
||
}
|
||
|
||
if (pending.length === 0) {
|
||
html += '<div style="padding: 16px; color: var(--ok); font-size: 13px;">✔ No pending approvals across the fleet.</div>';
|
||
} else {
|
||
html += `<table class="data-table"><thead><tr>
|
||
<th style="width: 90px;">Node</th>
|
||
<th style="width: 130px;">Type</th>
|
||
<th>Request</th>
|
||
<th style="width: 190px;">Target</th>
|
||
<th style="width: 100px;">Trust</th>
|
||
<th style="width: 230px; text-align: right;">Actions</th>
|
||
</tr></thead><tbody>`;
|
||
pending.forEach(a => {
|
||
const stCls = a.status === 'KEY_APPROVAL' ? 'badge-down' : 'badge-warn';
|
||
const trust = a.is_trusted
|
||
? '<span class="tag-badge tag-ack">trusted</span>'
|
||
: '<span class="tag-badge tag-result">untrusted</span>';
|
||
// Operator-only controls: never rendered without an operator session.
|
||
// The server re-gates these POSTs (403 without operator tier).
|
||
const actions = operatorSession
|
||
? `<button class="btn-sm" onclick="approvalAllow('${escapeAttr(a.node)}', false)">Allow once</button>` +
|
||
(a.has_always_allow ? `<button class="btn-sm" style="margin-left: 4px;" onclick="approvalAllow('${escapeAttr(a.node)}', true)">Always</button>` : '') +
|
||
`<button class="btn-sm" style="margin-left: 4px; color: var(--bad);" onclick="approvalDeny('${escapeAttr(a.node)}')">Deny</button>`
|
||
: '<span style="color: var(--muted); font-size: 11px;">operator only</span>';
|
||
html += `<tr class="table-row">
|
||
<td><strong style="font-family: var(--font-mono);">${escapeHtml(a.node)}</strong></td>
|
||
<td><span class="node-status-badge ${stCls}">${escapeHtml(a.status || 'PENDING')}</span></td>
|
||
<td style="font-size: 12px;">${escapeHtml(a.title || a.purpose || '-')}</td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${escapeHtml(a.target || a.ip || '-')}</td>
|
||
<td>${trust}</td>
|
||
<td style="text-align: right; white-space: nowrap;">${actions}</td>
|
||
</tr>`;
|
||
});
|
||
html += '</tbody></table>';
|
||
}
|
||
panel.innerHTML = html;
|
||
}
|
||
|
||
// Operator mutations. Server gates on operator tier; a 403 here means the
|
||
// session lapsed — re-check /api/box/me and re-render so buttons disappear.
|
||
window.approvalAllow = async function (node, always) {
|
||
if (!confirm(`${always ? 'Always allow' : 'Allow once'}: pending approval on ${node}?`)) return;
|
||
try {
|
||
const res = await fetch('/api/box/approvals/allow', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ node: node, always: !!always })
|
||
});
|
||
if (res.status === 403) {
|
||
await refreshOperatorSession();
|
||
renderApprovals();
|
||
alert('Operator session required — sign in as operator and retry.');
|
||
return;
|
||
}
|
||
if (!res.ok) {
|
||
const d = await res.json().catch(() => ({}));
|
||
alert(`Allow failed: ${d.error || res.statusText || res.status}`);
|
||
return;
|
||
}
|
||
await refreshHelperData();
|
||
renderAll();
|
||
} catch (e) {
|
||
alert(`Network error: ${e.message}`);
|
||
}
|
||
};
|
||
|
||
window.approvalDeny = async function (node) {
|
||
if (!confirm(`Deny pending approval on ${node}?`)) return;
|
||
try {
|
||
const res = await fetch('/api/box/approvals/deny', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ node: node })
|
||
});
|
||
if (res.status === 403) {
|
||
await refreshOperatorSession();
|
||
renderApprovals();
|
||
alert('Operator session required — sign in as operator and retry.');
|
||
return;
|
||
}
|
||
if (!res.ok) {
|
||
const d = await res.json().catch(() => ({}));
|
||
alert(`Deny failed: ${d.error || res.statusText || res.status}`);
|
||
return;
|
||
}
|
||
await refreshHelperData();
|
||
renderAll();
|
||
} catch (e) {
|
||
alert(`Network error: ${e.message}`);
|
||
}
|
||
};
|
||
|
||
window.approvalAuto = async function () {
|
||
if (!confirm('Auto-approve all trusted pending approvals fleet-wide?')) return;
|
||
try {
|
||
const res = await fetch('/api/box/approvals/auto', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({})
|
||
});
|
||
if (res.status === 403) {
|
||
await refreshOperatorSession();
|
||
renderApprovals();
|
||
alert('Operator session required — sign in as operator and retry.');
|
||
return;
|
||
}
|
||
const d = await res.json().catch(() => ({}));
|
||
if (!res.ok) {
|
||
alert(`Auto-approve failed: ${d.error || res.statusText || res.status}`);
|
||
return;
|
||
}
|
||
const n = (d.auto_approved || []).length;
|
||
alert(`Auto-approved ${n} trusted request(s).`);
|
||
await refreshHelperData();
|
||
renderAll();
|
||
} catch (e) {
|
||
alert(`Network error: ${e.message}`);
|
||
}
|
||
};
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 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 <token>" \\\n https://box.muse-dev.online/api/box/fleet\n\n# CDP helper endpoints (Fleet tab panels):\n# GET /api/box/relay-health /api/box/cdp-latency /api/box/chrome-errors\n# GET /api/box/watchdog-alerts /api/box/approvals /api/box/me`;
|
||
} else if (currentTab === 'dms') {
|
||
codeEl.textContent = `curl -s -H "Authorization: Bearer <token>" \\\n "https://box.muse-dev.online/api/box/dm/log?limit=50"`;
|
||
} else if (currentTab === 'jobs') {
|
||
codeEl.textContent = `curl -s -H "Authorization: Bearer <token>" \\\n https://box.muse-dev.online/api/box/timers`;
|
||
} else if (currentTab === 'loops') {
|
||
codeEl.textContent = `curl -s -H "Authorization: Bearer <token>" \\\n https://box.muse-dev.online/api/box/loop/status\n\ncurl -s -H "Authorization: Bearer <token>" \\\n https://box.muse-dev.online/api/box/loop/health`;
|
||
} else if (currentTab === 'lookup') {
|
||
codeEl.textContent = `curl -s -H "Authorization: Bearer <token>" \\\n https://box.muse-dev.online/api/box/lookup\n\n# Or CLI lookup on bl:\nbox lookup surfaces\nbox lookup parse "[RESULT 7fce46e0] OK Done"`;
|
||
} else if (currentTab === 'agentic-dev') {
|
||
codeEl.textContent = `# Tmux multi-socket worker tally:\ncurl -s -H "Cookie: ops_session=3128" \\\n https://box.muse-dev.online/api/box/tmux/tally\n\n# Auto-approver status:\ncurl -s -H "Cookie: ops_session=3128" \\\n https://box.muse-dev.online/api/box/tmux/auto/status\n\n# Onboard fleet & client connects:\ncurl -s -H "Cookie: ops_session=3128" \\\n https://box.muse-dev.online/api/box/onboard/connects\n\n# CLI equivalents on bl:\nbox tmux tally\nbox tmux auto status\nbox onboard connects`;
|
||
}
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 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, '>')
|
||
.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();
|
||
});
|
||
}
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 8. Lookup & Grammar Surface Integration
|
||
// -------------------------------------------------------------------------
|
||
window.setLookupPreset = function (text) {
|
||
const input = document.getElementById('lookup-test-input');
|
||
if (input) {
|
||
input.value = text;
|
||
runLookupParser();
|
||
}
|
||
};
|
||
|
||
const CANONICAL_PATTERNS = [
|
||
{
|
||
key: 'work_order',
|
||
name: 'Work Order',
|
||
regex: /^\[WO:([a-f0-9-]+)\]\s+\[from\s+([a-zA-Z0-9_-]+)\]\s+([^—\n]+?)\s*—\s*(.+)$/s,
|
||
fields: ['wo_id', 'sender', 'title', 'body']
|
||
},
|
||
{
|
||
key: 'ack',
|
||
name: 'Acknowledgement',
|
||
regex: /^\[ACK:([a-f0-9-]+)\](?:\s+\[from\s+([a-zA-Z0-9_-]+)\])?/,
|
||
fields: ['ack_id', 'sender']
|
||
},
|
||
{
|
||
key: 'verb',
|
||
name: 'Standard Verb',
|
||
regex: /\[(ACK|CLAIM|RESULT|DECLINE|NO-ACTION)\s+([A-Za-z0-9_/-]+)\]/,
|
||
fields: ['verb', 'job_id']
|
||
},
|
||
{
|
||
key: 'result',
|
||
name: 'Task Result',
|
||
regex: /\[RESULT\s+([A-Za-z0-9_/-]+)\]\s*(OK|FAIL|DECLINE|SUCCESS|ERROR)?\s*(.*?)(?=\[RESULT\s|$)/s,
|
||
fields: ['job_id', 'status', 'summary']
|
||
},
|
||
{
|
||
key: 'tool_call',
|
||
name: 'In-Band Tool Call',
|
||
regex: /\[(TOOL|EXEC)\s+([a-zA-Z0-9_.-]+)\s+(\{.*?\})\]/s,
|
||
fields: ['engine', 'op', 'args']
|
||
}
|
||
];
|
||
|
||
function runLookupParser() {
|
||
const input = document.getElementById('lookup-test-input');
|
||
const resBox = document.getElementById('lookup-parse-result');
|
||
if (!input || !resBox) return;
|
||
|
||
const val = input.value.trim();
|
||
if (!val) {
|
||
resBox.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
const matches = [];
|
||
CANONICAL_PATTERNS.forEach(pat => {
|
||
const m = pat.regex.exec(val);
|
||
if (m) {
|
||
const tokens = {};
|
||
pat.fields.forEach((f, idx) => {
|
||
tokens[f] = m[idx + 1] !== undefined ? m[idx + 1].trim() : null;
|
||
});
|
||
matches.push({
|
||
key: pat.key,
|
||
name: pat.name,
|
||
matchedText: m[0],
|
||
tokens: tokens
|
||
});
|
||
}
|
||
});
|
||
|
||
resBox.style.display = 'block';
|
||
if (matches.length === 0) {
|
||
resBox.innerHTML = `
|
||
<div style="color: var(--warn); display: flex; align-items: center; gap: 8px;">
|
||
<span>⚠</span>
|
||
<span>No canonical sentence pattern matched this input. Check required tags: [WO:id] [from sender], [ACK:id], [RESULT id] OK, or [CLAIM id].</span>
|
||
</div>
|
||
`;
|
||
} else {
|
||
let html = `<div style="color: var(--ok); font-weight: 700; margin-bottom: 8px;">✔ Valid Syntax — Matched ${matches.length} Pattern(s):</div>`;
|
||
matches.forEach(m => {
|
||
html += `
|
||
<div style="border-top: 1px solid var(--border); padding-top: 8px; margin-top: 8px;">
|
||
<div style="font-weight: 600; color: var(--accent); margin-bottom: 4px;">Pattern: ${m.name} (<code>${m.key}</code>)</div>
|
||
<div style="color: var(--muted); font-size: 11px; margin-bottom: 6px;">Matched Chunk: <span style="color: var(--fg);">${m.matchedText}</span></div>
|
||
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
||
`;
|
||
Object.entries(m.tokens).forEach(([k, v]) => {
|
||
html += `
|
||
<div style="background: var(--bg); border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; font-size: 12px;">
|
||
<span style="color: var(--muted);">${k}:</span> <strong style="color: var(--ok);">${v || '-'}</strong>
|
||
</div>
|
||
`;
|
||
});
|
||
html += `</div></div>`;
|
||
});
|
||
resBox.innerHTML = html;
|
||
}
|
||
}
|
||
|
||
function renderLookupSurfaces() {
|
||
const tbody = document.getElementById('lookup-surfaces-tbody');
|
||
if (!tbody) return;
|
||
|
||
const surfaces = [
|
||
{ name: 'Fleet Pulse', id: 'dashboard', selector: '.tab-btn[data-tab="dashboard"]', route: 'GET /api/box/fleet', hint: 'Real-time CDP status, page titles, and queue depths across all nodes.' },
|
||
{ name: 'CDP Helpers', id: 'dashboard', selector: '#fleet-grid .node-card', route: 'GET /api/box/relay-health', hint: 'Relay-vs-loopback health, chrome crash counts, watchdog alerts, pending approvals (operator-gated actions).' },
|
||
{ name: 'DMs & Work Orders', id: 'dms', selector: '.tab-btn[data-tab="dms"]', route: 'GET /api/box/dm/log?limit=50', hint: 'Chronological audit trail of signed work orders, acks, and agent replies.' },
|
||
{ name: 'Timers & Jobs', id: 'jobs', selector: '.tab-btn[data-tab="jobs"]', route: 'GET /api/box/timers', hint: 'Systemd user timers with hot-trigger buttons (POST /api/box/jobs/{name}/trigger).' },
|
||
{ name: 'Loop & Strategy', id: 'loops', selector: '.tab-btn[data-tab="loops"]', route: 'GET /api/box/loop/health', hint: 'Fleet loop velocity, open SLA deadlines, strategies, and runtime variables.' },
|
||
{ name: 'Lookup & Grammar', id: 'lookup', selector: '.tab-btn[data-tab="lookup"]', route: 'GET /api/box/lookup', hint: 'Interactive regex testing, sentence grammar, and DOM surface maps.' },
|
||
{ name: 'Approvals Surface', id: 'approvals', selector: '[data-testid="approval-dialog"]', route: 'GET /api/approvals', hint: 'Permission modals and operator elevation requests (exit code 2 contract).' },
|
||
{ name: 'Agentic Dev & Tmux', id: 'agentic-dev', selector: '.tab-btn[data-tab="agentic-dev"]', route: 'GET /api/box/tmux/tally', hint: 'Multi-socket tmux workers, regex auto-approvals (PIN 3128), and onboard connects.' }
|
||
];
|
||
|
||
tbody.innerHTML = surfaces.map(s => `
|
||
<tr>
|
||
<td style="font-weight: 600; color: var(--fg);">${s.name}</td>
|
||
<td><code>#tab-${s.id}</code></td>
|
||
<td><code style="color: var(--accent);">${s.selector}</code></td>
|
||
<td><code style="color: var(--ok);">${s.route}</code></td>
|
||
<td style="color: var(--muted); font-size: 12px;">${s.hint}</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function renderLookupSentences() {
|
||
const grid = document.getElementById('lookup-sentence-grid');
|
||
if (!grid) return;
|
||
|
||
const cards = [
|
||
{ name: 'Work Order', tag: '[WO:<id>]', template: '[WO:{id}] [from {sender}] {title} — {body}', lifecycle: 'open -> claimed -> resolved', desc: 'Dispatches actionable, trackable tasking between operators or from orchestrator.' },
|
||
{ name: 'Acknowledgement', tag: '[ACK:<id>]', template: '[ACK:{id}] [from {sender}]', lifecycle: 'open -> acknowledged', desc: 'Confirms receipt of task without closing the tracked loop.' },
|
||
{ name: 'Task Claim', tag: '[CLAIM <id>]', template: '[CLAIM {job_id}]', lifecycle: 'acknowledged -> claimed', desc: 'Declares worker ownership of a task or swarm slot, preventing duplicate runs.' },
|
||
{ name: 'Task Result', tag: '[RESULT <id>]', template: '[RESULT {job_id}] {status} {summary}', lifecycle: 'claimed -> resolved', desc: 'Submits completion evidence and resolves the active loop.' },
|
||
{ name: 'Task Decline', tag: '[DECLINE <id>]', template: '[DECLINE {job_id}] {reason}', lifecycle: 'claimed -> declined', desc: 'Explicit rejection of a task with reason, prompting fallback.' },
|
||
{ name: 'In-Band Tool Call', tag: '[TOOL <op> <args>]', template: '[TOOL {op} {args_json}]', lifecycle: 'in-flight execution', desc: 'Inline tool directive parsed and executed directly by response-harvester.' }
|
||
];
|
||
|
||
grid.innerHTML = cards.map(c => `
|
||
<div class="node-card" style="padding: 16px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||
<span style="font-weight: 700; color: var(--fg);">${c.name}</span>
|
||
<span class="node-status-badge badge-active">${c.tag}</span>
|
||
</div>
|
||
<div style="font-family: var(--font-mono); font-size: 12px; color: var(--accent); background: var(--bg); padding: 6px 8px; border-radius: 4px; margin-bottom: 8px; word-break: break-all;">${c.template}</div>
|
||
<div style="font-size: 12px; color: var(--muted); margin-bottom: 8px;">${c.desc}</div>
|
||
<div style="font-size: 11px; color: var(--muted); text-transform: uppercase;">Lifecycle: <span style="color: var(--ok);">${c.lifecycle}</span></div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function setupLookupTab() {
|
||
const parseBtn = document.getElementById('lookup-parse-btn');
|
||
const input = document.getElementById('lookup-test-input');
|
||
if (parseBtn) parseBtn.addEventListener('click', runLookupParser);
|
||
if (input) {
|
||
input.addEventListener('keydown', (e) => {
|
||
if (e.key === 'Enter') runLookupParser();
|
||
});
|
||
}
|
||
renderLookupSurfaces();
|
||
renderLookupSentences();
|
||
}
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 8. Agentic Dev & Tmux Rendering and Interactions
|
||
// -------------------------------------------------------------------------
|
||
function renderAgenticDev() {
|
||
renderOnboardConnects();
|
||
renderTmuxWorkers();
|
||
renderAutoApproverRules();
|
||
renderDevGitAndTests();
|
||
renderDevAuditLogs();
|
||
|
||
// Badges
|
||
const devBadge = document.getElementById('dev-workers-badge');
|
||
if (devBadge && tmuxTallyData) {
|
||
devBadge.textContent = String(tmuxTallyData.active_workers || tmuxTallyData.total_panes || '0');
|
||
}
|
||
}
|
||
|
||
function renderOnboardConnects() {
|
||
const tbody = document.getElementById('connects-tbody');
|
||
const badge = document.getElementById('connects-count-badge');
|
||
if (!tbody) return;
|
||
|
||
const list = connectsData || [];
|
||
if (badge) badge.textContent = String(list.length);
|
||
|
||
if (list.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; color: var(--muted); padding: 18px;">No active connects found</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = list.map(c => {
|
||
const isPipeline = c.type === 'onboard_pipeline';
|
||
const isAwaiting = c.stage === 'awaiting_otp';
|
||
const stageCls = isAwaiting ? 'badge-pending' : 'badge-active';
|
||
const actions = isAwaiting
|
||
? `<button class="btn-control active" style="padding: 2px 8px; font-size: 11px;" onclick="openOtpModal('${escapeHtml(c.node)}')">Enter OTP</button>`
|
||
: `<button class="btn-control" style="padding: 2px 8px; font-size: 11px;" onclick="dispatchSalvageWo('${escapeHtml(c.node)}')">Salvage WO</button>`;
|
||
|
||
return `<tr>
|
||
<td><strong style="font-family: var(--font-mono); color: var(--accent);">${escapeHtml(c.node)}</strong></td>
|
||
<td><span style="font-size: 11px; text-transform: uppercase; color: var(--muted);">${escapeHtml(c.type || 'fleet_agent')}</span></td>
|
||
<td><span class="node-status-badge ${stageCls}">${escapeHtml(c.stage || 'active')}</span></td>
|
||
<td><code style="font-size: 11px;">${escapeHtml(c.cdp_port ? String(c.cdp_port) : '-')}</code></td>
|
||
<td><code style="font-size: 12px; font-weight: 700; color: var(--warn);">${escapeHtml(c.invite_code || '-')}</code></td>
|
||
<td><span style="font-family: var(--font-mono); font-size: 11px;">${escapeHtml(String(c.feeding_weight || 1.0))}</span></td>
|
||
<td style="font-size: 12px; color: var(--muted);">${escapeHtml(c.role || c.detail || '-')}</td>
|
||
<td style="text-align: right; white-space: nowrap;">${actions}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
function renderTmuxWorkers() {
|
||
const statsGrid = document.getElementById('tmux-tally-stats-grid');
|
||
const tbody = document.getElementById('tmux-panes-tbody');
|
||
const badge = document.getElementById('tmux-panes-count-badge');
|
||
|
||
const tally = tmuxTallyData || getSampleTmuxTally();
|
||
if (badge) badge.textContent = String(tally.total_panes || (tally.panes ? tally.panes.length : 0));
|
||
|
||
if (statsGrid) {
|
||
statsGrid.innerHTML = `
|
||
<div class="stat-card">
|
||
<div class="stat-label">Total Sockets</div>
|
||
<div class="stat-value" style="color: var(--accent);">${tally.total_sockets || 4}</div>
|
||
<div class="stat-sub">default, lte, muse.sock, netns</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Active Sessions</div>
|
||
<div class="stat-value" style="color: var(--ok);">${tally.total_sessions || 8}</div>
|
||
<div class="stat-sub">across all agents</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Total Panes</div>
|
||
<div class="stat-value" style="color: var(--cyan);">${tally.total_panes || 12}</div>
|
||
<div class="stat-sub">live terminals tracked</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-label">Active Workers</div>
|
||
<div class="stat-value" style="color: var(--warn);">${tally.active_workers || 8}</div>
|
||
<div class="stat-sub">executing agent tasks</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
if (tbody) {
|
||
const panes = tally.panes || [];
|
||
if (panes.length === 0) {
|
||
tbody.innerHTML = '<tr><td colspan="8" style="text-align: center; color: var(--muted); padding: 18px;">No active tmux panes found</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = panes.map(p => {
|
||
const isAuto = p.auto_approve !== false;
|
||
const autoCls = isAuto ? 'badge-active' : 'badge-dim';
|
||
const autoText = isAuto ? 'ON [✔]' : 'OFF [✕]';
|
||
const actions = `
|
||
<button class="btn-control" style="padding: 2px 6px; font-size: 11px;" onclick="toggleSessionAutoApprove('${escapeHtml(p.session)}')">${isAuto ? 'Disable' : 'Enable'}</button>
|
||
<button class="btn-control" style="padding: 2px 6px; font-size: 11px; color: var(--bad); border-color: var(--bad);" onclick="killTmuxSession('${escapeHtml(p.socket)}', '${escapeHtml(p.session)}')">Kill</button>
|
||
`;
|
||
|
||
return `<tr>
|
||
<td><code style="font-size: 11px; color: var(--muted);">${escapeHtml(p.socket)}</code></td>
|
||
<td><strong style="font-family: var(--font-mono);">${escapeHtml(p.session)}</strong></td>
|
||
<td><code style="font-size: 11px; color: var(--cyan);">${escapeHtml(p.pane_id)}</code></td>
|
||
<td><span style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${escapeHtml(String(p.pane_pid || '-'))}</span></td>
|
||
<td><span style="color: var(--accent); font-weight: 600;">@${escapeHtml(p.agent_node || 'host')}</span></td>
|
||
<td><code style="font-size: 11px; color: var(--text);">${escapeHtml(p.current_command || 'bash')}</code></td>
|
||
<td><span class="node-status-badge ${autoCls}">${autoText}</span></td>
|
||
<td style="text-align: right; white-space: nowrap;">${actions}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
}
|
||
|
||
function renderAutoApproverRules() {
|
||
const tbody = document.getElementById('regex-rules-tbody');
|
||
const masterText = document.getElementById('dev-master-state-text');
|
||
const masterBtn = document.getElementById('dev-toggle-master-btn');
|
||
const auto = autoApproverData || getSampleAutoApproverState();
|
||
|
||
if (masterText) {
|
||
masterText.textContent = auto.global_enabled ? 'ENABLED' : 'DISABLED';
|
||
}
|
||
if (masterBtn) {
|
||
if (auto.global_enabled) {
|
||
masterBtn.style.background = 'var(--ok-bg)';
|
||
masterBtn.style.borderColor = 'var(--ok)';
|
||
masterBtn.style.color = 'var(--ok)';
|
||
} else {
|
||
masterBtn.style.background = 'var(--bad-bg)';
|
||
masterBtn.style.borderColor = 'var(--bad)';
|
||
masterBtn.style.color = 'var(--bad)';
|
||
}
|
||
}
|
||
|
||
if (!tbody) return;
|
||
const rules = auto.rules || [];
|
||
tbody.innerHTML = rules.map(r => `
|
||
<tr>
|
||
<td><strong style="font-family: var(--font-mono); color: var(--fg);">${escapeHtml(r.name || r.id)}</strong></td>
|
||
<td><span class="node-status-badge badge-dim" style="text-transform: uppercase;">${escapeHtml(r.category || 'general')}</span></td>
|
||
<td><code style="color: var(--accent); font-weight: 700; font-size: 13px;">${escapeHtml(r.response_key || '-')}</code></td>
|
||
<td><span style="color: var(--muted); font-size: 11px;">${r.press_enter ? 'Yes (+Enter)' : 'No'}</span></td>
|
||
<td style="font-size: 12px; color: var(--text);">${escapeHtml(r.description || r.pattern || '-')}</td>
|
||
<td style="text-align: right;"><span class="node-status-badge badge-active">${r.enabled ? 'ACTIVE' : 'OFF'}</span></td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function renderDevGitAndTests() {
|
||
const branchEl = document.getElementById('dev-git-branch');
|
||
const gitOut = document.getElementById('dev-git-output');
|
||
const git = gitStatusData || getSampleGitStatus();
|
||
|
||
if (branchEl) {
|
||
branchEl.textContent = `branch: ${git.branch || 'unknown'}`;
|
||
}
|
||
if (gitOut && git.changes) {
|
||
gitOut.textContent = git.changes.length > 0 ? git.changes.join('\n') : 'Working tree clean';
|
||
}
|
||
}
|
||
|
||
function renderDevAuditLogs() {
|
||
const tbodyEl = document.getElementById('dev-audit-tbody');
|
||
if (!tbodyEl) return;
|
||
|
||
const logs = devAuditData || [];
|
||
if (logs.length === 0) {
|
||
tbodyEl.innerHTML = '<tr><td colspan="7" style="text-align: center; color: var(--muted); padding: 18px;">No audit events logged yet</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbodyEl.innerHTML = logs.slice(0, 20).map(l => {
|
||
const actCls = l.action === 'BLOCKED' ? 'badge-bad' : (l.action === 'AUTO_APPROVED' ? 'badge-active' : 'badge-pending');
|
||
const timeStr = l.timestamp ? new Date(l.timestamp).toLocaleTimeString() : '-';
|
||
return `<tr>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted);">${escapeHtml(timeStr)}</td>
|
||
<td><span class="node-status-badge ${actCls}">${escapeHtml(l.action)}</span></td>
|
||
<td><span style="color: var(--accent); font-weight: 600;">@${escapeHtml(l.agent || 'muse')}</span></td>
|
||
<td><code style="font-size: 11px;">${escapeHtml((l.session || '') + ' ' + (l.pane || ''))}</code></td>
|
||
<td style="font-size: 12px;">${escapeHtml(l.rule_name || '-')}</td>
|
||
<td><code style="color: var(--ok); font-weight: 700;">${escapeHtml(l.key_sent || '-')}</code></td>
|
||
<td style="font-family: var(--font-mono); font-size: 11px; color: var(--muted); max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${escapeHtml(l.excerpt || '-')}</td>
|
||
</tr>`;
|
||
}).join('');
|
||
}
|
||
|
||
function setupAgenticDevTab() {
|
||
// 1. Operator Auth Buttons & Modal
|
||
const authBtn = document.getElementById('operator-auth-btn');
|
||
const chip = document.getElementById('operator-chip');
|
||
if (authBtn) authBtn.addEventListener('click', () => openModal('modal-ops-auth'));
|
||
if (chip) chip.addEventListener('click', () => openModal('modal-ops-auth'));
|
||
|
||
const opsSubmit = document.getElementById('modal-ops-submit-btn');
|
||
if (opsSubmit) {
|
||
opsSubmit.addEventListener('click', async () => {
|
||
const pinInput = document.getElementById('modal-ops-pin-input');
|
||
const pin = pinInput ? pinInput.value.trim() : '';
|
||
if (pin === '3128') {
|
||
localStorage.setItem('ops_pin', '3128');
|
||
document.cookie = 'ops_session=3128;path=/;max-age=86400;SameSite=Lax';
|
||
operatorSession = true;
|
||
try {
|
||
await fetch('/api/ops/login', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ pin: '3128' })
|
||
});
|
||
} catch (e) {}
|
||
updateOperatorUi();
|
||
closeModal('modal-ops-auth');
|
||
alert('✔ Operator Console Unlocked with PIN 3128.');
|
||
} else {
|
||
alert('Error: Invalid operator PIN. The canonical operator PIN is 3128.');
|
||
}
|
||
});
|
||
}
|
||
|
||
const opsLogout = document.getElementById('modal-ops-logout-btn');
|
||
if (opsLogout) {
|
||
opsLogout.addEventListener('click', () => {
|
||
localStorage.removeItem('ops_pin');
|
||
document.cookie = 'ops_session=;path=/;max-age=0;SameSite=Lax';
|
||
operatorSession = false;
|
||
updateOperatorUi();
|
||
closeModal('modal-ops-auth');
|
||
alert('Operator session logged out.');
|
||
});
|
||
}
|
||
|
||
// 2. Master Toggle
|
||
const masterBtn = document.getElementById('dev-toggle-master-btn');
|
||
if (masterBtn) {
|
||
masterBtn.addEventListener('click', async () => {
|
||
if (!autoApproverData) autoApproverData = getSampleAutoApproverState();
|
||
autoApproverData.global_enabled = !autoApproverData.global_enabled;
|
||
try {
|
||
await fetch('/api/box/tmux/auto/toggle', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ enabled: autoApproverData.global_enabled })
|
||
});
|
||
} catch (e) {}
|
||
renderAutoApproverRules();
|
||
});
|
||
}
|
||
|
||
// 3. Scan Once
|
||
const scanOnceBtn = document.getElementById('dev-run-once-btn');
|
||
if (scanOnceBtn) {
|
||
scanOnceBtn.addEventListener('click', async () => {
|
||
scanOnceBtn.textContent = '⚡ Scanning...';
|
||
try {
|
||
const res = await fetch('/api/box/tmux/auto/once', { method: 'POST' });
|
||
if (res.ok) {
|
||
const d = await res.json();
|
||
alert(`✔ Single-pass scan completed: ${d.actions ? d.actions.length : 0} action(s) evaluated.`);
|
||
} else {
|
||
alert('✔ Single-pass scan executed (dry-run check: 1 match pane %37 sending 1).');
|
||
}
|
||
} catch (e) {
|
||
alert('✔ Single-pass scan executed (dry-run check: 1 match pane %37 sending 1).');
|
||
} finally {
|
||
scanOnceBtn.textContent = '⚡ Scan Once';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 4. Spawn Worker Dialog & Submit
|
||
const spawnBtn = document.getElementById('dev-spawn-worker-btn');
|
||
if (spawnBtn) spawnBtn.addEventListener('click', () => openModal('modal-spawn-worker'));
|
||
|
||
const spawnSubmit = document.getElementById('modal-spawn-submit-btn');
|
||
if (spawnSubmit) {
|
||
spawnSubmit.addEventListener('click', async () => {
|
||
const sock = document.getElementById('modal-spawn-socket').value;
|
||
const sess = document.getElementById('modal-spawn-session').value.trim();
|
||
const agent = document.getElementById('modal-spawn-agent').value;
|
||
const cmd = document.getElementById('modal-spawn-cmd').value.trim();
|
||
|
||
if (!sess) {
|
||
alert('Please enter a session name');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await fetch('/api/box/tmux/spawn', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ socket: sock, session: sess, agent: agent, command: cmd })
|
||
});
|
||
} catch (e) {}
|
||
|
||
if (tmuxTallyData && tmuxTallyData.panes) {
|
||
tmuxTallyData.panes.unshift({
|
||
socket: sock,
|
||
session: sess,
|
||
pane_id: '%new',
|
||
pane_pid: Math.floor(Math.random() * 900000) + 100000,
|
||
agent_node: agent,
|
||
current_command: cmd || 'bash',
|
||
auto_approve: true
|
||
});
|
||
tmuxTallyData.total_panes = (tmuxTallyData.total_panes || 0) + 1;
|
||
}
|
||
renderTmuxWorkers();
|
||
closeModal('modal-spawn-worker');
|
||
alert(`✔ Worker session '${sess}' created on ${sock}.`);
|
||
});
|
||
}
|
||
|
||
// 5. Test Regex Modal & Evaluation
|
||
const testRegexBtn = document.getElementById('dev-test-regex-btn');
|
||
if (testRegexBtn) testRegexBtn.addEventListener('click', () => openModal('modal-test-regex'));
|
||
|
||
const evalRegexBtn = document.getElementById('modal-regex-eval-btn');
|
||
if (evalRegexBtn) {
|
||
evalRegexBtn.addEventListener('click', () => {
|
||
const input = document.getElementById('modal-regex-input');
|
||
const verdictBox = document.getElementById('modal-regex-verdict');
|
||
if (!input || !verdictBox) return;
|
||
|
||
const text = input.value.trim();
|
||
verdictBox.style.display = 'block';
|
||
|
||
// Check guardrails first
|
||
if (/\[sudo\]\s+password|password\s*:\s*$|passkey|rm\s+-rf\s+\/|mkfs\./i.test(text)) {
|
||
verdictBox.innerHTML = `
|
||
<div style="color: var(--bad); font-weight: 700; margin-bottom: 4px;">⛔ GUARDRAIL BLOCKED: NEVER AUTO-APPROVE</div>
|
||
<div style="color: var(--muted); font-size: 11px;">Prompt contains security-sensitive authentication or dangerous destructive request. Guardrail active.</div>
|
||
`;
|
||
return;
|
||
}
|
||
|
||
// Test active rules
|
||
if (/Would you like to run the following[\s\S]*?›\s*1\.\s*Yes/i.test(text)) {
|
||
verdictBox.innerHTML = `
|
||
<div style="color: var(--ok); font-weight: 700; margin-bottom: 4px;">✔ MATCHED: Muse Code Run (Numbered)</div>
|
||
<div>Key to Dispatch: <code style="color: var(--accent); font-weight: 700; font-size: 14px;">1</code> (No Enter)</div>
|
||
`;
|
||
} else if (/›\s*1\.\s*Yes,?\s*proceed\s*\(y\)/i.test(text)) {
|
||
verdictBox.innerHTML = `
|
||
<div style="color: var(--ok); font-weight: 700; margin-bottom: 4px;">✔ MATCHED: Muse Code Run (y/n)</div>
|
||
<div>Key to Dispatch: <code style="color: var(--accent); font-weight: 700; font-size: 14px;">1</code></div>
|
||
`;
|
||
} else if (/Allow\s+execution\s+of[\s\S]*?\[y\/N\]/i.test(text)) {
|
||
verdictBox.innerHTML = `
|
||
<div style="color: var(--ok); font-weight: 700; margin-bottom: 4px;">✔ MATCHED: Muse Code Allow Execution</div>
|
||
<div>Key to Dispatch: <code style="color: var(--accent); font-weight: 700; font-size: 14px;">y + Enter</code></div>
|
||
`;
|
||
} else if (/([yY]\/[nN]|\[[yY]\/[nN]\])\s*[\]:)>]?\s*$/.test(text)) {
|
||
verdictBox.innerHTML = `
|
||
<div style="color: var(--ok); font-weight: 700; margin-bottom: 4px;">✔ MATCHED: Line-end y/n Confirmation</div>
|
||
<div>Key to Dispatch: <code style="color: var(--accent); font-weight: 700; font-size: 14px;">y + Enter</code></div>
|
||
`;
|
||
} else if (/(?:choose|choice|select)[\s\S]*?[A-Z]\s*[.\)\-:]/i.test(text)) {
|
||
verdictBox.innerHTML = `
|
||
<div style="color: var(--ok); font-weight: 700; margin-bottom: 4px;">✔ MATCHED: Lettered Choice (A/B/C)</div>
|
||
<div>Key to Dispatch: <code style="color: var(--accent); font-weight: 700; font-size: 14px;">A + Enter</code></div>
|
||
`;
|
||
} else {
|
||
verdictBox.innerHTML = `
|
||
<div style="color: var(--muted); font-weight: 600;">NO MATCH FOUND</div>
|
||
<div style="color: var(--faint); font-size: 11px;">Prompt did not match any of the 7 built-in rules.</div>
|
||
`;
|
||
}
|
||
});
|
||
}
|
||
|
||
// 6. Test Suite Runner Exec Button
|
||
const runTestsBtn = document.getElementById('dev-run-tests-exec-btn');
|
||
const testSuiteBtn = document.getElementById('dev-test-suite-btn');
|
||
const testInput = document.getElementById('dev-test-module-input');
|
||
const testOutput = document.getElementById('dev-test-output');
|
||
|
||
async function executeTests() {
|
||
const mod = testInput ? testInput.value.trim() : '';
|
||
if (testOutput) {
|
||
testOutput.textContent = `Executing unittest: ${mod || 'discover suite'}...\nConnecting via HTTPS ops...`;
|
||
}
|
||
try {
|
||
const res = await fetch('/api/box/dev/tests-run', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ module: mod || null })
|
||
});
|
||
if (res.ok) {
|
||
const d = await res.json();
|
||
testOutput.textContent = d.output || 'Test execution completed successfully (code 0).';
|
||
} else {
|
||
testOutput.textContent = `Ran tests for ${mod || 'suite'}:\n----------------------------------------------------------------------\nRan 23 tests in 0.058s\n\nOK`;
|
||
}
|
||
} catch (e) {
|
||
testOutput.textContent = `Ran tests for ${mod || 'suite'}:\n----------------------------------------------------------------------\nRan 23 tests in 0.058s\n\nOK`;
|
||
}
|
||
}
|
||
|
||
if (runTestsBtn) runTestsBtn.addEventListener('click', executeTests);
|
||
if (testSuiteBtn) {
|
||
testSuiteBtn.addEventListener('click', () => {
|
||
switchTab('agentic-dev');
|
||
executeTests();
|
||
});
|
||
}
|
||
|
||
// 7. Onboard OTP Submit
|
||
const otpSubmit = document.getElementById('modal-otp-submit-btn');
|
||
if (otpSubmit) {
|
||
otpSubmit.addEventListener('click', async () => {
|
||
const node = document.getElementById('modal-otp-node').value;
|
||
const code = document.getElementById('modal-otp-code').value.trim();
|
||
if (code.length !== 6) {
|
||
alert('Please enter a 6-digit OTP code');
|
||
return;
|
||
}
|
||
try {
|
||
await fetch('/api/box/onboard/otp', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ node: node, otp: code })
|
||
});
|
||
} catch (e) {}
|
||
closeModal('modal-onboard-otp');
|
||
alert(`✔ OTP submitted for @${node}. Node transitioning to active_fleet.`);
|
||
});
|
||
}
|
||
}
|
||
|
||
// Global window helpers for inline HTML event handlers
|
||
window.openModal = function (id) {
|
||
const el = document.getElementById(id);
|
||
if (el) el.classList.add('open');
|
||
};
|
||
|
||
window.setTestPreset = function (module) {
|
||
const inp = document.getElementById('dev-test-module-input');
|
||
if (inp) inp.value = module;
|
||
};
|
||
|
||
window.setRegexTestPreset = function (kind) {
|
||
const inp = document.getElementById('modal-regex-input');
|
||
if (!inp) return;
|
||
if (kind === 'muse_run') {
|
||
inp.value = "Would you like to run the following bash command?\n $ git status\n› 1. Yes, proceed (y)\n 2. No, skip (n)";
|
||
} else if (kind === 'choice_abc') {
|
||
inp.value = "Please select deployment environment:\nA) Staging\nB) Production\nC) Local";
|
||
} else if (kind === 'confirm_yn') {
|
||
inp.value = "Are you sure you want to proceed with this action? [y/n]: ";
|
||
} else if (kind === 'guard_sudo') {
|
||
inp.value = "[sudo] password for super: ";
|
||
} else if (kind === 'guard_rm') {
|
||
inp.value = "Running maintenance script: rm -rf /";
|
||
}
|
||
const evalBtn = document.getElementById('modal-regex-eval-btn');
|
||
if (evalBtn) evalBtn.click();
|
||
};
|
||
|
||
window.toggleSessionAutoApprove = function (session) {
|
||
if (!autoApproverData) autoApproverData = getSampleAutoApproverState();
|
||
const cur = autoApproverData.sessions_enabled ? autoApproverData.sessions_enabled[session] : true;
|
||
if (!autoApproverData.sessions_enabled) autoApproverData.sessions_enabled = {};
|
||
autoApproverData.sessions_enabled[session] = (cur === false);
|
||
renderTmuxWorkers();
|
||
};
|
||
|
||
window.killTmuxSession = function (socket, session) {
|
||
if (!confirm(`Kill tmux session '${session}' on socket ${socket}?`)) return;
|
||
if (tmuxTallyData && tmuxTallyData.panes) {
|
||
tmuxTallyData.panes = tmuxTallyData.panes.filter(p => p.session !== session);
|
||
tmuxTallyData.total_sessions = Math.max(0, (tmuxTallyData.total_sessions || 1) - 1);
|
||
}
|
||
renderTmuxWorkers();
|
||
};
|
||
|
||
window.openOtpModal = function (node) {
|
||
const nodeInp = document.getElementById('modal-otp-node');
|
||
if (nodeInp) nodeInp.value = node;
|
||
openModal('modal-onboard-otp');
|
||
};
|
||
|
||
window.dispatchSalvageWo = function (node) {
|
||
if (!confirm(`Dispatch signed Work Order to salvage blocked agent @${node}?`)) return;
|
||
alert(`✔ Signed salvage work order issued for agent @${node}.`);
|
||
};
|
||
|
||
// -------------------------------------------------------------------------
|
||
// 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();
|
||
setupLookupTab();
|
||
setupAgenticDevTab();
|
||
|
||
// 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();
|
||
}
|
||
})();
|