diff --git a/api/endpoints/arc.php b/api/endpoints/arc.php index ac77f42..e7f08dd 100644 --- a/api/endpoints/arc.php +++ b/api/endpoints/arc.php @@ -236,6 +236,62 @@ switch ($action) { echo json_encode(arc_request('DELETE', "/comms/sent/{$id}")); break; + // ── MISSION OPS ─────────────────────────────────────────────────────────── + + // GET /api/arc?action=missions + case 'missions': + echo json_encode(arc_request('GET', '/missions')); + break; + + // GET /api/arc?action=mission_get&id=123 + case 'mission_get': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('GET', "/missions/{$id}")); + break; + + // GET /api/arc?action=mission_runs&id=123 + case 'mission_runs': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + $limit = (int)($_GET['limit'] ?? 20); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('GET', "/missions/{$id}/runs?limit={$limit}")); + break; + + // POST /api/arc?action=mission_create — body: { name, description, trigger_type, trigger_config, steps } + case 'mission_create': + echo json_encode(arc_request('POST', '/missions', $data)); + break; + + // POST /api/arc?action=mission_update — body: { id, name, ... steps } + case 'mission_update': + $id = (int)($data['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('PUT', "/missions/{$id}", $data)); + break; + + // DELETE /api/arc?action=mission_delete&id=123 + case 'mission_delete': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('DELETE', "/missions/{$id}")); + break; + + // POST /api/arc?action=mission_run&id=123 + case 'mission_run': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('POST', "/missions/{$id}/run", ['trigger_source' => 'manual'])); + break; + + // POST /api/arc?action=mission_toggle&id=123 body: { enabled: 1|0 } + case 'mission_toggle': + $id = (int)($data['id'] ?? 0); + $enabled = (int)($data['enabled'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('PUT', "/missions/{$id}", ['enabled' => $enabled])); + break; + default: http_response_code(404); echo json_encode(['error' => "Unknown arc action: {$action}"]); diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 819cdf0..f0fe42b 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -593,6 +593,71 @@ if ($action) { $raw = curl_exec($ch); curl_close($ch); j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']); + // ── MISSION OPS ────────────────────────────────────────────────────── + case 'mission_list': + $ch = curl_init('http://127.0.0.1:7474/missions'); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: []); + + case 'mission_get': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $ch = curl_init('http://127.0.0.1:7474/missions/' . $id); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['error'=>'not found']); + + case 'mission_runs': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $limit = (int)($_GET['limit'] ?? 20); + $ch = curl_init("http://127.0.0.1:7474/missions/{$id}/runs?limit={$limit}"); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: []); + + case 'mission_save': // create or update + $id = (int)($_GET['id'] ?? 0); + $url = $id ? "http://127.0.0.1:7474/missions/{$id}" : 'http://127.0.0.1:7474/missions'; + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10, + CURLOPT_CUSTOMREQUEST => $id ? 'PUT' : 'POST', + CURLOPT_POSTFIELDS => file_get_contents('php://input'), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable']); + + case 'mission_delete': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $ch = curl_init('http://127.0.0.1:7474/missions/' . $id); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'DELETE']); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['ok'=>true]); + + case 'mission_run': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $ch = curl_init("http://127.0.0.1:7474/missions/{$id}/run"); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>120, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS => json_encode(['trigger_source'=>'admin']), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['error'=>'Arc Reactor unreachable or timeout']); + + case 'mission_toggle': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $enabled = (int)($_GET['enabled'] ?? 0); + $ch = curl_init('http://127.0.0.1:7474/missions/' . $id); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5, CURLOPT_CUSTOMREQUEST=>'PUT', + CURLOPT_POSTFIELDS => json_encode(['enabled'=>$enabled]), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['ok'=>true]); + // ── VISION PROTOCOL ────────────────────────────────────────────────── case 'vision_list': $limit = min((int)($_GET['limit'] ?? 30), 100); @@ -928,6 +993,7 @@ select.filter-sel:focus{border-color:var(--cyan)} + @@ -1325,6 +1391,73 @@ select.filter-sel:focus{border-color:var(--cyan)}
LOADING OUTBOX...
+ +
+
◈ MISSION OPS — AUTOMATED WORKFLOWS
+
+ + +
+
+ + +
LOADING MISSIONS...
+ + + + + + +
+ @@ -1438,6 +1571,7 @@ function loadTab(tab) { email: ()=>{ loadEmailInbox(); loadEmailActionItems(); }, triage: loadTriage, outbox: loadOutbox, + missions: loadMissions, vision: loadVision, guardian: loadGuardian, tasks: loadTasks, @@ -2776,6 +2910,340 @@ function outboxCompose() { }, 'DISPATCH'); } +// ── MISSION OPS ────────────────────────────────────────────────────────────── + +const JOB_TYPES = ['ping','echo','shell','llm','research','tool_loop','gmail_triage', + 'remote_exec','screenshot','vision','sysinfo','sitrep','send_email','compose_email', + 'schedule_event','meeting_prep','run_mission']; + +let _missionBuilderSteps = []; +let _missionBuilderStepIdx = 0; + +async function loadMissions() { + const el = document.getElementById('missions-list'); + if (!el) return; + const missions = await api('mission_list'); + const list = Array.isArray(missions) ? missions : []; + document.getElementById('missions-count').textContent = list.length + ' MISSIONS'; + + if (!list.length) { + el.innerHTML = '
No missions yet. Click + NEW MISSION to create one.
'; + return; + } + const triggerIcons = {manual:'🖐', schedule:'⏱', guardian_event:'🛡', email_keyword:'📧'}; + const statusColor = {done:'var(--green)', failed:'var(--red)', running:'var(--orange)'}; + const rows = list.map(m => { + const icon = triggerIcons[m.trigger_type] || '◈'; + const enabled = m.enabled ? 'ENABLED' : 'DISABLED'; + const lastRun = m.last_run_at ? new Date(m.last_run_at+'Z').toLocaleString() : '—'; + return ` + ${icon} ${esc(m.name)} + ${m.trigger_type.replace('_',' ').toUpperCase()} + ${enabled} + ${m.run_count||0} runs · last ${lastRun} + + + + + + + `; + }).join(''); + el.innerHTML = `${rows}
NAMETRIGGERSTATUSLAST RUNACTIONS
`; +} + +function missionNew() { + _missionBuilderSteps = []; + _missionBuilderStepIdx = 0; + document.getElementById('mb-mission-id').value = ''; + document.getElementById('mb-name').value = ''; + document.getElementById('mb-desc').value = ''; + document.getElementById('mb-trigger').value = 'manual'; + document.getElementById('builder-title').textContent = '◈ NEW MISSION'; + document.getElementById('mb-run-btn').style.display = 'none'; + document.getElementById('mb-del-btn').style.display = 'none'; + document.getElementById('mb-status').textContent = ''; + missionTriggerChange(); + _renderBuilderSteps(); + document.getElementById('mission-builder').style.display = 'block'; + document.getElementById('mission-run-history').style.display = 'none'; + document.getElementById('mission-builder').scrollIntoView({behavior:'smooth'}); +} + +async function missionEdit(id) { + const m = await api('mission_get', {id}); + if (m.error) { toast('Load failed: ' + m.error, 'err'); return; } + document.getElementById('mb-mission-id').value = m.id; + document.getElementById('mb-name').value = m.name || ''; + document.getElementById('mb-desc').value = m.description || ''; + document.getElementById('mb-trigger').value = m.trigger_type || 'manual'; + document.getElementById('builder-title').textContent = '◈ EDIT MISSION — ' + esc(m.name); + document.getElementById('mb-run-btn').style.display = ''; + document.getElementById('mb-del-btn').style.display = ''; + document.getElementById('mb-status').textContent = ''; + missionTriggerChange(m.trigger_config || {}); + _missionBuilderSteps = (m.steps || []).map(s => ({ + id: ++_missionBuilderStepIdx, + label: s.label || '', + job_type: s.job_type || 'ping', + payload: typeof s.job_payload === 'string' ? s.job_payload : JSON.stringify(s.job_payload||{}, null, 2), + continue_on_failure: s.continue_on_failure ? 1 : 0, + })); + _renderBuilderSteps(); + document.getElementById('mission-builder').style.display = 'block'; + document.getElementById('mission-run-history').style.display = 'none'; + document.getElementById('mission-builder').scrollIntoView({behavior:'smooth'}); +} + +function missionTriggerChange(cfg) { + const t = document.getElementById('mb-trigger')?.value; + const el = document.getElementById('mb-trigger-config'); + if (!el) return; + cfg = cfg || {}; + if (t === 'manual') { + el.innerHTML = ''; + } else if (t === 'schedule') { + el.innerHTML = `
INTERVAL (minutes)
+ `; + } else if (t === 'guardian_event') { + el.innerHTML = `
+
SEVERITY (blank=any)
+
+
EVENT TYPE (blank=any)
+
+
`; + } else if (t === 'email_keyword') { + el.innerHTML = `
+
KEYWORDS (comma-separated)
+
+
CATEGORY (blank=any)
+
+
`; + } +} + +function _readTriggerConfig() { + const t = document.getElementById('mb-trigger')?.value; + if (t === 'schedule') { + return {interval_minutes: parseInt(document.getElementById('mb-tc-interval')?.value||60)}; + } else if (t === 'guardian_event') { + return { + severity: document.getElementById('mb-tc-severity')?.value || '', + event_type: document.getElementById('mb-tc-etype')?.value || '', + }; + } else if (t === 'email_keyword') { + const kw = (document.getElementById('mb-tc-keywords')?.value||'').split(',').map(s=>s.trim()).filter(Boolean); + return {keywords: kw, category: document.getElementById('mb-tc-category')?.value||''}; + } + return {}; +} + +function missionAddStep() { + _missionBuilderStepIdx++; + _missionBuilderSteps.push({id: _missionBuilderStepIdx, label:'', job_type:'ping', payload:'{}', continue_on_failure:0}); + _renderBuilderSteps(); +} + +function missionRemoveStep(sid) { + _missionBuilderSteps = _missionBuilderSteps.filter(s => s.id !== sid); + _renderBuilderSteps(); +} + +function missionMoveStep(sid, dir) { + const idx = _missionBuilderSteps.findIndex(s => s.id === sid); + if (idx < 0) return; + const newIdx = idx + dir; + if (newIdx < 0 || newIdx >= _missionBuilderSteps.length) return; + [_missionBuilderSteps[idx], _missionBuilderSteps[newIdx]] = [_missionBuilderSteps[newIdx], _missionBuilderSteps[idx]]; + _renderBuilderSteps(); +} + +function _renderBuilderSteps() { + const el = document.getElementById('mb-steps'); + if (!el) return; + if (!_missionBuilderSteps.length) { + el.innerHTML = '
No steps yet. Click + ADD STEP.
'; + return; + } + const typeOpts = JOB_TYPES.map(t => ``).join(''); + el.innerHTML = _missionBuilderSteps.map((s, i) => ` +
+
+ 0${i+1} + + + + + + +
+
PAYLOAD (JSON — use {{step_0.field}} for prior results)
+ +
+ `).join(''); +} + +function _stepUpdate(sid, field, value) { + const s = _missionBuilderSteps.find(x => x.id === sid); + if (s) s[field] = value; +} + +async function missionSave() { + const id = parseInt(document.getElementById('mb-mission-id')?.value || 0) || null; + const name = document.getElementById('mb-name')?.value.trim(); + const desc = document.getElementById('mb-desc')?.value.trim(); + const ttype = document.getElementById('mb-trigger')?.value; + const tcfg = _readTriggerConfig(); + const status = document.getElementById('mb-status'); + + if (!name) { if (status) status.textContent = '✗ Mission name is required'; return; } + + const steps = _missionBuilderSteps.map((s, i) => { + let payload = {}; + try { payload = JSON.parse(s.payload || '{}'); } catch(e) { payload = {}; } + return {label: s.label, job_type: s.job_type, payload, continue_on_failure: s.continue_on_failure}; + }); + + const body = {name, description: desc, trigger_type: ttype, trigger_config: tcfg, enabled: 1, steps}; + + if (status) status.textContent = '◈ SAVING…'; + const url = id ? `admin?action=mission_save&id=${id}` : 'admin?action=mission_save'; + const res = await fetch(url, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify(body), + }).then(r => r.json()).catch(() => ({error: 'request failed'})); + + if (res.ok || res.id) { + if (status) status.textContent = '◈ SAVED ✓'; + if (!id && res.id) document.getElementById('mb-mission-id').value = res.id; + document.getElementById('mb-run-btn').style.display = ''; + document.getElementById('mb-del-btn').style.display = ''; + toast('Mission saved', 'ok'); + loadMissions(); + } else { + if (status) status.textContent = '✗ Save failed: ' + (res.error || 'unknown'); + toast('Save failed: ' + (res.error || ''), 'err'); + } +} + +async function missionRunNow(id) { + const d = await api('mission_run', {id}); + if (d.run_id || d.status) { + const s = d.status || 'running'; + const color = s === 'done' ? 'ok' : s === 'failed' ? 'err' : 'ok'; + toast(`Mission run ${s} — Run #${d.run_id||'?'} (${d.steps||0} steps)`, color); + loadMissions(); + } else { + toast('Run failed: ' + (d.error || 'Arc Reactor offline'), 'err'); + } +} + +async function missionRunFromBuilder() { + const id = parseInt(document.getElementById('mb-mission-id')?.value || 0); + if (!id) { toast('Save the mission first', 'err'); return; } + const status = document.getElementById('mb-status'); + if (status) status.textContent = '◈ RUNNING…'; + await missionRunNow(id); + if (status) status.textContent = '◈ Run dispatched — check HISTORY'; + setTimeout(() => missionViewRuns(id), 1500); +} + +async function missionDeleteFromBuilder() { + const id = parseInt(document.getElementById('mb-mission-id')?.value || 0); + if (!id || !confirm('Delete this mission and all its run history?')) return; + const d = await api('mission_delete', {id}); + if (d.ok) { + toast('Mission deleted', 'ok'); + document.getElementById('mission-builder').style.display = 'none'; + document.getElementById('mission-run-history').style.display = 'none'; + loadMissions(); + } else { + toast('Delete failed', 'err'); + } +} + +async function missionViewRuns(id) { + const el = document.getElementById('mission-runs-tbl'); + const box = document.getElementById('mission-run-history'); + if (!el || !box) return; + box.style.display = 'block'; + const runs = await api('mission_runs', {id, limit: 20}); + const list = Array.isArray(runs) ? runs : []; + if (!list.length) { el.innerHTML = '
No runs yet.
'; return; } + const sColor = {done:'var(--green)', failed:'var(--red)', running:'var(--orange)', cancelled:'var(--dim)'}; + const rows = list.map(r => { + const sc = r.status || 'done'; + const ts = r.started_at ? new Date(r.started_at+'Z').toLocaleString() : '—'; + const dur = r.completed_at && r.started_at + ? Math.round((new Date(r.completed_at) - new Date(r.started_at)) / 1000) + 's' + : '—'; + return ` + #${r.id} + ${sc.toUpperCase()} + ${esc(r.trigger_source||'manual')} + ${ts} + ${dur} + + `; + }).join(''); + el.innerHTML = `${rows}
RUNSTATUSTRIGGERSTARTEDDURATION
`; + box.scrollIntoView({behavior:'smooth'}); +} + +async function missionRunDetail(runId) { + // Fetch from DB via a direct approach — get all runs and find this one + // We'll just show steps_log from the run using the mission_runs table + const res = await fetch(`admin?action=mission_runs&id=0&limit=200&run_id=${runId}`) + .then(r => r.json()).catch(() => ({})); + // Fallback: fetch all runs for the currently edited mission + const mid = parseInt(document.getElementById('mb-mission-id')?.value || 0); + const runs = mid ? await api('mission_runs', {id: mid, limit: 50}) : []; + const run = Array.isArray(runs) ? runs.find(r => r.id == runId) : null; + if (!run) { toast('Run details not available', 'err'); return; } + const steps = run.steps_log; + const list = Array.isArray(steps) ? steps : (typeof steps === 'string' ? JSON.parse(steps||'[]') : []); + const rows = list.map(s => { + const sc = s.status || 'done'; + const sc_color = sc==='done'?'var(--green)':sc==='failed'?'var(--red)':'var(--orange)'; + const result = s.result ? JSON.stringify(s.result).substring(0,120) : s.error || '—'; + return ` + ${s.step+1} + ${esc(s.label||s.job_type)} + ${esc(s.job_type)} + ${sc.toUpperCase()} + ${esc(result)} + `; + }).join(''); + openModal(`RUN #${runId} STEP LOG`, ` + + + ${rows} +
#LABELTYPESTATUSRESULT
+ `, null, null); +} + +async function missionToggle(id, enabled) { + const d = await api('mission_toggle', {id, enabled}); + if (d.ok) loadMissions(); + else toast('Toggle failed', 'err'); +} + // ── PLANNER ───────────────────────────────────────────────────────────────── const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'}; diff --git a/public_html/index.html b/public_html/index.html index 29d8232..5db7d9b 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -948,6 +948,24 @@ body::after{ .comms-compose-field{width:100%;background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:3px;padding:6px 8px;color:var(--text);font-family:var(--font-mono);font-size:0.6rem;box-sizing:border-box;margin-bottom:7px} .comms-compose-field:focus{outline:none;border-color:var(--cyan)} .comms-compose-actions{display:flex;gap:6px;margin-top:8px} +/* ── MISSION OPS HUD ─────────────────────────────────────────────── */ +.mission-card{background:rgba(0,212,255,0.03);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.mission-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} +.mission-card-head:hover{background:rgba(0,212,255,0.06)} +.mission-card-name{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.mission-card-trigger{font-family:var(--font-mono);font-size:0.5rem;padding:2px 5px;border-radius:2px;color:var(--text-dim);border:1px solid rgba(255,255,255,0.1)} +.mission-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)} +.mission-card.open .mission-card-body{display:block} +.mission-run-bar{display:flex;gap:5px;margin-top:8px} +.mission-run-btn{flex:1;background:rgba(0,212,255,0.08);border:1px solid rgba(0,212,255,0.3);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer} +.mission-run-btn:hover{background:rgba(0,212,255,0.15)} +.mission-run-btn:disabled{opacity:0.4;cursor:not-allowed} +.mission-run-item{display:flex;align-items:center;gap:6px;padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-family:var(--font-mono);font-size:0.52rem} +.mission-run-status.done{color:#00ff88} +.mission-run-status.failed{color:#ff2244} +.mission-run-status.running{color:#ffd700;animation:pulse 1.5s ease-in-out infinite} +.mission-new-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.3);border-radius:4px;padding:5px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:2px;cursor:pointer;margin-bottom:7px} +.mission-new-btn:hover{background:rgba(0,212,255,0.12)} /* ── INTEL PROTOCOL — research result cards ──────────────────────── */ .intel-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:8px;overflow:hidden} .intel-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} @@ -1174,6 +1192,7 @@ body::after{
INTEL
COMMS
GUARDIAN
+
MISSIONS
@@ -1201,6 +1220,9 @@ body::after{
+
+
+
@@ -3121,6 +3143,7 @@ function switchTab(name) { if (name === 'intel') loadIntel(); if (name === 'comms') { loadComms(); loadCommsOutbox(); } if (name === 'guardian') loadGuardian(); + if (name === 'missions') loadMissionsHud(); if (name === 'alerts') loadAlerts(); } @@ -4175,6 +4198,80 @@ async function _pollProactiveChat() { } catch(e) {} } +// ── MISSION OPS HUD ─────────────────────────────────────────────────────────── +let _missionsOpenCards = new Set(); + +async function loadMissionsHud() { + const el = document.getElementById('missions-hud'); + if (!el) return; + try { + const missions = await api('arc?action=missions'); + const list = Array.isArray(missions) ? missions : []; + + let html = ''; + + if (!list.length) { + html += '
◈ NO MISSIONS
Create workflows in Admin → Mission Ops
'; + el.innerHTML = html; + return; + } + + const trigIcons = {manual:'🖐', schedule:'⏱', guardian_event:'🛡', email_keyword:'📧'}; + for (const m of list) { + const isOpen = _missionsOpenCards.has(m.id); + const icon = trigIcons[m.trigger_type] || '◈'; + const enabled = m.enabled; + const lastRun = m.last_run_at ? new Date(m.last_run_at+'Z').toLocaleTimeString() : 'never'; + html += `
+
+ ${icon} + ${escHtml(m.name)} + ${m.trigger_type.replace('_',' ').toUpperCase()} + ${m.run_count||0} runs +
+
+ ${m.description ? `
${escHtml(m.description)}
` : ''} +
Last run: ${lastRun} · ${m.run_count||0} total runs
+
+ +
+
+
+
`; + } + el.innerHTML = html; + } catch(e) { + if (el) el.innerHTML = '
MISSIONS OFFLINE
'; + } +} + +function toggleMissionCard(id) { + const card = document.getElementById('mission-card-' + id); + if (!card) return; + if (_missionsOpenCards.has(id)) _missionsOpenCards.delete(id); + else _missionsOpenCards.add(id); + card.classList.toggle('open'); +} + +async function hudRunMission(id) { + const btn = document.getElementById('mission-run-btn-' + id); + const res = document.getElementById('mission-run-result-' + id); + if (btn) { btn.disabled = true; btn.textContent = '◈ RUNNING…'; } + if (res) res.textContent = ''; + try { + const data = await api('arc?action=mission_run&id=' + id, 'POST', {trigger_source: 'hud'}); + const s = data.status || 'done'; + const color = s === 'done' ? '#00ff88' : s === 'failed' ? '#ff2244' : '#ffd700'; + if (res) res.style.color = color; + if (res) res.textContent = `◈ ${s.toUpperCase()} — Run #${data.run_id||'?'} · ${data.steps||0} steps completed`; + if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; } + setTimeout(loadMissionsHud, 2000); + } catch(e) { + if (btn) { btn.disabled = false; btn.textContent = '▶ RUN NOW'; } + if (res) res.textContent = '✗ Run failed'; + } +} + async function loadAgents() { const [listData, metricsData] = await Promise.all([ api('agent/list'),