mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: email intelligence — action item detection, task/appt creation, admin EMAIL tab, full voice intents
This commit is contained in:
@@ -320,6 +320,49 @@ if ($action) {
|
||||
j(['vms'=>$pve['vms']??[],'containers'=>$pve['containers']??[],'node_info'=>$pve['node_info']??[],'node_status'=>$pve['node_status']??null,'ts'=>$raw['ts']]);
|
||||
|
||||
// ── USERS ────────────────────────────────────────────────────────────
|
||||
case 'email_inbox':
|
||||
// Call via server's own IP — REMOTE_ADDR matches JARVIS_IP so auth bypass applies
|
||||
$acct = $_GET['account'] ?? 'all';
|
||||
$force = !empty($_GET['force']) ? '&force=1' : '';
|
||||
$ch = curl_init('https://165.22.1.228/api/email?account=' . $acct . $force);
|
||||
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>25,
|
||||
CURLOPT_SSL_VERIFYPEER=>false,CURLOPT_SSL_VERIFYHOST=>false,
|
||||
CURLOPT_HTTPHEADER=>['Host: jarvis.orbishosting.com']]);
|
||||
$r = curl_exec($ch); $code = curl_getinfo($ch,CURLINFO_HTTP_CODE); curl_close($ch);
|
||||
if($code===200 && $r) j(json_decode($r,true));
|
||||
else j(['error'=>'Email fetch failed (HTTP '.$code.')']);
|
||||
|
||||
case 'email_action_items':
|
||||
$rows = JarvisDB::query("SELECT * FROM email_actions WHERE dismissed=0 AND task_id IS NULL AND appointment_id IS NULL ORDER BY received_at DESC LIMIT 100") ?? [];
|
||||
j(['action_items'=>$rows]);
|
||||
|
||||
case 'email_create_task':
|
||||
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
|
||||
$ea=JarvisDB::single('SELECT * FROM email_actions WHERE id=?',[$id]); if(!$ea) bad('Not found');
|
||||
$title=trim($_POST['title']??$ea['suggested_title']);
|
||||
$due=trim($_POST['due_date']??$ea['suggested_date']??'');
|
||||
$notes="From: {$ea['from_name']} <{$ea['from_email']}>\nSubject: {$ea['subject']}";
|
||||
$tid=JarvisDB::insert('INSERT INTO tasks(title,notes,category,priority,due_date)VALUES(?,?,?,?,?)',
|
||||
[$title,$notes,'work','normal',$due?:null]);
|
||||
JarvisDB::execute('UPDATE email_actions SET task_id=?,dismissed=1 WHERE id=?',[$tid,$id]);
|
||||
j(['ok'=>true,'task_id'=>$tid]);
|
||||
|
||||
case 'email_create_appt':
|
||||
$id=(int)($_POST['id']??0); if(!$id) bad('No id');
|
||||
$ea=JarvisDB::single('SELECT * FROM email_actions WHERE id=?',[$id]); if(!$ea) bad('Not found');
|
||||
$title=trim($_POST['title']??$ea['suggested_title']);
|
||||
$start=trim($_POST['start_at']??'');
|
||||
if(!$start) $start=($ea['suggested_date']??date('Y-m-d')).' 09:00:00';
|
||||
$aid=JarvisDB::insert('INSERT INTO appointments(title,description,category,start_at)VALUES(?,?,?,?)',
|
||||
[$title,"From: {$ea['from_name']} <{$ea['from_email']}>",'work',$start]);
|
||||
JarvisDB::execute('UPDATE email_actions SET appointment_id=?,dismissed=1 WHERE id=?',[$aid,$id]);
|
||||
j(['ok'=>true,'appointment_id'=>$aid]);
|
||||
|
||||
case 'email_dismiss':
|
||||
$id=(int)($_POST['id']??0);
|
||||
if($id) JarvisDB::execute('UPDATE email_actions SET dismissed=1 WHERE id=?',[$id]);
|
||||
j(['ok'=>true]);
|
||||
|
||||
case 'task_list':
|
||||
$status = trim($_GET['status'] ?? '');
|
||||
$category = trim($_GET['category'] ?? '');
|
||||
@@ -612,6 +655,8 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
||||
<div class="nav-item" data-tab="ha" onclick="nav(this)">HOME ASSISTANT</div>
|
||||
<div class="nav-item" data-tab="news" onclick="nav(this)">NEWS</div>
|
||||
<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-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>
|
||||
@@ -772,6 +817,29 @@ select.filter-sel:focus{border-color:var(--cyan)}
|
||||
<div id="sites-content"><div class="loading">SCANNING...</div></div>
|
||||
</div>
|
||||
|
||||
<!-- EMAIL -->
|
||||
<div class="tab" id="tab-email">
|
||||
<div class="page-title">EMAIL INTELLIGENCE
|
||||
<div class="actions">
|
||||
<button class="btn btn-sm" id="email-tab-inbox" onclick="emailShowTab('inbox')" style="background:rgba(0,212,255,0.15)">📥 INBOX</button>
|
||||
<button class="btn btn-sm" id="email-tab-actions" onclick="emailShowTab('actions')">⚡ ACTION ITEMS <span id="email-ai-badge" style="background:var(--orange);color:#000;border-radius:10px;padding:0 5px;font-size:0.6rem;margin-left:4px"></span></button>
|
||||
<select id="email-acct-filter" onchange="loadEmailInbox()" class="filter-sel">
|
||||
<option value="all">ALL ACCOUNTS</option>
|
||||
<option value="gmail">Gmail</option>
|
||||
<option value="outlook">Outlook</option>
|
||||
<option value="icloud">iCloud</option>
|
||||
</select>
|
||||
<button class="btn btn-sm" onclick="loadEmailInbox(true)">↺ REFRESH</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="email-inbox-view">
|
||||
<div class="tbl-wrap" id="email-tbl"></div>
|
||||
</div>
|
||||
<div id="email-actions-view" style="display:none">
|
||||
<div class="tbl-wrap" id="email-actions-tbl"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TASKS -->
|
||||
<div class="tab" id="tab-tasks">
|
||||
<div class="page-title">TASKS
|
||||
@@ -919,6 +987,7 @@ function loadTab(tab) {
|
||||
vms: loadVMs,
|
||||
sites: loadSites,
|
||||
users: loadUsers,
|
||||
email: ()=>{ loadEmailInbox(); loadEmailActionItems(); },
|
||||
tasks: loadTasks,
|
||||
appointments: loadAppts,
|
||||
})[tab]?.();
|
||||
@@ -1658,6 +1727,86 @@ document.getElementById('app').style.display='flex';
|
||||
document.getElementById('adminUser').textContent = '<?= htmlspecialchars($_SESSION['admin_name'] ?? $_SESSION['admin_user']) ?>'.toUpperCase();
|
||||
initApp();
|
||||
<?php endif; ?>
|
||||
// ── EMAIL ───────────────────────────────────────────────────────────────────
|
||||
let _emailCurrentTab = 'inbox';
|
||||
|
||||
function emailShowTab(tab) {
|
||||
_emailCurrentTab = tab;
|
||||
document.getElementById('email-inbox-view').style.display = tab==='inbox' ? '' : 'none';
|
||||
document.getElementById('email-actions-view').style.display = tab==='actions' ? '' : 'none';
|
||||
document.getElementById('email-tab-inbox').style.background = tab==='inbox' ? 'rgba(0,212,255,0.15)' : '';
|
||||
document.getElementById('email-tab-actions').style.background = tab==='actions' ? 'rgba(0,212,255,0.15)' : '';
|
||||
if (tab === 'actions') loadEmailActionItems();
|
||||
else loadEmailInbox();
|
||||
}
|
||||
|
||||
async function loadEmailInbox(force=false) {
|
||||
const acct = document.getElementById('email-acct-filter')?.value || 'all';
|
||||
const el = document.getElementById('email-tbl');
|
||||
if (el) el.innerHTML = '<div class="loading">FETCHING EMAIL…</div>';
|
||||
const d = await api('email_inbox', {account: acct, ...(force?{force:1}:{})});
|
||||
if (d.error) { el.innerHTML = `<div class="loading text-red">${d.error}</div>`; return; }
|
||||
// Update action item badge
|
||||
const badge = document.getElementById('email-ai-badge');
|
||||
if (badge && d.action_items_count) badge.textContent = d.action_items_count; else if(badge) badge.textContent = '';
|
||||
const msgs = d.summary?.recent || [];
|
||||
if (!msgs.length) { el.innerHTML='<div class="loading">No messages.</div>'; return; }
|
||||
const rows = msgs.map(m => {
|
||||
const ai = m.action_type ? `<span style="background:${m.action_type==='appointment'?'var(--cyan)':'var(--orange)'};color:#000;border-radius:3px;padding:0 4px;font-size:0.55rem">${m.action_type.toUpperCase()}</span> ` : '';
|
||||
const unread = m.unread ? `<span style="color:var(--cyan);font-weight:700">●</span> ` : '';
|
||||
const acctBadge = m.account ? `<span style="color:var(--text-dim);font-size:0.58rem">[${m.account.toUpperCase()}]</span>` : '';
|
||||
return `<tr${m.unread?' style="background:rgba(0,212,255,0.04)"':''}>
|
||||
<td style="width:16px">${unread}</td>
|
||||
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(m.from_name||m.from_email||'')}</td>
|
||||
<td style="max-width:280px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${ai}${esc(m.subject||'')}</td>
|
||||
<td style="color:var(--text-dim);font-size:0.62rem;white-space:nowrap">${esc(m.date||'')} ${acctBadge}</td>
|
||||
<td style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-dim);font-size:0.62rem">${esc((m.preview||'').substring(0,120))}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
el.innerHTML = `<table><thead><tr><th></th><th>FROM</th><th>SUBJECT</th><th>DATE</th><th>PREVIEW</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
}
|
||||
|
||||
async function loadEmailActionItems() {
|
||||
const el = document.getElementById('email-actions-tbl');
|
||||
if (!el) return;
|
||||
const d = await api('email_action_items');
|
||||
const items = d.action_items || [];
|
||||
const badge = document.getElementById('email-ai-badge');
|
||||
if (badge) badge.textContent = items.length || '';
|
||||
if (!items.length) { el.innerHTML='<div class="loading">No action items pending — inbox is clear.</div>'; return; }
|
||||
const rows = items.map(it => {
|
||||
const typeColor = it.action_type==='appointment' ? 'var(--cyan)' : 'var(--orange)';
|
||||
const sugDate = it.suggested_date ? `<input type="date" id="ead-${it.id}" value="${it.suggested_date}" style="background:#060a0e;border:1px solid var(--border2);color:var(--text);padding:2px 4px;font-size:0.6rem;width:110px">` : `<input type="date" id="ead-${it.id}" style="background:#060a0e;border:1px solid var(--border2);color:var(--text);padding:2px 4px;font-size:0.6rem;width:110px">`;
|
||||
const titleIn = `<input id="eat-${it.id}" value="${esc((it.suggested_title||it.subject||'').substring(0,80))}" style="background:#060a0e;border:1px solid var(--border2);color:var(--text);padding:2px 6px;font-size:0.65rem;width:200px">`;
|
||||
const btnTask = `<button class="btn btn-xs" style="border-color:var(--orange);color:var(--orange)" onclick="emailMakeTask(${it.id})">+ TASK</button>`;
|
||||
const btnAppt = `<button class="btn btn-xs" style="border-color:var(--cyan);color:var(--cyan)" onclick="emailMakeAppt(${it.id})">📅 APPT</button>`;
|
||||
const btnDismiss = `<button class="btn btn-xs" onclick="emailDismiss(${it.id})">✗ DISMISS</button>`;
|
||||
return `<tr>
|
||||
<td style="white-space:nowrap"><span style="background:${typeColor};color:#000;border-radius:3px;padding:1px 5px;font-size:0.6rem">${it.action_type.toUpperCase()}</span></td>
|
||||
<td style="max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(it.from_name||it.from_email||'')}</td>
|
||||
<td>${titleIn}</td>
|
||||
<td>${sugDate}</td>
|
||||
<td style="white-space:nowrap">${btnTask} ${btnAppt} ${btnDismiss}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
el.innerHTML = `<table><thead><tr><th>TYPE</th><th>FROM</th><th>TITLE</th><th>DATE</th><th>ACTIONS</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||
}
|
||||
|
||||
function emailMakeTask(id) {
|
||||
const title = document.getElementById('eat-'+id)?.value || '';
|
||||
const due = document.getElementById('ead-'+id)?.value || '';
|
||||
apiPost('email_create_task',{id,title,due_date:due},()=>{ toast('Task created','ok'); loadEmailActionItems(); loadTasks(); });
|
||||
}
|
||||
function emailMakeAppt(id) {
|
||||
const title = document.getElementById('eat-'+id)?.value || '';
|
||||
const dateVal = document.getElementById('ead-'+id)?.value || '';
|
||||
const start = dateVal ? dateVal + 'T09:00' : '';
|
||||
apiPost('email_create_appt',{id,title,start_at:start},()=>{ toast('Appointment created','ok'); loadEmailActionItems(); loadAppts(); });
|
||||
}
|
||||
function emailDismiss(id) {
|
||||
apiPost('email_dismiss',{id},()=>{ toast('Dismissed','ok'); loadEmailActionItems(); });
|
||||
}
|
||||
|
||||
// ── PLANNER ─────────────────────────────────────────────────────────────────
|
||||
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user