mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Phase 6: Comms v2 — send email, compose, schedule, meeting prep
- arc.php: comms_sent / comms_sent_get / comms_sent_delete + outbox backend - chat.php: Tier 0.9f-0.9h — send_email, compose_email, schedule_event, meeting_prep voice detection - index.html: COMMS tab SEND REPLY button, COMPOSE modal, OUTBOX section, onArcJobStarted routes comms jobs to COMMS tab - admin/index.php: OUTBOX nav + tab, send_reply/compose_email/outbox_list/outbox_delete PHP actions, outboxCompose() modal, triageSendReply() inline Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -213,6 +213,29 @@ switch ($action) {
|
|||||||
echo json_encode(arc_request('GET', '/guardian/chat' . $qs));
|
echo json_encode(arc_request('GET', '/guardian/chat' . $qs));
|
||||||
break;
|
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:
|
default:
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
echo json_encode(['error' => "Unknown arc action: {$action}"]);
|
echo json_encode(['error' => "Unknown arc action: {$action}"]);
|
||||||
|
|||||||
@@ -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) ───────────────────────────────
|
// ── Tier 1: Intent Engine (instant, no LLM) ───────────────────────────────
|
||||||
if (!$reply) {
|
if (!$reply) {
|
||||||
$matched = KBEngine::match($message);
|
$matched = KBEngine::match($message);
|
||||||
|
|||||||
+151
-2
@@ -550,6 +550,49 @@ if ($action) {
|
|||||||
$raw = curl_exec($ch); curl_close($ch);
|
$raw = curl_exec($ch); curl_close($ch);
|
||||||
j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']);
|
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 ──────────────────────────────────────────────────
|
// ── VISION PROTOCOL ──────────────────────────────────────────────────
|
||||||
case 'vision_list':
|
case 'vision_list':
|
||||||
$limit = min((int)($_GET['limit'] ?? 30), 100);
|
$limit = min((int)($_GET['limit'] ?? 30), 100);
|
||||||
@@ -876,6 +919,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
|||||||
<div class="nav-section">COMMUNICATIONS</div>
|
<div class="nav-section">COMMUNICATIONS</div>
|
||||||
<div class="nav-item" data-tab="email" onclick="nav(this)">📧 EMAIL</div>
|
<div class="nav-item" data-tab="email" onclick="nav(this)">📧 EMAIL</div>
|
||||||
<div class="nav-item" data-tab="triage" onclick="nav(this)">◈ GMAIL TRIAGE</div>
|
<div class="nav-item" data-tab="triage" onclick="nav(this)">◈ GMAIL TRIAGE</div>
|
||||||
|
<div class="nav-item" data-tab="outbox" onclick="nav(this)">◈ OUTBOX</div>
|
||||||
<div class="nav-section">PLANNER</div>
|
<div class="nav-section">PLANNER</div>
|
||||||
<div class="nav-item" data-tab="tasks" onclick="nav(this)">📋 TASKS</div>
|
<div class="nav-item" data-tab="tasks" onclick="nav(this)">📋 TASKS</div>
|
||||||
<div class="nav-item" data-tab="appointments" onclick="nav(this)">📅 APPOINTMENTS</div>
|
<div class="nav-item" data-tab="appointments" onclick="nav(this)">📅 APPOINTMENTS</div>
|
||||||
@@ -1264,6 +1308,23 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
|||||||
<div class="tbl-wrap" id="triage-tbl"><div class="loading">LOADING TRIAGE DATA...</div></div>
|
<div class="tbl-wrap" id="triage-tbl"><div class="loading">LOADING TRIAGE DATA...</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- OUTBOX -->
|
||||||
|
<div class="tab" id="tab-outbox">
|
||||||
|
<div class="page-title">◈ COMMS OUTBOX — SENT & QUEUED</div>
|
||||||
|
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-sm btn-green" onclick="outboxCompose()">+ COMPOSE</button>
|
||||||
|
<button class="btn btn-sm" onclick="loadOutbox()">↻ REFRESH</button>
|
||||||
|
<select id="outbox-status" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadOutbox()">
|
||||||
|
<option value="">ALL</option>
|
||||||
|
<option value="sent">SENT</option>
|
||||||
|
<option value="queued">QUEUED</option>
|
||||||
|
<option value="failed">FAILED</option>
|
||||||
|
</select>
|
||||||
|
<div id="outbox-count" style="margin-left:auto;font-family:var(--mono);font-size:0.65rem;color:var(--dim)"></div>
|
||||||
|
</div>
|
||||||
|
<div class="tbl-wrap" id="outbox-tbl"><div class="loading">LOADING OUTBOX...</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div><!-- /content -->
|
</div><!-- /content -->
|
||||||
</div><!-- /main -->
|
</div><!-- /main -->
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
@@ -1376,6 +1437,7 @@ function loadTab(tab) {
|
|||||||
users: loadUsers,
|
users: loadUsers,
|
||||||
email: ()=>{ loadEmailInbox(); loadEmailActionItems(); },
|
email: ()=>{ loadEmailInbox(); loadEmailActionItems(); },
|
||||||
triage: loadTriage,
|
triage: loadTriage,
|
||||||
|
outbox: loadOutbox,
|
||||||
vision: loadVision,
|
vision: loadVision,
|
||||||
guardian: loadGuardian,
|
guardian: loadGuardian,
|
||||||
tasks: loadTasks,
|
tasks: loadTasks,
|
||||||
@@ -2573,7 +2635,8 @@ async function loadTriage() {
|
|||||||
const catColor = _TRIAGE_COLORS[it.category] || 'var(--text-dim)';
|
const catColor = _TRIAGE_COLORS[it.category] || 'var(--text-dim)';
|
||||||
const catBadge = `<span style="color:${catColor};font-weight:700">${(it.category||'').toUpperCase()}</span>`;
|
const catBadge = `<span style="color:${catColor};font-weight:700">${(it.category||'').toUpperCase()}</span>`;
|
||||||
const hasDraft = it.draft_reply && it.draft_reply.trim().length > 5;
|
const hasDraft = it.draft_reply && it.draft_reply.trim().length > 5;
|
||||||
const draftBtn = hasDraft ? `<button class="btn btn-xs" style="border-color:var(--cyan);color:var(--cyan)" onclick="triageViewDraft(${it.id})">VIEW DRAFT</button> ` : '';
|
const draftBtn = hasDraft ? `<button class="btn btn-xs" style="border-color:var(--cyan);color:var(--cyan)" onclick="triageViewDraft(${it.id})">VIEW DRAFT</button> ` : '';
|
||||||
|
const sendBtn = hasDraft ? `<button class="btn btn-xs btn-green" onclick="triageSendReply(${it.id})">◈ SEND</button> ` : '';
|
||||||
return `<tr>
|
return `<tr>
|
||||||
<td style="width:60px">${catBadge}</td>
|
<td style="width:60px">${catBadge}</td>
|
||||||
<td style="width:30px;text-align:center;color:${it.priority>=8?'var(--red)':it.priority>=5?'var(--orange)':'var(--text-dim)'}">${it.priority||0}</td>
|
<td style="width:30px;text-align:center;color:${it.priority>=8?'var(--red)':it.priority>=5?'var(--orange)':'var(--text-dim)'}">${it.priority||0}</td>
|
||||||
@@ -2581,7 +2644,7 @@ async function loadTriage() {
|
|||||||
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(it.subject||'')}</td>
|
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(it.subject||'')}</td>
|
||||||
<td style="max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:0.62rem">${esc(it.summary||'')}</td>
|
<td style="max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:0.62rem">${esc(it.summary||'')}</td>
|
||||||
<td style="white-space:nowrap">
|
<td style="white-space:nowrap">
|
||||||
${draftBtn}
|
${sendBtn}${draftBtn}
|
||||||
<button class="btn btn-xs btn-green" onclick="triageMarkDone(${it.id})">✓ DONE</button>
|
<button class="btn btn-xs btn-green" onclick="triageMarkDone(${it.id})">✓ DONE</button>
|
||||||
<button class="btn btn-xs" onclick="triageDismiss(${it.id})">✗</button>
|
<button class="btn btn-xs" onclick="triageDismiss(${it.id})">✗</button>
|
||||||
</td>
|
</td>
|
||||||
@@ -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 = '<div class="loading">No messages in outbox.</div>'; 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 `<tr>
|
||||||
|
<td style="color:${sCol};font-size:0.6rem;font-weight:700">${sc.toUpperCase()}</td>
|
||||||
|
<td style="max-width:140px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.65rem">${esc(m.to_email||m.to_name||'')}</td>
|
||||||
|
<td style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(m.subject||'(no subject)')}</td>
|
||||||
|
<td style="font-size:0.6rem;color:var(--dim)">${m.account||'gmail'}</td>
|
||||||
|
<td style="font-size:0.6rem;color:var(--dim)">${ts}</td>
|
||||||
|
<td style="white-space:nowrap">
|
||||||
|
<button class="btn btn-xs" onclick="outboxViewBody(${m.id})">VIEW</button>
|
||||||
|
<button class="btn btn-xs btn-red" onclick="outboxDelete(${m.id})">DEL</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}).join('');
|
||||||
|
el.innerHTML = `<table><thead><tr><th>STATUS</th><th>TO</th><th>SUBJECT</th><th>ACCOUNT</th><th>SENT AT</th><th>ACTIONS</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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||''), `
|
||||||
|
<div style="font-family:var(--mono);font-size:0.65rem;color:var(--text-dim);margin-bottom:8px">TO: ${esc(m.to_email||'')} · ACCOUNT: ${m.account||'gmail'}</div>
|
||||||
|
<pre style="font-size:0.65rem;white-space:pre-wrap;max-height:300px;overflow-y:auto;background:rgba(0,212,255,0.03);border:1px solid var(--border);border-radius:3px;padding:8px">${esc(m.body||'(no body)')}</pre>
|
||||||
|
`, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function outboxCompose() {
|
||||||
|
openModal('COMPOSE MESSAGE', `
|
||||||
|
<div style="display:flex;flex-direction:column;gap:8px">
|
||||||
|
<select id="oc-account" class="inp" style="padding:4px 8px;font-size:0.65rem">
|
||||||
|
<option value="gmail">Gmail</option>
|
||||||
|
<option value="icloud">iCloud</option>
|
||||||
|
</select>
|
||||||
|
<input id="oc-to" class="inp" placeholder="To: email address" type="email">
|
||||||
|
<input id="oc-subject" class="inp" placeholder="Subject">
|
||||||
|
<textarea id="oc-body" class="inp" rows="5" style="resize:vertical" placeholder="Describe what to say — AI will draft the full message"></textarea>
|
||||||
|
</div>
|
||||||
|
`, 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 ─────────────────────────────────────────────────────────────────
|
// ── PLANNER ─────────────────────────────────────────────────────────────────
|
||||||
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
|
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
|
||||||
|
|
||||||
|
|||||||
+197
-9
@@ -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{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-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-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 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{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}
|
.intel-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
|
||||||
@@ -1175,6 +1195,8 @@ body::after{
|
|||||||
</div>
|
</div>
|
||||||
<div id="tab-comms" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
<div id="tab-comms" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
||||||
<div id="comms-list"><div class="loading-shimmer"></div></div>
|
<div id="comms-list"><div class="loading-shimmer"></div></div>
|
||||||
|
<div class="comms-section-label" style="margin:12px 4px 6px">◈ OUTBOX — SENT & QUEUED</div>
|
||||||
|
<div id="comms-outbox"><div class="loading-shimmer"></div></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-guardian" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
<div id="tab-guardian" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
||||||
<div id="guardian-list"><div class="loading-shimmer"></div></div>
|
<div id="guardian-list"><div class="loading-shimmer"></div></div>
|
||||||
@@ -3097,7 +3119,7 @@ function switchTab(name) {
|
|||||||
if (name === 'news') loadNews();
|
if (name === 'news') loadNews();
|
||||||
if (name === 'agents') loadAgents();
|
if (name === 'agents') loadAgents();
|
||||||
if (name === 'intel') loadIntel();
|
if (name === 'intel') loadIntel();
|
||||||
if (name === 'comms') loadComms();
|
if (name === 'comms') { loadComms(); loadCommsOutbox(); }
|
||||||
if (name === 'guardian') loadGuardian();
|
if (name === 'guardian') loadGuardian();
|
||||||
if (name === 'alerts') loadAlerts();
|
if (name === 'alerts') loadAlerts();
|
||||||
}
|
}
|
||||||
@@ -3721,8 +3743,8 @@ function intelPrompt() {
|
|||||||
|
|
||||||
// Called when arc_job is returned from chat response
|
// Called when arc_job is returned from chat response
|
||||||
function onArcJobStarted(jobId, jobType) {
|
function onArcJobStarted(jobId, jobType) {
|
||||||
if (jobType === 'arc:gmail_triage') {
|
const commsTypes = ['arc:gmail_triage', 'arc:send_email', 'arc:compose_email', 'arc:schedule_event', 'arc:meeting_prep'];
|
||||||
// Route to COMMS tab
|
if (commsTypes.includes(jobType)) {
|
||||||
const commsBtn = document.getElementById('tab-btn-comms');
|
const commsBtn = document.getElementById('tab-btn-comms');
|
||||||
if (commsBtn) commsBtn.click();
|
if (commsBtn) commsBtn.click();
|
||||||
startCommsPolling();
|
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 catOrder = {urgent:0, action:1, reply:2, meeting:3, info:4, promo:5, spam:6};
|
||||||
const catIcons = {urgent:'🔴', action:'⚡', reply:'◈', meeting:'📅', info:'ℹ', promo:'📢', spam:'🗑'};
|
const catIcons = {urgent:'🔴', action:'⚡', reply:'◈', meeting:'📅', info:'ℹ', promo:'📢', spam:'🗑'};
|
||||||
|
|
||||||
let html = '<button class="comms-triage-btn" onclick="commsTriageNow()">◈ TRIAGE INBOX NOW</button>';
|
let html = '<div style="display:flex;gap:5px;margin-bottom:5px">';
|
||||||
|
html += '<button class="comms-triage-btn" style="flex:3;margin-bottom:0" onclick="commsTriageNow()">◈ TRIAGE INBOX</button>';
|
||||||
|
html += '<button class="comms-compose-btn" style="flex:2;margin-bottom:0" onclick="commsShowCompose()">+ COMPOSE</button>';
|
||||||
|
html += '</div>';
|
||||||
html += '<div class="comms-header-bar">';
|
html += '<div class="comms-header-bar">';
|
||||||
for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) {
|
for (const [f, label] of [['priority','PRIORITY'],['urgent','URGENT'],['action','ACTION'],['all','ALL']]) {
|
||||||
html += `<div class="comms-filter-btn${_commsFilter===f?' active':''}" onclick="commsSetFilter('${f}')">${label}</div>`;
|
html += `<div class="comms-filter-btn${_commsFilter===f?' active':''}" onclick="commsSetFilter('${f}')">${label}</div>`;
|
||||||
@@ -3780,9 +3805,10 @@ async function loadComms() {
|
|||||||
<div class="comms-card-body">
|
<div class="comms-card-body">
|
||||||
<div class="comms-card-from">FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}</div>
|
<div class="comms-card-from">FROM: ${escHtml((item.from_name||item.from_email||'').substring(0,50))}</div>
|
||||||
<div class="comms-card-summary">${escHtml(item.summary||'')}</div>
|
<div class="comms-card-summary">${escHtml(item.summary||'')}</div>
|
||||||
${hasReply ? `<div class="comms-draft-label">DRAFT REPLY</div><div class="comms-draft">${escHtml(item.draft_reply)}</div>` : ''}
|
${hasReply ? `<div class="comms-draft-label">DRAFT REPLY</div><div class="comms-draft" id="comms-draft-${item.id}">${escHtml(item.draft_reply)}</div>` : ''}
|
||||||
<div style="display:flex;gap:5px;margin-top:8px">
|
<div style="display:flex;gap:5px;margin-top:8px">
|
||||||
${hasReply ? `<button onclick="commsCopyReply(${item.id})" style="flex:1;background:rgba(0,212,255,0.08);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--cyan);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">COPY REPLY</button>` : ''}
|
${hasReply ? `<button class="comms-send-btn" id="comms-send-${item.id}" onclick="commsSendReply(${item.id})">◈ SEND REPLY</button>` : ''}
|
||||||
|
${hasReply ? `<button onclick="commsCopyReply(${item.id})" style="flex:1;background:rgba(0,212,255,0.05);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">COPY</button>` : ''}
|
||||||
<button onclick="commsDismiss(${item.id})" style="flex:1;background:rgba(255,255,255,0.03);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">DISMISS</button>
|
<button onclick="commsDismiss(${item.id})" style="flex:1;background:rgba(255,255,255,0.03);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">DISMISS</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3815,11 +3841,173 @@ async function commsDismiss(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function commsCopyReply(id) {
|
async function commsCopyReply(id) {
|
||||||
const draft = document.querySelector(`#comms-card-${id} .comms-draft`);
|
const draft = document.querySelector(`#comms-draft-${id}`);
|
||||||
if (draft) {
|
if (draft) {
|
||||||
navigator.clipboard.writeText(draft.innerText).catch(() => {});
|
navigator.clipboard.writeText(draft.innerText).catch(() => {});
|
||||||
const btn = document.querySelector(`#comms-card-${id} [onclick*="commsCopyReply"]`);
|
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 = `
|
||||||
|
<div class="comms-compose-inner">
|
||||||
|
<div class="comms-compose-title">◈ COMPOSE MESSAGE</div>
|
||||||
|
<select id="cc-account" class="comms-compose-field" style="cursor:pointer">
|
||||||
|
<option value="gmail">Gmail</option>
|
||||||
|
<option value="icloud">iCloud</option>
|
||||||
|
</select>
|
||||||
|
<input id="cc-to" class="comms-compose-field" placeholder="To: email address" type="email">
|
||||||
|
<input id="cc-subject" class="comms-compose-field" placeholder="Subject">
|
||||||
|
<textarea id="cc-instructions" class="comms-compose-field" rows="4" placeholder="Describe what to say (AI will draft it)"></textarea>
|
||||||
|
<div id="cc-preview" style="display:none">
|
||||||
|
<div class="comms-draft-label">DRAFTED MESSAGE</div>
|
||||||
|
<div class="comms-draft" id="cc-preview-body" style="max-height:200px"></div>
|
||||||
|
</div>
|
||||||
|
<div class="comms-compose-actions">
|
||||||
|
<button class="comms-send-btn" style="flex:1" onclick="commsComposeDraft()">◈ DRAFT</button>
|
||||||
|
<button class="comms-send-btn" style="flex:1;display:none" id="cc-send-btn" onclick="commsComposeAndSend()">◈ SEND NOW</button>
|
||||||
|
<button onclick="document.getElementById('comms-compose-modal').remove()" style="flex:1;background:rgba(255,255,255,0.03);border:1px solid var(--panel-border);border-radius:3px;padding:3px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer">CANCEL</button>
|
||||||
|
</div>
|
||||||
|
<div id="cc-status" style="font-family:var(--font-mono);font-size:0.55rem;color:var(--cyan);margin-top:6px;min-height:14px"></div>
|
||||||
|
</div>`;
|
||||||
|
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 = '<div class="comms-empty" style="padding:10px">No sent messages yet</div>';
|
||||||
|
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 += `<div class="comms-outbox-card">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
|
<div class="comms-outbox-to">TO: ${escHtml((m.to_email||'').substring(0,40))}</div>
|
||||||
|
<span class="comms-outbox-status ${sc}">${sc.toUpperCase()}</span>
|
||||||
|
</div>
|
||||||
|
<div class="comms-outbox-subj">${escHtml((m.subject||'(no subject)').substring(0,60))}</div>
|
||||||
|
<div style="font-family:var(--font-mono);font-size:0.48rem;color:var(--text-dim)">${ts} · ${m.account||'gmail'}</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
} catch(e) {
|
||||||
|
el.innerHTML = '<div class="comms-empty" style="padding:10px">OUTBOX OFFLINE</div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3831,7 +4019,7 @@ function commsTriageNow() {
|
|||||||
function startCommsPolling() {
|
function startCommsPolling() {
|
||||||
if (_commsPollTimer) return;
|
if (_commsPollTimer) return;
|
||||||
_commsPollTimer = setInterval(() => {
|
_commsPollTimer = setInterval(() => {
|
||||||
if (document.getElementById('tab-comms')?.classList.contains('active')) loadComms();
|
if (document.getElementById('tab-comms')?.classList.contains('active')) { loadComms(); loadCommsOutbox(); }
|
||||||
}, 8000);
|
}, 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user