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:
+53
-1
@@ -49,7 +49,6 @@ switch ($action) {
|
||||
break;
|
||||
|
||||
// POST /api/arc — create a job
|
||||
// body: { action: "job_create", type: "ping", payload: {}, priority: 5 }
|
||||
case 'job_create':
|
||||
$type = $data['type'] ?? '';
|
||||
$payload = $data['payload'] ?? [];
|
||||
@@ -102,6 +101,59 @@ switch ($action) {
|
||||
echo json_encode($result);
|
||||
break;
|
||||
|
||||
// GET /api/arc?action=triage&limit=50&filter=priority
|
||||
// Returns email_triage rows for the COMMS tab
|
||||
case 'triage':
|
||||
$limit = min((int)($_GET['limit'] ?? 50), 100);
|
||||
$filter = $_GET['filter'] ?? 'priority';
|
||||
|
||||
if ($filter === 'urgent') {
|
||||
$sql = "SELECT id, account, from_name, from_email, subject, date_received,
|
||||
category, priority, summary, draft_reply, action_taken, created_at
|
||||
FROM email_triage
|
||||
WHERE action_taken != 'dismissed' AND category = 'urgent'
|
||||
ORDER BY priority DESC, created_at DESC LIMIT ?";
|
||||
} elseif ($filter === 'action') {
|
||||
$sql = "SELECT id, account, from_name, from_email, subject, date_received,
|
||||
category, priority, summary, draft_reply, action_taken, created_at
|
||||
FROM email_triage
|
||||
WHERE action_taken != 'dismissed' AND category IN ('urgent','action','reply','meeting')
|
||||
ORDER BY priority DESC, created_at DESC LIMIT ?";
|
||||
} elseif ($filter === 'priority') {
|
||||
$sql = "SELECT id, account, from_name, from_email, subject, date_received,
|
||||
category, priority, summary, draft_reply, action_taken, created_at
|
||||
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 ?";
|
||||
} else {
|
||||
$sql = "SELECT id, account, from_name, from_email, subject, date_received,
|
||||
category, priority, summary, draft_reply, action_taken, created_at
|
||||
FROM email_triage
|
||||
WHERE action_taken != 'dismissed'
|
||||
ORDER BY priority DESC, created_at DESC LIMIT ?";
|
||||
}
|
||||
$rows = JarvisDB::query($sql, [$limit]);
|
||||
echo json_encode($rows ?: []);
|
||||
break;
|
||||
|
||||
// POST /api/arc?action=triage_action&id=123 body: { action: "dismissed"|"replied"|"done" }
|
||||
case 'triage_action':
|
||||
$id = (int)($_GET['id'] ?? $data['id'] ?? 0);
|
||||
$actionTaken = $data['action'] ?? 'dismissed';
|
||||
$allowed = ['dismissed', 'replied', 'done', 'snoozed'];
|
||||
if (!$id || !in_array($actionTaken, $allowed)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid id or action']);
|
||||
break;
|
||||
}
|
||||
JarvisDB::execute(
|
||||
"UPDATE email_triage SET action_taken = ? WHERE id = ?",
|
||||
[$actionTaken, $id]
|
||||
);
|
||||
echo json_encode(['ok' => true, 'id' => $id, 'action_taken' => $actionTaken]);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(404);
|
||||
echo json_encode(['error' => "Unknown arc action: {$action}"]);
|
||||
|
||||
+95
-19
@@ -1063,10 +1063,102 @@ if (!$reply && preg_match('/\b(news|headlines|latest|what.?s happening|current e
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tier 0.9: Intel Protocol — research & tool_loop detection ────────────
|
||||
// ── Tier 0.9: Intel Protocol — research, tool_loop, gmail_triage, remote_exec ─
|
||||
$arcJobId = null;
|
||||
|
||||
// Detect "research X", "look up X", "deep dive X", "investigate X", "find out about X"
|
||||
// Helper: submit job to Arc Reactor
|
||||
function arcSubmitJob(string $type, array $payload, string $sessionId): ?array {
|
||||
$ch = curl_init('http://127.0.0.1:7474/job');
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'type' => $type,
|
||||
'payload' => $payload,
|
||||
'priority' => 7,
|
||||
'created_by' => 'chat:' . $sessionId,
|
||||
]),
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
]);
|
||||
$res = json_decode(curl_exec($ch), true);
|
||||
curl_close($ch);
|
||||
return $res;
|
||||
}
|
||||
|
||||
// ── Tier 0.9a: Comms Protocol — gmail_triage detection ────────────────────
|
||||
if (!$reply) {
|
||||
$triagePatterns = [
|
||||
'/^(?:jarvis[,\s]+)?(?:check|triage|sort)\s+(?:my\s+)?(?:email|inbox|gmail|mail)/i',
|
||||
'/^(?:jarvis[,\s]+)?what(?:\'s|\s+is)\s+(?:urgent|important)\s+(?:in\s+my\s+)?(?:email|inbox|mail)/i',
|
||||
'/^(?:jarvis[,\s]+)?(?:any\s+)?(?:urgent|important)\s+(?:email|emails|messages)/i',
|
||||
'/^(?:jarvis[,\s]+)?email\s+(?:briefing|brief|report|summary)/i',
|
||||
];
|
||||
foreach ($triagePatterns as $pat) {
|
||||
if (preg_match($pat, $message)) {
|
||||
$account = preg_match('/icloud/i', $message) ? 'icloud' : 'gmail';
|
||||
$maxEmails = 20;
|
||||
if (preg_match('/\b(\d+)\s+emails?\b/i', $message, $em)) $maxEmails = min((int)$em[1], 40);
|
||||
$arcRes = arcSubmitJob('gmail_triage', [
|
||||
'account' => $account,
|
||||
'max_emails' => $maxEmails,
|
||||
'provider' => 'claude',
|
||||
], $sessionId);
|
||||
if (isset($arcRes['job_id'])) {
|
||||
$arcJobId = $arcRes['job_id'];
|
||||
$acctLabel = strtoupper($account);
|
||||
$reply = "◈ COMMS PROTOCOL ACTIVATED — Triaging your {$acctLabel} inbox (Job #{$arcJobId}). I'm fetching emails and running priority analysis now, {$userAddr}. Switch to the COMMS tab to see results.";
|
||||
$source = 'arc:gmail_triage';
|
||||
} else {
|
||||
$reply = "Comms Protocol is offline, {$userAddr}. Arc Reactor may be unavailable.";
|
||||
$source = 'arc:offline';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tier 0.9b: Field Protocol — remote_exec detection ────────────────────
|
||||
if (!$reply) {
|
||||
$remotePatterns = [
|
||||
'/^(?:jarvis[,\s]+)?restart\s+(.+?)\s+on\s+(.+)/i' => ['restart_service', 1, 2],
|
||||
'/^(?:jarvis[,\s]+)?(?:run|execute|exec)\s+(.+?)\s+on\s+(.+)/i' => ['shell', 1, 2],
|
||||
'/^(?:jarvis[,\s]+)?get\s+logs?\s+(?:from\s+|for\s+)?(.+?)\s+on\s+(.+)/i' => ['get_logs', 1, 2],
|
||||
'/^(?:jarvis[,\s]+)?(?:ping|check)\s+agent\s+(.+)/i' => ['ping', 1, null],
|
||||
'/^(?:jarvis[,\s]+)?what(?:\'s|\s+is)\s+(?:running|the\s+status)\s+on\s+(.+)/i' => ['ping', 1, null],
|
||||
];
|
||||
foreach ($remotePatterns as $pat => $cfg) {
|
||||
if (preg_match($pat, $message, $m)) {
|
||||
[$cmdType, $cmdIdx, $agentIdx] = $cfg;
|
||||
$cmdArg = $agentIdx ? trim($m[$cmdIdx]) : '';
|
||||
$agentArg = $agentIdx ? trim($m[$agentIdx]) : trim($m[$cmdIdx]);
|
||||
$cmdData = match($cmdType) {
|
||||
'restart_service' => ['service' => $cmdArg],
|
||||
'shell' => ['command' => $cmdArg],
|
||||
'get_logs' => ['service' => $cmdArg, 'lines' => 50],
|
||||
default => [],
|
||||
};
|
||||
$arcRes = arcSubmitJob('remote_exec', [
|
||||
'agent' => $agentArg,
|
||||
'command_type' => $cmdType,
|
||||
'command_data' => $cmdData,
|
||||
'timeout' => 35,
|
||||
], $sessionId);
|
||||
if (isset($arcRes['job_id'])) {
|
||||
$arcJobId = $arcRes['job_id'];
|
||||
$reply = "◈ FIELD PROTOCOL ACTIVATED — Dispatching **{$cmdType}** to agent **{$agentArg}** (Job #{$arcJobId}). I'll relay the result when the field station responds, {$userAddr}.";
|
||||
$source = 'arc:remote_exec';
|
||||
} else {
|
||||
$reply = "Field Protocol is offline, {$userAddr}. Arc Reactor may be unavailable.";
|
||||
$source = 'arc:offline';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tier 0.9c: Intel Protocol — research & tool_loop detection ────────────
|
||||
$intelPatterns = [
|
||||
'/^(?:jarvis[,\s]+)?(?:research|investigate|deep[- ]dive|deep dive)\s+(.+)/i' => 'research',
|
||||
'/^(?:jarvis[,\s]+)?(?:look\s+(?:up|into)|find\s+out\s+(?:about)?)\s+(.+)/i' => 'research',
|
||||
@@ -1088,23 +1180,7 @@ if (!$reply) {
|
||||
? ['query' => $queryOrTask, 'depth' => $depth, 'provider' => 'claude']
|
||||
: ['task' => $queryOrTask, 'max_iterations' => 12, 'provider' => 'claude'];
|
||||
|
||||
// Submit to Arc Reactor
|
||||
$arcCh = curl_init('http://127.0.0.1:7474/job');
|
||||
curl_setopt_array($arcCh, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode([
|
||||
'type' => $jobType,
|
||||
'payload' => $jobPayload,
|
||||
'priority' => 7,
|
||||
'created_by' => 'chat:' . $sessionId,
|
||||
]),
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_CONNECTTIMEOUT => 3,
|
||||
]);
|
||||
$arcRes = json_decode(curl_exec($arcCh), true);
|
||||
curl_close($arcCh);
|
||||
$arcRes = arcSubmitJob($jobType, $jobPayload, $sessionId);
|
||||
|
||||
if (isset($arcRes['job_id'])) {
|
||||
$arcJobId = $arcRes['job_id'];
|
||||
|
||||
@@ -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)'};
|
||||
|
||||
|
||||
+146
-6
@@ -875,6 +875,31 @@ body::after{
|
||||
transition:filter 1.2s ease;
|
||||
}
|
||||
#app.sleeping #sleepOverlay{display:flex}
|
||||
/* ── COMMS PROTOCOL — email triage cards ─────────────────────────── */
|
||||
.comms-card{background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden}
|
||||
.comms-card-head{display:flex;align-items:center;gap:7px;padding:7px 10px;cursor:pointer;user-select:none}
|
||||
.comms-card-head:hover{background:rgba(0,212,255,0.06)}
|
||||
.comms-card-subject{font-family:var(--font-display);font-size:0.58rem;letter-spacing:1px;color:var(--cyan);flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||||
.comms-card-cat{font-family:var(--font-mono);font-size:0.52rem;padding:2px 5px;border-radius:2px;flex-shrink:0;text-transform:uppercase;letter-spacing:1px}
|
||||
.comms-card-cat.urgent{color:#ff2244;border:1px solid rgba(255,34,68,0.4);animation:pulse 1.5s ease-in-out infinite}
|
||||
.comms-card-cat.action{color:#ffd700;border:1px solid rgba(255,215,0,0.4)}
|
||||
.comms-card-cat.reply{color:var(--cyan);border:1px solid rgba(0,212,255,0.3)}
|
||||
.comms-card-cat.meeting{color:#a78bfa;border:1px solid rgba(167,139,250,0.4)}
|
||||
.comms-card-cat.info{color:var(--text-dim);border:1px solid rgba(255,255,255,0.1)}
|
||||
.comms-card-cat.promo,.comms-card-cat.spam{color:rgba(255,255,255,0.25);border:1px solid rgba(255,255,255,0.08)}
|
||||
.comms-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)}
|
||||
.comms-card.open .comms-card-body{display:block}
|
||||
.comms-card-from{font-family:var(--font-mono);font-size:0.55rem;color:var(--text-dim);margin:7px 0 3px}
|
||||
.comms-card-summary{font-size:0.62rem;line-height:1.5;color:var(--text);margin:5px 0}
|
||||
.comms-draft-label{font-family:var(--font-display);font-size:0.5rem;letter-spacing:2px;color:var(--text-dim);margin:8px 0 4px}
|
||||
.comms-draft{font-size:0.6rem;line-height:1.5;color:rgba(0,212,255,0.7);background:rgba(0,212,255,0.04);border:1px solid rgba(0,212,255,0.15);border-radius:3px;padding:7px 9px;white-space:pre-wrap;max-height:160px;overflow-y:auto}
|
||||
.comms-empty{text-align:center;padding:24px 10px;font-family:var(--font-mono);font-size:0.6rem;color:var(--text-dim);letter-spacing:1px}
|
||||
.comms-header-bar{display:flex;gap:5px;margin-bottom:7px;flex-wrap:wrap}
|
||||
.comms-filter-btn{flex:1;min-width:50px;background:rgba(0,212,255,0.05);border:1px solid var(--panel-border);border-radius:3px;padding:4px 6px;color:var(--text-dim);font-family:var(--font-display);font-size:0.5rem;letter-spacing:1px;cursor:pointer;text-align:center}
|
||||
.comms-filter-btn.active,.comms-filter-btn:hover{background:rgba(0,212,255,0.12);color:var(--cyan);border-color:var(--cyan)}
|
||||
.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-prio{font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim);flex-shrink:0}
|
||||
/* ── 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-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none}
|
||||
@@ -1099,6 +1124,7 @@ body::after{
|
||||
<div class="tab" onclick="switchTab('agents')">AGENTS</div>
|
||||
<div class="tab" onclick="switchTab('sites')">SITES</div>
|
||||
<div class="tab" id="tab-btn-intel" onclick="switchTab('intel')">INTEL</div>
|
||||
<div class="tab" id="tab-btn-comms" onclick="switchTab('comms')">COMMS</div>
|
||||
</div>
|
||||
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||
<div id="vm-list"><div class="loading-shimmer"></div></div>
|
||||
@@ -1118,6 +1144,9 @@ body::after{
|
||||
<div id="tab-intel" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
|
||||
<div id="intel-list"><div class="loading-shimmer"></div></div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -3023,6 +3052,7 @@ function switchTab(name) {
|
||||
if (name === 'news') loadNews();
|
||||
if (name === 'agents') loadAgents();
|
||||
if (name === 'intel') loadIntel();
|
||||
if (name === 'comms') loadComms();
|
||||
if (name === 'alerts') loadAlerts();
|
||||
}
|
||||
|
||||
@@ -3643,15 +3673,125 @@ function intelPrompt() {
|
||||
if (input) { input.value = 'research '; input.focus(); }
|
||||
}
|
||||
|
||||
// Called from chat.js when arc_job is returned
|
||||
// Called when arc_job is returned from chat response
|
||||
function onArcJobStarted(jobId, jobType) {
|
||||
_intelActiveJobs.add(jobId);
|
||||
// Auto-switch to INTEL tab
|
||||
const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]');
|
||||
if (intelTab) intelTab.click();
|
||||
startIntelPolling();
|
||||
if (jobType === 'arc:gmail_triage') {
|
||||
// Route to COMMS tab
|
||||
const commsBtn = document.getElementById('tab-btn-comms');
|
||||
if (commsBtn) commsBtn.click();
|
||||
startCommsPolling();
|
||||
} else {
|
||||
_intelActiveJobs.add(jobId);
|
||||
const intelTab = document.querySelector('[onclick*="switchTab(\'intel\')"]');
|
||||
if (intelTab) intelTab.click();
|
||||
startIntelPolling();
|
||||
}
|
||||
}
|
||||
|
||||
// ── COMMS PROTOCOL — email triage HUD ────────────────────────────────────
|
||||
let _commsPollTimer = null;
|
||||
let _commsFilter = 'priority';
|
||||
let _commsOpenCards = new Set();
|
||||
|
||||
async function loadComms() {
|
||||
const el = document.getElementById('comms-list');
|
||||
if (!el) return;
|
||||
|
||||
try {
|
||||
const res = await api('arc?action=triage&limit=50&filter=' + _commsFilter);
|
||||
const items = Array.isArray(res) ? res : (res.items || []);
|
||||
|
||||
if (!items.length) {
|
||||
el.innerHTML = '<button class="comms-triage-btn" onclick="commsTriageNow()">◈ TRIAGE INBOX NOW</button>'
|
||||
+ '<div class="comms-empty">◈ NO TRIAGE DATA<br><span style="opacity:0.5">Say "check my email" to activate</span></div>';
|
||||
stopCommsPolling();
|
||||
return;
|
||||
}
|
||||
|
||||
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:'🗑'};
|
||||
|
||||
let html = '<button class="comms-triage-btn" onclick="commsTriageNow()">◈ TRIAGE INBOX NOW</button>';
|
||||
html += '<div class="comms-header-bar">';
|
||||
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>';
|
||||
|
||||
for (const item of items) {
|
||||
const cat = item.category || 'info';
|
||||
const icon = catIcons[cat] || '◈';
|
||||
const prio = item.priority || 0;
|
||||
const isOpen = _commsOpenCards.has(item.id);
|
||||
const hasReply = item.draft_reply && item.draft_reply.trim().length > 5;
|
||||
|
||||
html += `<div class="comms-card${isOpen?' open':''}" id="comms-card-${item.id}">
|
||||
<div class="comms-card-head" onclick="toggleCommsCard(${item.id})">
|
||||
<span class="comms-card-cat ${cat}">${icon} ${cat.toUpperCase()}</span>
|
||||
<span class="comms-card-subject">${escHtml((item.subject||'(no subject)').substring(0,60))}</span>
|
||||
<span class="comms-prio">${prio}/10</span>
|
||||
</div>
|
||||
<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-summary">${escHtml(item.summary||'')}</div>
|
||||
${hasReply ? `<div class="comms-draft-label">DRAFT REPLY</div><div class="comms-draft">${escHtml(item.draft_reply)}</div>` : ''}
|
||||
<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>` : ''}
|
||||
<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>`;
|
||||
}
|
||||
|
||||
el.innerHTML = html;
|
||||
|
||||
} catch(e) {
|
||||
if (el) el.innerHTML = '<div class="comms-empty">COMMS OFFLINE</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function toggleCommsCard(id) {
|
||||
const card = document.getElementById('comms-card-' + id);
|
||||
if (!card) return;
|
||||
if (_commsOpenCards.has(id)) _commsOpenCards.delete(id);
|
||||
else _commsOpenCards.add(id);
|
||||
card.classList.toggle('open');
|
||||
}
|
||||
|
||||
function commsSetFilter(f) {
|
||||
_commsFilter = f;
|
||||
loadComms();
|
||||
}
|
||||
|
||||
async function commsDismiss(id) {
|
||||
await api('arc?action=triage_action&id=' + id, 'POST', {action: 'dismissed'}).catch(() => {});
|
||||
loadComms();
|
||||
}
|
||||
|
||||
async function commsCopyReply(id) {
|
||||
const draft = document.querySelector(`#comms-card-${id} .comms-draft`);
|
||||
if (draft) {
|
||||
navigator.clipboard.writeText(draft.innerText).catch(() => {});
|
||||
const btn = document.querySelector(`#comms-card-${id} [onclick*="commsCopyReply"]`);
|
||||
if (btn) { btn.textContent = 'COPIED!'; setTimeout(() => btn.textContent = 'COPY REPLY', 1500); }
|
||||
}
|
||||
}
|
||||
|
||||
function commsTriageNow() {
|
||||
const input = document.getElementById('textInput');
|
||||
if (input) { input.value = 'check my email'; input.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true})); }
|
||||
}
|
||||
|
||||
function startCommsPolling() {
|
||||
if (_commsPollTimer) return;
|
||||
_commsPollTimer = setInterval(() => {
|
||||
if (document.getElementById('tab-comms')?.classList.contains('active')) loadComms();
|
||||
}, 8000);
|
||||
}
|
||||
|
||||
function stopCommsPolling() {
|
||||
if (_commsPollTimer) { clearInterval(_commsPollTimer); _commsPollTimer = null; }
|
||||
}
|
||||
|
||||
async function loadAgents() {
|
||||
const [listData, metricsData] = await Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user