From 068aff27b4bc80ac26fa5aa04aa6353514bf4af7 Mon Sep 17 00:00:00 2001 From: Myron Blair Date: Thu, 11 Jun 2026 04:33:43 +0000 Subject: [PATCH] Phase 3: Comms Protocol + Field Protocol - chat.php: Add Tier 0.9a (gmail_triage), Tier 0.9b (remote_exec) detection; refactor arc submit into arcSubmitJob() helper; natural-language triggers for email triage (check my email, triage inbox) and remote exec (restart X on Y, run X on Y, get logs from X on Y) - arc.php: Add triage and triage_action endpoints (read/update email_triage table) - index.html: Add COMMS tab with triage card UI (filter bar, category badges, draft reply viewer, copy/dismiss actions); loadComms() with 8s polling; onArcJobStarted() routes gmail_triage jobs to COMMS tab - admin/index.php: Add GMAIL TRIAGE section under COMMUNICATIONS nav; triage_list/ triage_action/triage_run PHP actions; loadTriage() JS with full table + draft modal; triageRunNow() submits gmail_triage job to Arc Reactor --- api/endpoints/arc.php | 54 +++++++++++- api/endpoints/chat.php | 114 ++++++++++++++++++++----- public_html/admin/index.php | 163 ++++++++++++++++++++++++++++++++++++ public_html/index.html | 152 +++++++++++++++++++++++++++++++-- 4 files changed, 457 insertions(+), 26 deletions(-) diff --git a/api/endpoints/arc.php b/api/endpoints/arc.php index 9afe6fc..0db0db3 100644 --- a/api/endpoints/arc.php +++ b/api/endpoints/arc.php @@ -49,7 +49,6 @@ switch ($action) { break; // POST /api/arc — create a job - // body: { action: "job_create", type: "ping", payload: {}, priority: 5 } case 'job_create': $type = $data['type'] ?? ''; $payload = $data['payload'] ?? []; @@ -102,6 +101,59 @@ switch ($action) { echo json_encode($result); break; + // GET /api/arc?action=triage&limit=50&filter=priority + // Returns email_triage rows for the COMMS tab + case 'triage': + $limit = min((int)($_GET['limit'] ?? 50), 100); + $filter = $_GET['filter'] ?? 'priority'; + + if ($filter === 'urgent') { + $sql = "SELECT id, account, from_name, from_email, subject, date_received, + category, priority, summary, draft_reply, action_taken, created_at + FROM email_triage + WHERE action_taken != 'dismissed' AND category = 'urgent' + ORDER BY priority DESC, created_at DESC LIMIT ?"; + } elseif ($filter === 'action') { + $sql = "SELECT id, account, from_name, from_email, subject, date_received, + category, priority, summary, draft_reply, action_taken, created_at + FROM email_triage + WHERE action_taken != 'dismissed' AND category IN ('urgent','action','reply','meeting') + ORDER BY priority DESC, created_at DESC LIMIT ?"; + } elseif ($filter === 'priority') { + $sql = "SELECT id, account, from_name, from_email, subject, date_received, + category, priority, summary, draft_reply, action_taken, created_at + FROM email_triage + WHERE action_taken != 'dismissed' AND category IN ('urgent','action','reply','meeting') + AND priority >= 5 + ORDER BY priority DESC, created_at DESC LIMIT ?"; + } else { + $sql = "SELECT id, account, from_name, from_email, subject, date_received, + category, priority, summary, draft_reply, action_taken, created_at + FROM email_triage + WHERE action_taken != 'dismissed' + ORDER BY priority DESC, created_at DESC LIMIT ?"; + } + $rows = JarvisDB::query($sql, [$limit]); + echo json_encode($rows ?: []); + break; + + // POST /api/arc?action=triage_action&id=123 body: { action: "dismissed"|"replied"|"done" } + case 'triage_action': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + $actionTaken = $data['action'] ?? 'dismissed'; + $allowed = ['dismissed', 'replied', 'done', 'snoozed']; + if (!$id || !in_array($actionTaken, $allowed)) { + http_response_code(400); + echo json_encode(['error' => 'Invalid id or action']); + break; + } + JarvisDB::execute( + "UPDATE email_triage SET action_taken = ? WHERE id = ?", + [$actionTaken, $id] + ); + echo json_encode(['ok' => true, 'id' => $id, 'action_taken' => $actionTaken]); + break; + default: http_response_code(404); echo json_encode(['error' => "Unknown arc action: {$action}"]); diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index 3f561bd..925f3e2 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1063,10 +1063,102 @@ if (!$reply && preg_match('/\b(news|headlines|latest|what.?s happening|current e } } -// ── Tier 0.9: Intel Protocol — research & tool_loop detection ──────────── +// ── Tier 0.9: Intel Protocol — research, tool_loop, gmail_triage, remote_exec ─ $arcJobId = null; -// Detect "research X", "look up X", "deep dive X", "investigate X", "find out about X" +// Helper: submit job to Arc Reactor +function arcSubmitJob(string $type, array $payload, string $sessionId): ?array { + $ch = curl_init('http://127.0.0.1:7474/job'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode([ + 'type' => $type, + 'payload' => $payload, + 'priority' => 7, + 'created_by' => 'chat:' . $sessionId, + ]), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_TIMEOUT => 5, + CURLOPT_CONNECTTIMEOUT => 3, + ]); + $res = json_decode(curl_exec($ch), true); + curl_close($ch); + return $res; +} + +// ── Tier 0.9a: Comms Protocol — gmail_triage detection ──────────────────── +if (!$reply) { + $triagePatterns = [ + '/^(?:jarvis[,\s]+)?(?:check|triage|sort)\s+(?:my\s+)?(?:email|inbox|gmail|mail)/i', + '/^(?:jarvis[,\s]+)?what(?:\'s|\s+is)\s+(?:urgent|important)\s+(?:in\s+my\s+)?(?:email|inbox|mail)/i', + '/^(?:jarvis[,\s]+)?(?:any\s+)?(?:urgent|important)\s+(?:email|emails|messages)/i', + '/^(?:jarvis[,\s]+)?email\s+(?:briefing|brief|report|summary)/i', + ]; + foreach ($triagePatterns as $pat) { + if (preg_match($pat, $message)) { + $account = preg_match('/icloud/i', $message) ? 'icloud' : 'gmail'; + $maxEmails = 20; + if (preg_match('/\b(\d+)\s+emails?\b/i', $message, $em)) $maxEmails = min((int)$em[1], 40); + $arcRes = arcSubmitJob('gmail_triage', [ + 'account' => $account, + 'max_emails' => $maxEmails, + 'provider' => 'claude', + ], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $acctLabel = strtoupper($account); + $reply = "◈ COMMS PROTOCOL ACTIVATED — Triaging your {$acctLabel} inbox (Job #{$arcJobId}). I'm fetching emails and running priority analysis now, {$userAddr}. Switch to the COMMS tab to see results."; + $source = 'arc:gmail_triage'; + } else { + $reply = "Comms Protocol is offline, {$userAddr}. Arc Reactor may be unavailable."; + $source = 'arc:offline'; + } + break; + } + } +} + +// ── Tier 0.9b: Field Protocol — remote_exec detection ──────────────────── +if (!$reply) { + $remotePatterns = [ + '/^(?:jarvis[,\s]+)?restart\s+(.+?)\s+on\s+(.+)/i' => ['restart_service', 1, 2], + '/^(?:jarvis[,\s]+)?(?:run|execute|exec)\s+(.+?)\s+on\s+(.+)/i' => ['shell', 1, 2], + '/^(?:jarvis[,\s]+)?get\s+logs?\s+(?:from\s+|for\s+)?(.+?)\s+on\s+(.+)/i' => ['get_logs', 1, 2], + '/^(?:jarvis[,\s]+)?(?:ping|check)\s+agent\s+(.+)/i' => ['ping', 1, null], + '/^(?:jarvis[,\s]+)?what(?:\'s|\s+is)\s+(?:running|the\s+status)\s+on\s+(.+)/i' => ['ping', 1, null], + ]; + foreach ($remotePatterns as $pat => $cfg) { + if (preg_match($pat, $message, $m)) { + [$cmdType, $cmdIdx, $agentIdx] = $cfg; + $cmdArg = $agentIdx ? trim($m[$cmdIdx]) : ''; + $agentArg = $agentIdx ? trim($m[$agentIdx]) : trim($m[$cmdIdx]); + $cmdData = match($cmdType) { + 'restart_service' => ['service' => $cmdArg], + 'shell' => ['command' => $cmdArg], + 'get_logs' => ['service' => $cmdArg, 'lines' => 50], + default => [], + }; + $arcRes = arcSubmitJob('remote_exec', [ + 'agent' => $agentArg, + 'command_type' => $cmdType, + 'command_data' => $cmdData, + 'timeout' => 35, + ], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $reply = "◈ FIELD PROTOCOL ACTIVATED — Dispatching **{$cmdType}** to agent **{$agentArg}** (Job #{$arcJobId}). I'll relay the result when the field station responds, {$userAddr}."; + $source = 'arc:remote_exec'; + } else { + $reply = "Field Protocol is offline, {$userAddr}. Arc Reactor may be unavailable."; + $source = 'arc:offline'; + } + break; + } + } +} + +// ── Tier 0.9c: Intel Protocol — research & tool_loop detection ──────────── $intelPatterns = [ '/^(?:jarvis[,\s]+)?(?:research|investigate|deep[- ]dive|deep dive)\s+(.+)/i' => 'research', '/^(?:jarvis[,\s]+)?(?:look\s+(?:up|into)|find\s+out\s+(?:about)?)\s+(.+)/i' => 'research', @@ -1088,23 +1180,7 @@ if (!$reply) { ? ['query' => $queryOrTask, 'depth' => $depth, 'provider' => 'claude'] : ['task' => $queryOrTask, 'max_iterations' => 12, 'provider' => 'claude']; - // Submit to Arc Reactor - $arcCh = curl_init('http://127.0.0.1:7474/job'); - curl_setopt_array($arcCh, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => json_encode([ - 'type' => $jobType, - 'payload' => $jobPayload, - 'priority' => 7, - 'created_by' => 'chat:' . $sessionId, - ]), - CURLOPT_HTTPHEADER => ['Content-Type: application/json'], - CURLOPT_TIMEOUT => 5, - CURLOPT_CONNECTTIMEOUT => 3, - ]); - $arcRes = json_decode(curl_exec($arcCh), true); - curl_close($arcCh); + $arcRes = arcSubmitJob($jobType, $jobPayload, $sessionId); if (isset($arcRes['job_id'])) { $arcJobId = $arcRes['job_id']; diff --git a/public_html/admin/index.php b/public_html/admin/index.php index c44b353..21acc89 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -498,6 +498,58 @@ if ($action) { $raw = curl_exec($ch); curl_close($ch); j(json_decode($raw, true) ?: ['ok'=>true]); + // ── GMAIL TRIAGE ────────────────────────────────────────────────────── + case 'triage_list': + $limit = min((int)($_GET['limit'] ?? 100), 200); + $filter = $_GET['filter'] ?? 'priority'; + if ($filter === 'urgent') { + $rows = JarvisDB::query( + "SELECT * FROM email_triage WHERE action_taken != 'dismissed' AND category = 'urgent' ORDER BY priority DESC, created_at DESC LIMIT ?", + [$limit] + ); + } elseif ($filter === 'action') { + $rows = JarvisDB::query( + "SELECT * FROM email_triage WHERE action_taken != 'dismissed' AND category IN ('urgent','action','reply','meeting') ORDER BY priority DESC, created_at DESC LIMIT ?", + [$limit] + ); + } elseif ($filter === 'priority') { + $rows = JarvisDB::query( + "SELECT * FROM email_triage WHERE action_taken != 'dismissed' AND category IN ('urgent','action','reply','meeting') AND priority >= 5 ORDER BY priority DESC, created_at DESC LIMIT ?", + [$limit] + ); + } else { + $rows = JarvisDB::query( + "SELECT * FROM email_triage ORDER BY priority DESC, created_at DESC LIMIT ?", + [$limit] + ); + } + $counts = JarvisDB::single("SELECT COUNT(*) AS total, + SUM(category='urgent') AS urgent, SUM(category='action') AS action, + SUM(category='reply') AS reply, SUM(category='meeting') AS meeting, + SUM(action_taken='none') AS pending + FROM email_triage WHERE action_taken != 'dismissed'"); + j(['items' => $rows ?: [], 'counts' => $counts]); + + case 'triage_action': + $id = (int)($_GET['id'] ?? $_POST['id'] ?? 0); if (!$id) bad('Missing id'); + $act = $_POST['action'] ?? $_GET['action_val'] ?? 'dismissed'; + $allowed = ['dismissed','replied','done','snoozed']; + if (!in_array($act, $allowed)) bad('Invalid action'); + JarvisDB::execute("UPDATE email_triage SET action_taken = ? WHERE id = ?", [$act, $id]); + j(['ok' => true]); + + case 'triage_run': + $account = $_GET['account'] ?? 'gmail'; + $maxEmails = (int)($_GET['max'] ?? 25); + $ch = curl_init('http://127.0.0.1:7474/job'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 5, CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode(['type'=>'gmail_triage','payload'=>['account'=>$account,'max_emails'=>$maxEmails,'provider'=>'claude'],'priority'=>7,'created_by'=>'admin']), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']); + case 'users_list': j(JarvisDB::query('SELECT id,username,display_name,last_seen,created_at FROM users ORDER BY username')); @@ -730,6 +782,7 @@ select.filter-sel:focus{border-color:var(--cyan)} + @@ -1032,6 +1085,28 @@ select.filter-sel:focus{border-color:var(--cyan)}
INITIALIZING...
+ +
+
◈ COMMS PROTOCOL — GMAIL TRIAGE
+ +
+ + + + +
+
+ + + +
LOADING TRIAGE DATA...
+
+ @@ -1143,9 +1218,11 @@ function loadTab(tab) { sites: loadSites, users: loadUsers, email: ()=>{ loadEmailInbox(); loadEmailActionItems(); }, + triage: loadTriage, tasks: loadTasks, appointments: loadAppts, calendar: loadCalFeeds, + arc: loadArc, })[tab]?.(); } @@ -2033,6 +2110,92 @@ function emailDismiss(id) { apiPost('email_dismiss',{id},()=>{ toast('Dismissed','ok'); loadEmailActionItems(); }); } +// ── GMAIL TRIAGE ───────────────────────────────────────────────────────────── +const _TRIAGE_COLORS = {urgent:'var(--red)',action:'var(--orange)',reply:'var(--cyan)',meeting:'#a78bfa',info:'var(--text-dim)',promo:'rgba(255,255,255,0.25)',spam:'rgba(255,255,255,0.15)'}; + +async function loadTriage() { + const el = document.getElementById('triage-tbl'); + if (!el) return; + el.innerHTML = '
LOADING...
'; + const filter = document.getElementById('triage-filter')?.value || 'priority'; + const d = await api('triage_list', {filter, limit: 100}); + const items = d.items || []; + const counts = d.counts || {}; + + document.getElementById('triage-count').textContent = `${items.length} ITEMS`; + const sumEl = document.getElementById('triage-summary'); + if (counts && sumEl) { + sumEl.style.display = 'flex'; + sumEl.innerHTML = `URGENT: ${counts.urgent||0}` + + `ACTION: ${counts.action||0}` + + `REPLY: ${counts.reply||0}` + + `MEETING: ${counts.meeting||0}` + + `PENDING: ${counts.pending||0}`; + } + + if (!items.length) { + el.innerHTML = '
No triage items matching filter. Run a triage to populate.
'; + return; + } + + const rows = items.map(it => { + const catColor = _TRIAGE_COLORS[it.category] || 'var(--text-dim)'; + const catBadge = `${(it.category||'').toUpperCase()}`; + const hasDraft = it.draft_reply && it.draft_reply.trim().length > 5; + const draftBtn = hasDraft ? ` ` : ''; + return ` + ${catBadge} + ${it.priority||0} + ${esc(it.from_name||it.from_email||'')} + ${esc(it.subject||'')} + ${esc(it.summary||'')} + + ${draftBtn} + + + + `; + }).join(''); + + el.innerHTML = ` + + ${rows}
CATEGORYPRIFROMSUBJECTSUMMARYACTIONS
`; +} + +async function triageRunNow(account = 'gmail') { + const d = await api('triage_run', {account, max: 25}); + if (d.job_id) { + toast('Triage job started — Job #' + d.job_id, 'ok'); + setTimeout(() => loadTriage(), 3000); + } else { + toast('Failed to start triage: ' + (d.error || 'Arc Reactor offline'), 'err'); + } +} + +async function triageDismiss(id) { + await apiPost('triage_action', {id, action: 'dismissed'}, () => loadTriage()); +} + +async function triageMarkDone(id) { + await apiPost('triage_action', {id, action: 'done'}, () => { toast('Marked done', 'ok'); loadTriage(); }); +} + +function triageViewDraft(id) { + api('triage_list', {filter: 'all', limit: 200}).then(d => { + const item = (d.items || []).find(i => i.id == id); + if (!item) return; + openModal('DRAFT REPLY — ' + esc(item.subject||''), ` +
FROM: ${esc(item.from_name||item.from_email||'')} · PRIORITY: ${item.priority}/10
+
${esc(item.summary||'')}
+
DRAFT REPLY
+ + `, async () => { + await navigator.clipboard.writeText(document.getElementById('triage-draft-edit')?.value || '').catch(() => {}); + await apiPost('triage_action', {id, action: 'replied'}, () => { toast('Copied & marked replied', 'ok'); loadTriage(); }); + }, 'COPY & MARK REPLIED'); + }); +} + // ── 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 e3ba141..8821e10 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -875,6 +875,31 @@ body::after{ transition:filter 1.2s ease; } #app.sleeping #sleepOverlay{display:flex} +/* ── COMMS PROTOCOL — email triage cards ─────────────────────────── */ +.comms-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.comms-card-head{display:flex;align-items:center;gap:7px;padding:7px 10px;cursor:pointer;user-select:none} +.comms-card-head:hover{background:rgba(0,212,255,0.06)} +.comms-card-subject{font-family:var(--font-display);font-size:0.58rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.comms-card-cat{font-family:var(--font-mono);font-size:0.52rem;padding:2px 5px;border-radius:2px;flex-shrink:0;text-transform:uppercase;letter-spacing:1px} +.comms-card-cat.urgent{color:#ff2244;border:1px solid rgba(255,34,68,0.4);animation:pulse 1.5s ease-in-out infinite} +.comms-card-cat.action{color:#ffd700;border:1px solid rgba(255,215,0,0.4)} +.comms-card-cat.reply{color:var(--cyan);border:1px solid rgba(0,212,255,0.3)} +.comms-card-cat.meeting{color:#a78bfa;border:1px solid rgba(167,139,250,0.4)} +.comms-card-cat.info{color:var(--text-dim);border:1px solid rgba(255,255,255,0.1)} +.comms-card-cat.promo,.comms-card-cat.spam{color:rgba(255,255,255,0.25);border:1px solid rgba(255,255,255,0.08)} +.comms-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)} +.comms-card.open .comms-card-body{display:block} +.comms-card-from{font-family:var(--font-mono);font-size:0.55rem;color:var(--text-dim);margin:7px 0 3px} +.comms-card-summary{font-size:0.62rem;line-height:1.5;color:var(--text);margin:5px 0} +.comms-draft-label{font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--text-dim);margin:8px 0 4px} +.comms-draft{font-size:0.6rem;line-height:1.5;color:rgba(0,212,255,0.7);background:rgba(0,212,255,0.04);border:1px solid rgba(0,212,255,0.15);border-radius:3px;padding:7px 9px;white-space:pre-wrap;max-height:160px;overflow-y:auto} +.comms-empty{text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px} +.comms-header-bar{display:flex;gap:5px;margin-bottom:7px;flex-wrap:wrap} +.comms-filter-btn{flex:1;min-width:50px;background:rgba(0,212,255,0.05);border:1px solid var(--panel-border);border-radius:3px;padding:4px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer;text-align:center} +.comms-filter-btn.active,.comms-filter-btn:hover{background:rgba(0,212,255,0.12);color:var(--cyan);border-color:var(--cyan)} +.comms-triage-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid var(--panel-border);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} +.comms-triage-btn:hover{background:rgba(0,212,255,0.12)} +.comms-prio{font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);flex-shrink:0} /* ── 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} @@ -1099,6 +1124,7 @@ body::after{
AGENTS
SITES
INTEL
+
COMMS
@@ -1118,6 +1144,9 @@ body::after{
+
+
+
@@ -3023,6 +3052,7 @@ function switchTab(name) { if (name === 'news') loadNews(); if (name === 'agents') loadAgents(); if (name === 'intel') loadIntel(); + if (name === 'comms') loadComms(); if (name === 'alerts') loadAlerts(); } @@ -3643,15 +3673,125 @@ function intelPrompt() { if (input) { input.value = 'research '; input.focus(); } } -// Called from chat.js when arc_job is returned +// Called when arc_job is returned from chat response function onArcJobStarted(jobId, jobType) { - _intelActiveJobs.add(jobId); - // Auto-switch to INTEL tab - const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]'); - if (intelTab) intelTab.click(); - startIntelPolling(); + if (jobType === 'arc:gmail_triage') { + // Route to COMMS tab + const commsBtn = document.getElementById('tab-btn-comms'); + if (commsBtn) commsBtn.click(); + startCommsPolling(); + } else { + _intelActiveJobs.add(jobId); + const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]'); + if (intelTab) intelTab.click(); + startIntelPolling(); + } } +// ── COMMS PROTOCOL — email triage HUD ──────────────────────────────────── +let _commsPollTimer = null; +let _commsFilter = 'priority'; +let _commsOpenCards = new Set(); + +async function loadComms() { + const el = document.getElementById('comms-list'); + if (!el) return; + + try { + const res = await api('arc?action=triage&limit=50&filter=' + _commsFilter); + const items = Array.isArray(res) ? res : (res.items || []); + + if (!items.length) { + el.innerHTML = '' + + '
◈ NO TRIAGE DATA
Say "check my email" to activate
'; + stopCommsPolling(); + return; + } + + const catOrder = {urgent:0, action:1, reply:2, meeting:3, info:4, promo:5, spam:6}; + const catIcons = {urgent:'🔴', action:'⚡', reply:'◈', meeting:'📅', info:'ℹ', promo:'📢', spam:'🗑'}; + + let html = ''; + html += '
'; + for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) { + html += `
${label}
`; + } + html += '
'; + + for (const item of items) { + const cat = item.category || 'info'; + const icon = catIcons[cat] || '◈'; + const prio = item.priority || 0; + const isOpen = _commsOpenCards.has(item.id); + const hasReply = item.draft_reply && item.draft_reply.trim().length > 5; + + html += `
+
+ ${icon} ${cat.toUpperCase()} + ${escHtml((item.subject||'(no subject)').substring(0,60))} + ${prio}/10 +
+
+
FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}
+
${escHtml(item.summary||'')}
+ ${hasReply ? `
DRAFT REPLY
${escHtml(item.draft_reply)}
` : ''} +
+ ${hasReply ? `` : ''} + +
+
+
`; + } + + el.innerHTML = html; + + } catch(e) { + if (el) el.innerHTML = '
COMMS OFFLINE
'; + } +} + +function toggleCommsCard(id) { + const card = document.getElementById('comms-card-' + id); + if (!card) return; + if (_commsOpenCards.has(id)) _commsOpenCards.delete(id); + else _commsOpenCards.add(id); + card.classList.toggle('open'); +} + +function commsSetFilter(f) { + _commsFilter = f; + loadComms(); +} + +async function commsDismiss(id) { + await api('arc?action=triage_action&id=' + id, 'POST', {action: 'dismissed'}).catch(() => {}); + loadComms(); +} + +async function commsCopyReply(id) { + const draft = document.querySelector(`#comms-card-${id} .comms-draft`); + if (draft) { + navigator.clipboard.writeText(draft.innerText).catch(() => {}); + const btn = document.querySelector(`#comms-card-${id} [onclick*="commsCopyReply"]`); + if (btn) { btn.textContent = 'COPIED!'; setTimeout(() => btn.textContent = 'COPY REPLY', 1500); } + } +} + +function commsTriageNow() { + const input = document.getElementById('textInput'); + if (input) { input.value = 'check my email'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); } +} + +function startCommsPolling() { + if (_commsPollTimer) return; + _commsPollTimer = setInterval(() => { + if (document.getElementById('tab-comms')?.classList.contains('active')) loadComms(); + }, 8000); +} + +function stopCommsPolling() { + if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; } +} async function loadAgents() { const [listData, metricsData] = await Promise.all([