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:
2026-06-11 05:05:00 +00:00
parent f15225994a
commit 8229f52b8b
4 changed files with 465 additions and 11 deletions
+151 -2
View File
@@ -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)}
<div class="nav-section">COMMUNICATIONS</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="outbox" onclick="nav(this)">◈ OUTBOX</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="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>
<!-- OUTBOX -->
<div class="tab" id="tab-outbox">
<div class="page-title">◈ COMMS OUTBOX — SENT &amp; 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><!-- /main -->
</div><!-- /app -->
@@ -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 = `<span style="color:${catColor};font-weight:700">${(it.category||'').toUpperCase()}</span>`;
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>
<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>
@@ -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: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">
${draftBtn}
${sendBtn}${draftBtn}
<button class="btn btn-xs btn-green" onclick="triageMarkDone(${it.id})">✓ DONE</button>
<button class="btn btn-xs" onclick="triageDismiss(${it.id})">✗</button>
</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 ─────────────────────────────────────────────────────────────────
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};