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
This commit is contained in:
2026-06-11 04:33:43 +00:00
parent 9ea43c852b
commit 068aff27b4
4 changed files with 457 additions and 26 deletions
+163
View File
@@ -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)}
<div class="nav-item" data-tab="vms" onclick="nav(this)">PROXMOX VMs</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="triage" onclick="nav(this)">◈ GMAIL TRIAGE</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>
@@ -1032,6 +1085,28 @@ select.filter-sel:focus{border-color:var(--cyan)}
<div class="tbl-wrap" id="arc-jobs-tbl"><div class="loading">INITIALIZING...</div></div>
</div>
<!-- GMAIL TRIAGE -->
<div class="tab" id="tab-triage">
<div class="page-title">◈ COMMS PROTOCOL — GMAIL TRIAGE</div>
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
<button class="btn btn-sm btn-green" onclick="triageRunNow('gmail')">⚡ TRIAGE GMAIL</button>
<button class="btn btn-sm" onclick="triageRunNow('icloud')">⚡ TRIAGE ICLOUD</button>
<button class="btn btn-sm" onclick="loadTriage()">↻ REFRESH</button>
<select id="triage-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadTriage()">
<option value="priority">PRIORITY</option>
<option value="action">ACTION NEEDED</option>
<option value="urgent">URGENT ONLY</option>
<option value="all">ALL</option>
</select>
<div id="triage-count" style="margin-left:auto;font-family:var(--mono);font-size:0.65rem;color:var(--dim)"></div>
</div>
<div id="triage-summary" style="display:none;margin-bottom:12px;padding:10px 14px;background:rgba(0,212,255,0.04);border:1px solid var(--border);border-radius:4px;font-family:var(--mono);font-size:0.65rem;display:flex;gap:20px;flex-wrap:wrap"></div>
<div class="tbl-wrap" id="triage-tbl"><div class="loading">LOADING TRIAGE DATA...</div></div>
</div>
</div><!-- /content -->
</div><!-- /main -->
</div><!-- /app -->
@@ -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 = '<div class="loading">LOADING...</div>';
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 = `<span style="color:var(--red)">URGENT: ${counts.urgent||0}</span>`
+ `<span style="color:var(--orange)">ACTION: ${counts.action||0}</span>`
+ `<span style="color:var(--cyan)">REPLY: ${counts.reply||0}</span>`
+ `<span style="color:#a78bfa">MEETING: ${counts.meeting||0}</span>`
+ `<span style="color:var(--text-dim);margin-left:auto">PENDING: ${counts.pending||0}</span>`;
}
if (!items.length) {
el.innerHTML = '<div class="loading">No triage items matching filter. Run a triage to populate.</div>';
return;
}
const rows = items.map(it => {
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> ` : '';
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>
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:0.65rem">${esc(it.from_name||it.from_email||'')}</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="white-space:nowrap">
${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>
</tr>`;
}).join('');
el.innerHTML = `<table><thead><tr>
<th>CATEGORY</th><th>PRI</th><th>FROM</th><th>SUBJECT</th><th>SUMMARY</th><th>ACTIONS</th>
</tr></thead><tbody>${rows}</tbody></table>`;
}
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||''), `
<div style="font-size:0.65rem;color:var(--text-dim);margin-bottom:10px">FROM: ${esc(item.from_name||item.from_email||'')} · PRIORITY: ${item.priority}/10</div>
<div style="font-size:0.65rem;color:var(--text);margin-bottom:10px;padding:8px;background:rgba(0,212,255,0.04);border:1px solid var(--border);border-radius:3px">${esc(item.summary||'')}</div>
<div style="font-size:0.55rem;letter-spacing:2px;color:var(--dim);margin-bottom:6px">DRAFT REPLY</div>
<textarea id="triage-draft-edit" style="width:100%;height:160px;background:#060a0e;border:1px solid var(--border);color:var(--text);padding:8px;font-size:0.65rem;resize:vertical;border-radius:3px">${esc(item.draft_reply||'')}</textarea>
`, 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)'};