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)}
⚡ ARC REACTOR
◈ VISION PROTOCOL
◈ GUARDIAN MODE
+ ◈ MISSION OPS
INFO
SITES
USERS
@@ -1325,6 +1391,73 @@ select.filter-sel:focus{border-color:var(--cyan)}
+
+
+
◈ MISSION OPS — AUTOMATED WORKFLOWS
+
+
+
+
+
+
+
+
+
+
+
+
+
◈ MISSION BUILDER
+
+
+
+
+
+
+
TRIGGER
+
+
+
+
+
+
+
+
DESCRIPTION (optional)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 = `| NAME | TRIGGER | STATUS | LAST RUN | ACTIONS |
${rows}
`;
+}
+
+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)
+
+
+
`;
+ } else if (t === 'email_keyword') {
+ el.innerHTML = `
+
+
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 = `| RUN | STATUS | TRIGGER | STARTED | DURATION | |
${rows}
`;
+ 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`, `
+
+ | # | LABEL | TYPE | STATUS | RESULT |
+ ${rows}
+
+ `, 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'),