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:
+151
-2
@@ -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 & 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)'};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user