mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
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:
@@ -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)'};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user