mirror of
https://github.com/myronblair/jarvis
synced 2026-07-28 00:34:35 -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:
+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