diff --git a/api/endpoints/arc.php b/api/endpoints/arc.php index 831a45e..ac77f42 100644 --- a/api/endpoints/arc.php +++ b/api/endpoints/arc.php @@ -213,6 +213,29 @@ switch ($action) { echo json_encode(arc_request('GET', '/guardian/chat' . $qs)); break; + // ── COMMS v2 ────────────────────────────────────────────────────────────── + // GET /api/arc?action=comms_sent&limit=50&status=sent + case 'comms_sent': + $limit = min((int)($_GET['limit'] ?? 50), 200); + $status = $_GET['status'] ?? ''; + $qs = http_build_query(array_filter(['limit' => $limit, 'status' => $status])); + echo json_encode(arc_request('GET', '/comms/sent' . ($qs ? "?{$qs}" : ''))); + break; + + // GET /api/arc?action=comms_sent_get&id=123 + case 'comms_sent_get': + $id = (int)($_GET['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('GET', "/comms/sent/{$id}")); + break; + + // DELETE /api/arc?action=comms_sent_delete&id=123 + case 'comms_sent_delete': + $id = (int)($_GET['id'] ?? 0); + if (!$id) { http_response_code(400); echo json_encode(['error' => 'Missing id']); break; } + echo json_encode(arc_request('DELETE', "/comms/sent/{$id}")); + 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 17b6b07..28bdae9 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -1261,6 +1261,100 @@ if (!$reply) { } } +// ── Tier 0.9f: Comms v2 — send_email, compose_email ───────────────────────── +if (!$reply) { + // "reply to [name/id] saying..." or "send [name] a reply..." + if (preg_match('/^(?:jarvis[,\s]+)?(?:reply\s+to|send\s+(?:a\s+)?reply\s+to)\s+(.+?)\s+(?:saying|that|:)\s+(.+)/i', $message, $m)) { + $target = trim($m[1]); + $content = trim($m[2]); + $arcRes = arcSubmitJob('send_email', [ + 'target' => $target, + 'content' => $content, + ], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $reply = "◈ COMMS PROTOCOL — Sending reply to **{$target}** (Job #{$arcJobId}). Drafting and transmitting now, {$userAddr}."; + $source = 'arc:send_email'; + } else { + $reply = "Comms Protocol is offline, {$userAddr}. Arc Reactor may be unavailable."; + $source = 'arc:offline'; + } + } + // "send email to [name] about [subject]" or "compose email to..." + elseif (preg_match('/^(?:jarvis[,\s]+)?(?:send\s+(?:an?\s+)?email\s+to|compose\s+(?:an?\s+)?email\s+to|write\s+(?:an?\s+)?email\s+to|draft\s+(?:an?\s+)?email\s+to)\s+(.+?)(?:\s+(?:about|regarding|re:?)\s+(.+))?$/i', $message, $m)) { + $recipient = trim($m[1]); + $instructions = isset($m[2]) ? trim($m[2]) : $message; + $arcRes = arcSubmitJob('compose_email', [ + 'recipient' => $recipient, + 'instructions' => $instructions, + 'auto_send' => false, + ], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $reply = "◈ COMMS PROTOCOL — Composing email to **{$recipient}** (Job #{$arcJobId}). I'll draft it and show you before sending, {$userAddr}. Check the COMMS tab."; + $source = 'arc:compose_email'; + } else { + $reply = "Comms Protocol is offline, {$userAddr}."; + $source = 'arc:offline'; + } + } +} + +// ── Tier 0.9g: Comms v2 — schedule_event ───────────────────────────────────── +if (!$reply) { + $schedulePatterns = [ + '/^(?:jarvis[,\s]+)?(?:schedule|book|set\s+up|create)\s+(?:a\s+)?(?:meeting|call|appointment|event|session)\s+(?:with|for|about)?\s*(.+)/i', + '/^(?:jarvis[,\s]+)?(?:add\s+(?:a\s+)?(?:meeting|call|appointment|event)\s+(?:to\s+my\s+calendar)?\s*(?:with|for|about)?\s*(.+))/i', + '/^(?:jarvis[,\s]+)?(?:put\s+(?:a\s+)?(?:meeting|call|appointment)\s+(?:on\s+(?:my\s+)?calendar|in\s+my\s+schedule)(?:\s+(?:with|for|about)?\s+(.+))?)/i', + ]; + foreach ($schedulePatterns as $pat) { + if (preg_match($pat, $message, $m)) { + $details = trim($m[1] ?? $message); + $arcRes = arcSubmitJob('schedule_event', [ + 'request' => $message, + 'details' => $details, + 'provider' => 'claude', + ], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $reply = "◈ SCHEDULING PROTOCOL — Processing your calendar request (Job #{$arcJobId}). I'm parsing the details and creating the appointment now, {$userAddr}."; + $source = 'arc:schedule_event'; + } else { + $reply = "Scheduling Protocol is offline, {$userAddr}. Arc Reactor may be unavailable."; + $source = 'arc:offline'; + } + break; + } + } +} + +// ── Tier 0.9h: Comms v2 — meeting_prep ──────────────────────────────────────── +if (!$reply) { + $meetingPrepPatterns = [ + '/^(?:jarvis[,\s]+)?(?:prep(?:are)?(?:\s+me)?\s+for|brief(?:ing)?\s+(?:me\s+)?(?:for|on|about)?)\s+(?:my\s+)?(?:next\s+)?(?:meeting|call|appointment)/i', + '/^(?:jarvis[,\s]+)?(?:what(?:\'s|\s+do\s+i\s+need\s+to\s+know)?\s+(?:is\s+)?(?:my\s+)?(?:next\s+)?meeting)/i', + '/^(?:jarvis[,\s]+)?(?:meeting\s+prep|pre[- ]meeting\s+(?:brief|notes|prep))/i', + '/^(?:jarvis[,\s]+)?(?:get\s+(?:me\s+)?ready\s+for\s+my\s+(?:next\s+)?(?:meeting|call))/i', + ]; + foreach ($meetingPrepPatterns as $pat) { + if (preg_match($pat, $message)) { + $arcRes = arcSubmitJob('meeting_prep', [ + 'provider' => 'claude', + 'research' => true, + ], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $reply = "◈ MISSION PROTOCOL — Preparing your meeting briefing (Job #{$arcJobId}). I'm pulling your next appointment details and researching the participants, {$userAddr}. Stand by."; + $source = 'arc:meeting_prep'; + } else { + $reply = "Mission Protocol is offline, {$userAddr}. Arc Reactor may be unavailable."; + $source = 'arc:offline'; + } + break; + } + } +} + // ── Tier 1: Intent Engine (instant, no LLM) ─────────────────────────────── if (!$reply) { $matched = KBEngine::match($message); diff --git a/public_html/admin/index.php b/public_html/admin/index.php index 2181dbd..819cdf0 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -550,6 +550,49 @@ if ($action) { $raw = curl_exec($ch); curl_close($ch); j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']); + // ── OUTBOX ──────────────────────────────────────────────────────────── + case 'outbox_list': + $limit = min((int)($_GET['limit'] ?? 50), 200); + $status = $_GET['status'] ?? ''; + $qs = http_build_query(array_filter(['limit' => $limit, 'status' => $status])); + $ch = curl_init('http://127.0.0.1:7474/comms/sent' . ($qs ? "?{$qs}" : '')); + curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]); + $raw = curl_exec($ch); curl_close($ch); + j(json_decode($raw, true) ?: []); + + case 'outbox_delete': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $ch = curl_init('http://127.0.0.1:7474/comms/sent/' . $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) ?: ['error'=>'failed']); + + case 'send_reply': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing triage id'); + $content = $_GET['content'] ?? ''; + $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'=>'send_email','payload'=>['triage_id'=>$id,'content'=>$content],'priority'=>8,'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 'compose_email': + $to = $_GET['to'] ?? ''; if (!$to) bad('Missing recipient'); + $subject = $_GET['subject'] ?? ''; + $body = $_GET['body'] ?? ''; + $account = $_GET['account'] ?? 'gmail'; + $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'=>'compose_email','payload'=>['recipient'=>$to,'subject'=>$subject,'instructions'=>$body,'account'=>$account,'auto_send'=>false],'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']); + // ── VISION PROTOCOL ────────────────────────────────────────────────── case 'vision_list': $limit = min((int)($_GET['limit'] ?? 30), 100); @@ -876,6 +919,7 @@ select.filter-sel:focus{border-color:var(--cyan)} + @@ -1264,6 +1308,23 @@ select.filter-sel:focus{border-color:var(--cyan)}
LOADING TRIAGE DATA...
+ +
+
◈ COMMS OUTBOX — SENT & QUEUED
+
+ + + +
+
+
LOADING OUTBOX...
+
+ @@ -1376,6 +1437,7 @@ function loadTab(tab) { users: loadUsers, email: ()=>{ loadEmailInbox(); loadEmailActionItems(); }, triage: loadTriage, + outbox: loadOutbox, vision: loadVision, guardian: loadGuardian, tasks: loadTasks, @@ -2573,7 +2635,8 @@ async function loadTriage() { 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 ? ` ` : ''; + const draftBtn = hasDraft ? ` ` : ''; + const sendBtn = hasDraft ? ` ` : ''; return ` ${catBadge} ${it.priority||0} @@ -2581,7 +2644,7 @@ async function loadTriage() { ${esc(it.subject||'')} ${esc(it.summary||'')} - ${draftBtn} + ${sendBtn}${draftBtn} @@ -2627,6 +2690,92 @@ function triageViewDraft(id) { }); } +async function triageSendReply(id) { + if (!confirm('Send the drafted reply for this email now?')) return; + const d = await api('send_reply', {id}); + if (d.job_id) { + toast('Send job dispatched — Job #' + d.job_id, 'ok'); + setTimeout(() => { loadTriage(); loadOutbox(); }, 3000); + } else { + toast('Send failed: ' + (d.error || 'Arc Reactor offline'), 'err'); + } +} + +// ── OUTBOX ─────────────────────────────────────────────────────────────────── +async function loadOutbox() { + const el = document.getElementById('outbox-tbl'); + if (!el) return; + const status = document.getElementById('outbox-status')?.value || ''; + const d = await api('outbox_list', {limit: 100, status}); + const sent = Array.isArray(d) ? d : (d.sent || []); + document.getElementById('outbox-count').textContent = sent.length + ' MESSAGES'; + if (!sent.length) { el.innerHTML = '
No messages in outbox.
'; return; } + const statusColor = {sent:'var(--green)',failed:'var(--red)',queued:'var(--orange)'}; + const rows = sent.map(m => { + const sc = m.status || 'sent'; + const ts = m.sent_at ? new Date(m.sent_at + 'Z').toLocaleString() : '—'; + const sCol = statusColor[sc] || 'var(--text-dim)'; + return ` + ${sc.toUpperCase()} + ${esc(m.to_email||m.to_name||'')} + ${esc(m.subject||'(no subject)')} + ${m.account||'gmail'} + ${ts} + + + + + `; + }).join(''); + el.innerHTML = `${rows}
STATUSTOSUBJECTACCOUNTSENT ATACTIONS
`; +} + +async function outboxDelete(id) { + if (!confirm('Delete this sent message record?')) return; + const d = await api('outbox_delete', {id}); + if (d.ok || d.deleted) { toast('Deleted', 'ok'); loadOutbox(); } + else toast('Delete failed: ' + (d.error||''), 'err'); +} + +async function outboxViewBody(id) { + const d = await api('outbox_list', {limit: 200}); + const sent = Array.isArray(d) ? d : (d.sent || []); + const m = sent.find(x => x.id == id); + if (!m) return; + openModal('SENT MESSAGE — ' + esc(m.subject||''), ` +
TO: ${esc(m.to_email||'')} · ACCOUNT: ${m.account||'gmail'}
+
${esc(m.body||'(no body)')}
+ `, null, null); +} + +function outboxCompose() { + openModal('COMPOSE MESSAGE', ` +
+ + + + +
+ `, async () => { + const to = document.getElementById('oc-to')?.value.trim(); + const subject = document.getElementById('oc-subject')?.value.trim(); + const body = document.getElementById('oc-body')?.value.trim(); + const account = document.getElementById('oc-account')?.value; + if (!to || !body) { toast('Please fill To and message description', 'err'); return; } + const d = await api('compose_email', {to, subject, body, account}); + if (d.job_id) { + toast('Compose job dispatched — Job #' + d.job_id, 'ok'); + closeModal(); + setTimeout(loadOutbox, 5000); + } else { + toast('Failed: ' + (d.error||'Arc Reactor offline'), 'err'); + } + }, 'DISPATCH'); +} + // ── 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 6dd5007..29d8232 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -928,6 +928,26 @@ body::after{ .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} +.comms-section-label{font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--text-dim);border-bottom:1px solid var(--panel-border);padding:6px 0 4px;margin:8px 0 6px} +.comms-compose-btn{width:100%;background:rgba(0,212,255,0.08);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:5px} +.comms-compose-btn:hover{background:rgba(0,212,255,0.15)} +.comms-send-btn{flex:1;background:rgba(0,212,255,0.1);border:1px solid rgba(0,212,255,0.4);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer} +.comms-send-btn:hover{background:rgba(0,212,255,0.2)} +.comms-send-btn:disabled{opacity:0.4;cursor:not-allowed} +.comms-outbox-card{background:rgba(0,212,255,0.03);border:1px solid rgba(0,212,255,0.1);border-radius:3px;padding:6px 9px;margin-bottom:5px} +.comms-outbox-to{font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)} +.comms-outbox-subj{font-family:var(--font-display);font-size:0.55rem;color:var(--cyan);margin:2px 0} +.comms-outbox-status{font-family:var(--font-mono);font-size:0.48rem;padding:1px 4px;border-radius:2px} +.comms-outbox-status.sent{color:#00ff88;border:1px solid rgba(0,255,136,0.3)} +.comms-outbox-status.failed{color:#ff2244;border:1px solid rgba(255,34,68,0.3)} +.comms-outbox-status.queued{color:#ffd700;border:1px solid rgba(255,215,0,0.3)} +/* compose modal */ +.comms-compose-modal{position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9000} +.comms-compose-inner{background:#0a0e14;border:1px solid var(--cyan);border-radius:6px;padding:16px;width:min(90vw,480px);max-height:80vh;overflow-y:auto} +.comms-compose-title{font-family:var(--font-display);font-size:0.65rem;letter-spacing:2px;color:var(--cyan);margin-bottom:12px} +.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} /* ── 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} @@ -1175,6 +1195,8 @@ body::after{
+
◈ OUTBOX — SENT & QUEUED
+
@@ -3097,7 +3119,7 @@ function switchTab(name) { if (name === 'news') loadNews(); if (name === 'agents') loadAgents(); if (name === 'intel') loadIntel(); - if (name === 'comms') loadComms(); + if (name === 'comms') { loadComms(); loadCommsOutbox(); } if (name === 'guardian') loadGuardian(); if (name === 'alerts') loadAlerts(); } @@ -3721,8 +3743,8 @@ function intelPrompt() { // Called when arc_job is returned from chat response function onArcJobStarted(jobId, jobType) { - if (jobType === 'arc:gmail_triage') { - // Route to COMMS tab + const commsTypes = ['arc:gmail_triage', 'arc:send_email', 'arc:compose_email', 'arc:schedule_event', 'arc:meeting_prep']; + if (commsTypes.includes(jobType)) { const commsBtn = document.getElementById('tab-btn-comms'); if (commsBtn) commsBtn.click(); startCommsPolling(); @@ -3757,7 +3779,10 @@ async function loadComms() { 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 = ''; + let html = '
'; + html += ''; + html += ''; + html += '
'; html += '
'; for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) { html += `
${label}
`; @@ -3780,9 +3805,10 @@ async function loadComms() {
FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}
${escHtml(item.summary||'')}
- ${hasReply ? `
DRAFT REPLY
${escHtml(item.draft_reply)}
` : ''} + ${hasReply ? `
DRAFT REPLY
${escHtml(item.draft_reply)}
` : ''}
- ${hasReply ? `` : ''} + ${hasReply ? `` : ''} + ${hasReply ? `` : ''}
@@ -3815,11 +3841,173 @@ async function commsDismiss(id) { } async function commsCopyReply(id) { - const draft = document.querySelector(`#comms-card-${id} .comms-draft`); + const draft = document.querySelector(`#comms-draft-${id}`); 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); } + if (btn) { btn.textContent = 'COPIED!'; setTimeout(() => btn.textContent = 'COPY', 1500); } + } +} + +async function commsSendReply(id) { + const btn = document.getElementById('comms-send-' + id); + const draft = document.getElementById('comms-draft-' + id); + if (!btn || !draft) return; + btn.disabled = true; + btn.textContent = '◈ SENDING…'; + try { + const res = await api('arc', 'POST', { + action: 'job_create', + type: 'send_email', + payload: { triage_id: id, content: draft.innerText }, + priority: 8, + }); + if (res.job_id) { + btn.textContent = '◈ SENT ✓'; + btn.style.color = '#00ff88'; + setTimeout(() => loadComms(), 3000); + loadCommsOutbox(); + } else { + btn.disabled = false; + btn.textContent = '◈ SEND REPLY'; + alert('Send failed: ' + (res.error || 'unknown error')); + } + } catch(e) { + btn.disabled = false; + btn.textContent = '◈ SEND REPLY'; + } +} + +function commsShowCompose() { + const existing = document.getElementById('comms-compose-modal'); + if (existing) existing.remove(); + const modal = document.createElement('div'); + modal.className = 'comms-compose-modal'; + modal.id = 'comms-compose-modal'; + modal.innerHTML = ` +
+
◈ COMPOSE MESSAGE
+ + + + + +
+ + + +
+
+
`; + document.body.appendChild(modal); + modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); }); +} + +let _ccDraftedBody = ''; + +async function commsComposeDraft() { + const to = document.getElementById('cc-to')?.value.trim(); + const subject = document.getElementById('cc-subject')?.value.trim(); + const instructions = document.getElementById('cc-instructions')?.value.trim(); + const account = document.getElementById('cc-account')?.value; + const status = document.getElementById('cc-status'); + if (!to || !instructions) { if (status) status.textContent = 'Please fill in To and message description.'; return; } + if (status) status.textContent = '◈ DRAFTING…'; + try { + const res = await api('arc', 'POST', { + action: 'job_create', type: 'compose_email', + payload: { recipient: to, subject, instructions, account, auto_send: false }, + priority: 7, + }); + if (!res.job_id) throw new Error(res.error || 'No job'); + // poll for result + let attempts = 0; + const poll = async () => { + const job = await api('arc?action=job_get&id=' + res.job_id); + if (job.status === 'done' && job.result?.drafted_body) { + _ccDraftedBody = job.result.drafted_body; + document.getElementById('cc-preview-body').textContent = _ccDraftedBody; + document.getElementById('cc-preview').style.display = 'block'; + document.getElementById('cc-send-btn').style.display = ''; + if (status) status.textContent = '◈ DRAFT READY — Review and send'; + } else if (job.status === 'failed') { + if (status) status.textContent = '✗ Draft failed: ' + (job.error || 'unknown'); + } else if (attempts++ < 20) { + setTimeout(poll, 1500); + } else { + if (status) status.textContent = '◈ Job still running — check INTEL tab'; + } + }; + setTimeout(poll, 1500); + } catch(e) { + if (status) status.textContent = '✗ Error: ' + e.message; + } +} + +async function commsComposeAndSend() { + const to = document.getElementById('cc-to')?.value.trim(); + const subject = document.getElementById('cc-subject')?.value.trim(); + const account = document.getElementById('cc-account')?.value; + const status = document.getElementById('cc-status'); + const btn = document.getElementById('cc-send-btn'); + if (!to || !_ccDraftedBody) return; + if (btn) { btn.disabled = true; btn.textContent = '◈ SENDING…'; } + if (status) status.textContent = '◈ TRANSMITTING…'; + try { + const res = await api('arc', 'POST', { + action: 'job_create', type: 'send_email', + payload: { to_email: to, subject, body: _ccDraftedBody, account }, + priority: 9, + }); + if (res.job_id) { + if (status) status.textContent = '◈ SENT ✓ (Job #' + res.job_id + ')'; + setTimeout(() => { + document.getElementById('comms-compose-modal')?.remove(); + loadCommsOutbox(); + }, 1500); + } else { + if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; } + if (status) status.textContent = '✗ Send failed: ' + (res.error || 'unknown'); + } + } catch(e) { + if (btn) { btn.disabled = false; btn.textContent = '◈ SEND NOW'; } + if (status) status.textContent = '✗ Error: ' + e.message; + } +} + +async function loadCommsOutbox() { + const el = document.getElementById('comms-outbox'); + if (!el) return; + try { + const data = await api('arc?action=comms_sent&limit=20'); + const sent = Array.isArray(data) ? data : (data.sent || []); + if (!sent.length) { + el.innerHTML = '
No sent messages yet
'; + return; + } + const statusColor = {sent:'#00ff88', failed:'#ff2244', queued:'#ffd700'}; + let html = ''; + for (const m of sent) { + const ts = m.sent_at ? new Date(m.sent_at + 'Z').toLocaleString() : '—'; + const sc = m.status || 'sent'; + html += `
+
+
TO: ${escHtml((m.to_email||'').substring(0,40))}
+ ${sc.toUpperCase()} +
+
${escHtml((m.subject||'(no subject)').substring(0,60))}
+
${ts} · ${m.account||'gmail'}
+
`; + } + el.innerHTML = html; + } catch(e) { + el.innerHTML = '
OUTBOX OFFLINE
'; } } @@ -3831,7 +4019,7 @@ function commsTriageNow() { function startCommsPolling() { if (_commsPollTimer) return; _commsPollTimer = setInterval(() => { - if (document.getElementById('tab-comms')?.classList.contains('active')) loadComms(); + if (document.getElementById('tab-comms')?.classList.contains('active')) { loadComms(); loadCommsOutbox(); } }, 8000); }