diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index 28bdae9..eaf2f9d 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -760,6 +760,21 @@ if (!$reply) { if ($ov > 0) $parts[] = $ov . ' overdue task' . ($ov > 1 ? 's' : '') . ' need attention'; $ai = (int)($email_actions['cnt'] ?? 0); if ($ai > 0) $parts[] = $ai . ' email' . ($ai > 1 ? 's' : '') . ' require action'; + // Include active directive progress summary + $active_dirs = JarvisDB::query( + "SELECT d.title, + COALESCE(SUM(kr.current_value),0) AS cur, + COALESCE(SUM(kr.target_value),1) AS tgt + FROM directives d + LEFT JOIN directive_key_results kr ON kr.directive_id=d.id + WHERE d.status='active' + GROUP BY d.id + ORDER BY d.priority DESC LIMIT 3" + ) ?? []; + if ($active_dirs) { + $dp = array_map(fn($d) => $d['title'] . ' (' . round($d['cur'] / max($d['tgt'],1) * 100) . '%)', $active_dirs); + $parts[] = count($active_dirs) . ' active directive' . (count($active_dirs) > 1 ? 's' : '') . ': ' . implode(', ', $dp); + } $reply = $parts ? "Good morning, {$userAddr}. " . implode('. ', $parts) . '.' : "Good morning, {$userAddr}. Your schedule is clear — no tasks, appointments, or email actions pending today."; @@ -1355,6 +1370,31 @@ if (!$reply) { } } +// ── Tier 0.9i: Directives — review objectives, progress check ───────────────── +if (!$reply) { + $dirReviewPatterns = [ + '/^(?:jarvis[,\s]+)?(?:review\s+(?:my\s+)?(?:directives?|objectives?|goals?|OKRs?))/i', + '/^(?:jarvis[,\s]+)?(?:how\s+am\s+i\s+doing\s+on\s+(?:my\s+)?(?:directives?|objectives?|goals?))/i', + '/^(?:jarvis[,\s]+)?(?:directives?\s+(?:review|status|update|progress|briefing))/i', + '/^(?:jarvis[,\s]+)?(?:OKR\s+(?:review|update|status))/i', + '/^(?:jarvis[,\s]+)?(?:what(?:\'s|\s+is)\s+(?:my\s+)?(?:progress|status)\s+on\s+(?:my\s+)?(?:directives?|goals?|objectives?))/i', + ]; + foreach ($dirReviewPatterns as $pat) { + if (preg_match($pat, $message)) { + $arcRes = arcSubmitJob('directive_review', ['provider' => 'claude'], $sessionId); + if (isset($arcRes['job_id'])) { + $arcJobId = $arcRes['job_id']; + $reply = "◈ DIRECTIVE REVIEW INITIATED (Job #{$arcJobId}). I'm analyzing your active objectives and key results now, {$userAddr}. Stand by for your progress briefing."; + $source = 'arc:directive_review'; + } else { + $reply = "Directive review is offline, {$userAddr}. Arc Reactor may be unavailable."; + $source = 'arc:offline'; + } + break; + } + } +} + // ── Tier 1: Intent Engine (instant, no LLM) ─────────────────────────────── if (!$reply) { $matched = KBEngine::match($message); diff --git a/api/endpoints/directives.php b/api/endpoints/directives.php new file mode 100644 index 0000000..9a75f05 --- /dev/null +++ b/api/endpoints/directives.php @@ -0,0 +1,171 @@ + 0) + ? (float)round($r['kr_current_sum'] / $r['kr_target_sum'] * 100, 1) + : 0; + } + unset($r); + echo json_encode(['directives' => $rows]); + break; + + // GET directives/get?id=X — full directive with key results and links + case 'get': + $id = (int)($_GET['id'] ?? 0); + if (!$id) { echo json_encode(['error' => 'Missing id']); break; } + $d = JarvisDB::single("SELECT * FROM directives WHERE id=?", [$id]); + if (!$d) { echo json_encode(['error' => 'Not found']); break; } + $krs = JarvisDB::query("SELECT * FROM directive_key_results WHERE directive_id=? ORDER BY id ASC", [$id]) ?: []; + $links = JarvisDB::query( + "SELECT dl.*, t.title AS task_title, a.title AS appt_title + FROM directive_links dl + LEFT JOIN tasks t ON dl.link_type='task' AND t.id=dl.link_id + LEFT JOIN appointments a ON dl.link_type='appointment' AND a.id=dl.link_id + WHERE dl.directive_id=? + ORDER BY dl.created_at DESC", + [$id] + ) ?: []; + $sum_cur = array_sum(array_column($krs, 'current_value')); + $sum_tgt = array_sum(array_column($krs, 'target_value')); + $d['progress'] = ($sum_tgt > 0) ? round($sum_cur / $sum_tgt * 100, 1) : 0; + $d['key_results'] = $krs; + $d['links'] = $links; + echo json_encode($d); + break; + + // POST directives/save — create or update directive + key results + case 'save': + $id = (int)($data['id'] ?? 0); + $title = trim($data['title'] ?? ''); + $description = trim($data['description'] ?? ''); + $category = $data['category'] ?? 'work'; + $status = $data['status'] ?? 'active'; + $priority = (int)($data['priority'] ?? 5); + $target_date = !empty($data['target_date']) ? $data['target_date'] : null; + if (!$title) { echo json_encode(['error' => 'Title required']); break; } + if ($id) { + JarvisDB::execute( + "UPDATE directives SET title=?,description=?,category=?,status=?,priority=?,target_date=?,updated_at=NOW() WHERE id=?", + [$title,$description,$category,$status,$priority,$target_date,$id] + ); + } else { + $id = JarvisDB::insert( + "INSERT INTO directives (title,description,category,status,priority,target_date) VALUES (?,?,?,?,?,?)", + [$title,$description,$category,$status,$priority,$target_date] + ); + } + // Replace key results if provided + if (isset($data['key_results']) && is_array($data['key_results'])) { + JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]); + foreach ($data['key_results'] as $kr) { + $krtitle = trim($kr['title'] ?? ''); if (!$krtitle) continue; + JarvisDB::execute( + "INSERT INTO directive_key_results (directive_id,title,current_value,target_value,unit) VALUES (?,?,?,?,?)", + [$id, $krtitle, (float)($kr['current_value']??0), (float)($kr['target_value']??100), $kr['unit']??'%'] + ); + } + } + echo json_encode(['ok' => true, 'id' => $id]); + break; + + // POST directives/delete?id=X + case 'delete': + $id = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$id) { echo json_encode(['error' => 'Missing id']); break; } + JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]); + JarvisDB::execute("DELETE FROM directive_links WHERE directive_id=?", [$id]); + JarvisDB::execute("DELETE FROM directives WHERE id=?", [$id]); + echo json_encode(['ok' => true]); + break; + + // POST directives/key_result_update — update a single KR's current value + case 'key_result_update': + $krid = (int)($data['id'] ?? 0); + $value = (float)($data['current_value'] ?? 0); + if (!$krid) { echo json_encode(['error' => 'Missing kr id']); break; } + JarvisDB::execute( + "UPDATE directive_key_results SET current_value=?, updated_at=NOW() WHERE id=?", + [$value, $krid] + ); + echo json_encode(['ok' => true]); + break; + + // POST directives/link — link a task or appointment to a directive + case 'link': + $did = (int)($data['directive_id'] ?? 0); + $link_type = $data['link_type'] ?? 'task'; + $link_id = (int)($data['link_id'] ?? 0); + $note = trim($data['note'] ?? ''); + if (!$did) { echo json_encode(['error' => 'Missing directive_id']); break; } + JarvisDB::execute( + "INSERT INTO directive_links (directive_id,link_type,link_id,note) VALUES (?,?,?,?)", + [$did, $link_type, $link_id ?: null, $note] + ); + echo json_encode(['ok' => true]); + break; + + // POST directives/unlink?id=X — remove a link + case 'unlink': + $lid = (int)($_GET['id'] ?? $data['id'] ?? 0); + if (!$lid) { echo json_encode(['error' => 'Missing id']); break; } + JarvisDB::execute("DELETE FROM directive_links WHERE id=?", [$lid]); + echo json_encode(['ok' => true]); + break; + + // GET directives/summary — compact progress snapshot for chat/briefing injection + case 'summary': + $rows = JarvisDB::query( + "SELECT d.id, d.title, d.category, d.target_date, + COALESCE(SUM(kr.current_value),0) AS kr_cur, + COALESCE(SUM(kr.target_value),1) AS kr_tgt + FROM directives d + LEFT JOIN directive_key_results kr ON kr.directive_id=d.id + WHERE d.status='active' + GROUP BY d.id + ORDER BY d.priority DESC, d.target_date ASC + LIMIT 10" + ) ?: []; + $summary = []; + foreach ($rows as $r) { + $pct = round($r['kr_cur'] / max($r['kr_tgt'], 1) * 100, 0); + $summary[] = [ + 'id' => $r['id'], + 'title' => $r['title'], + 'category' => $r['category'], + 'target_date' => $r['target_date'], + 'progress' => $pct, + ]; + } + echo json_encode(['directives' => $summary]); + break; + + default: + http_response_code(404); + echo json_encode(['error' => "Unknown directives action: {$action}"]); +} diff --git a/public_html/admin/index.php b/public_html/admin/index.php index f0fe42b..8f042c2 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -593,6 +593,111 @@ if ($action) { $raw = curl_exec($ch); curl_close($ch); j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']); + // ── DIRECTIVES ─────────────────────────────────────────────────────── + case 'directive_list': + $status = $_GET['status'] ?? 'active'; + $category = $_GET['category'] ?? ''; + $where = '1=1'; $params = []; + if ($status && $status !== 'all') { $where .= ' AND d.status=?'; $params[] = $status; } + if ($category) { $where .= ' AND d.category=?'; $params[] = $category; } + $rows = JarvisDB::query( + "SELECT d.*, + COUNT(kr.id) AS kr_count, + COALESCE(SUM(kr.current_value),0) AS kr_current_sum, + COALESCE(SUM(kr.target_value),0) AS kr_target_sum, + (SELECT COUNT(*) FROM directive_links dl WHERE dl.directive_id=d.id) AS link_count + FROM directives d + LEFT JOIN directive_key_results kr ON kr.directive_id=d.id + WHERE {$where} + GROUP BY d.id + ORDER BY d.priority DESC, d.target_date ASC, d.created_at DESC", + $params + ) ?: []; + foreach ($rows as &$r) { + $r['progress'] = ($r['kr_target_sum'] > 0) + ? (float)round($r['kr_current_sum'] / $r['kr_target_sum'] * 100, 1) + : 0; + } + j(['directives' => $rows]); + + case 'directive_get': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + $d = JarvisDB::single("SELECT * FROM directives WHERE id=?", [$id]); + if (!$d) bad('Not found', 404); + $krs = JarvisDB::query("SELECT * FROM directive_key_results WHERE directive_id=? ORDER BY id", [$id]) ?: []; + $links = JarvisDB::query( + "SELECT dl.*, COALESCE(t.title,a.title) AS linked_title + FROM directive_links dl + LEFT JOIN tasks t ON dl.link_type='task' AND t.id=dl.link_id + LEFT JOIN appointments a ON dl.link_type='appointment' AND a.id=dl.link_id + WHERE dl.directive_id=? ORDER BY dl.created_at DESC", + [$id] + ) ?: []; + $cur = array_sum(array_column($krs,'current_value')); + $tgt = array_sum(array_column($krs,'target_value')); + $d['progress'] = $tgt > 0 ? round($cur/$tgt*100,1) : 0; + $d['key_results'] = $krs; + $d['links'] = $links; + j($d); + + case 'directive_save': + $id = (int)($_GET['id'] ?? 0); + $body = file_get_contents('php://input'); + $data_in = json_decode($body, true) ?: []; + $title = trim($data_in['title'] ?? ''); + $description = trim($data_in['description'] ?? ''); + $category = $data_in['category'] ?? 'work'; + $status = $data_in['status'] ?? 'active'; + $priority = (int)($data_in['priority'] ?? 5); + $target_date = $data_in['target_date'] ?? null; + $krs = $data_in['key_results'] ?? []; + if (!$title) bad('Title required'); + if ($id) { + JarvisDB::execute( + "UPDATE directives SET title=?,description=?,category=?,status=?,priority=?,target_date=?,updated_at=NOW() WHERE id=?", + [$title,$description,$category,$status,$priority,$target_date?:null,$id] + ); + } else { + $id = JarvisDB::insert( + "INSERT INTO directives (title,description,category,status,priority,target_date) VALUES (?,?,?,?,?,?)", + [$title,$description,$category,$status,$priority,$target_date?:null] + ); + } + if (is_array($krs)) { + JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]); + foreach ($krs as $kr) { + $krt = trim($kr['title'] ?? ''); if (!$krt) continue; + JarvisDB::execute( + "INSERT INTO directive_key_results (directive_id,title,current_value,target_value,unit) VALUES (?,?,?,?,?)", + [$id,$krt,(float)($kr['current_value']??0),(float)($kr['target_value']??100),$kr['unit']??'%'] + ); + } + } + j(['ok' => true, 'id' => $id]); + + case 'directive_delete': + $id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id'); + JarvisDB::execute("DELETE FROM directive_key_results WHERE directive_id=?", [$id]); + JarvisDB::execute("DELETE FROM directive_links WHERE directive_id=?", [$id]); + JarvisDB::execute("DELETE FROM directives WHERE id=?", [$id]); + j(['ok' => true]); + + case 'arc_action': + $body = file_get_contents('php://input'); + $d = json_decode($body, true) ?: []; + $type = $d['action'] === 'job_create' ? ($d['type'] ?? '') : ''; + $payload = $d['payload'] ?? []; + $pri = (int)($d['priority'] ?? 5); + if (!$type) bad('Missing type'); + $ch = curl_init('http://127.0.0.1:7474/job'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>10, CURLOPT_POST=>true, + CURLOPT_POSTFIELDS => json_encode(['type'=>$type,'payload'=>$payload,'priority'=>$pri,'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']); + // ── MISSION OPS ────────────────────────────────────────────────────── case 'mission_list': $ch = curl_init('http://127.0.0.1:7474/missions'); @@ -994,6 +1099,7 @@ select.filter-sel:focus{border-color:var(--cyan)} + @@ -1458,6 +1564,99 @@ select.filter-sel:focus{border-color:var(--cyan)} + +
+
◈ MISSION DIRECTIVES — OBJECTIVES & KEY RESULTS
+
+ + + + + +
+
+ +
LOADING DIRECTIVES...
+ + + +
+ @@ -1572,6 +1771,7 @@ function loadTab(tab) { triage: loadTriage, outbox: loadOutbox, missions: loadMissions, + directives: loadDirectives, vision: loadVision, guardian: loadGuardian, tasks: loadTasks, @@ -3244,6 +3444,193 @@ async function missionToggle(id, enabled) { else toast('Toggle failed', 'err'); } +// ── DIRECTIVES ─────────────────────────────────────────────────────────────── + +let _dirKRs = []; +let _dirKRIdx = 0; + +const CAT_COLORS = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--orange)',other:'var(--text-dim)'}; + +async function loadDirectives() { + const el = document.getElementById('directives-list'); + if (!el) return; + const status = document.getElementById('dir-status-filter')?.value || 'active'; + const category = document.getElementById('dir-cat-filter')?.value || ''; + const params = {status}; + if (category) params.category = category; + const d = await api('directive_list', params); + const list = d.directives || []; + document.getElementById('directives-count').textContent = list.length + ' DIRECTIVES'; + if (!list.length) { + el.innerHTML = '
No directives found. Click + NEW DIRECTIVE to create one.
'; + return; + } + const rows = list.map(dir => { + const pct = Math.min(100, Math.round(dir.progress || 0)); + const catColor = CAT_COLORS[dir.category] || 'var(--text-dim)'; + const daysLeft = dir.target_date + ? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000) + : null; + const dueBadge = daysLeft !== null + ? ` + ${daysLeft<0?'OVERDUE '+Math.abs(daysLeft)+'d':daysLeft+'d left'}` + : ''; + const statusBadge = dir.status !== 'active' + ? `[${dir.status.toUpperCase()}]` + : ''; + return ` + +
${esc(dir.title)}${statusBadge}
+
${dir.category.toUpperCase()} · P${dir.priority}
+ + +
+
+
+
+ ${pct}% +
+ + ${dueBadge} + ${dir.kr_count||0} KRs · ${dir.link_count||0} links + + + + + `; + }).join(''); + el.innerHTML = `${rows}
OBJECTIVEPROGRESSDUEDETAILSACTIONS
`; +} + +function directiveNew() { + _dirKRs = []; _dirKRIdx = 0; + document.getElementById('dir-id').value = ''; + document.getElementById('dir-title').value = ''; + document.getElementById('dir-desc').value = ''; + document.getElementById('dir-category').value = 'work'; + document.getElementById('dir-status').value = 'active'; + document.getElementById('dir-priority').value = 5; + document.getElementById('dir-target-date').value = ''; + document.getElementById('dir-editor-title').textContent = '◈ NEW DIRECTIVE'; + document.getElementById('dir-del-btn').style.display = 'none'; + document.getElementById('dir-save-status').textContent = ''; + _renderDirKRs(); + document.getElementById('directive-editor').style.display = 'block'; + document.getElementById('directive-editor').scrollIntoView({behavior:'smooth'}); +} + +async function directiveEdit(id) { + const d = await api('directive_get', {id}); + if (d.error) { toast('Load failed: ' + d.error, 'err'); return; } + document.getElementById('dir-id').value = d.id; + document.getElementById('dir-title').value = d.title || ''; + document.getElementById('dir-desc').value = d.description || ''; + document.getElementById('dir-category').value = d.category || 'work'; + document.getElementById('dir-status').value = d.status || 'active'; + document.getElementById('dir-priority').value = d.priority || 5; + document.getElementById('dir-target-date').value = d.target_date || ''; + document.getElementById('dir-editor-title').textContent = '◈ EDIT — ' + esc(d.title); + document.getElementById('dir-del-btn').style.display = ''; + document.getElementById('dir-save-status').textContent = ''; + _dirKRs = (d.key_results || []).map(kr => ({ + id: ++_dirKRIdx, dbid: kr.id, + title: kr.title, current_value: kr.current_value, + target_value: kr.target_value, unit: kr.unit || '%', + })); + _renderDirKRs(); + document.getElementById('directive-editor').style.display = 'block'; + document.getElementById('directive-editor').scrollIntoView({behavior:'smooth'}); +} + +function dirAddKR() { + _dirKRIdx++; + _dirKRs.push({id: _dirKRIdx, dbid: null, title:'', current_value:0, target_value:100, unit:'%'}); + _renderDirKRs(); +} + +function dirRemoveKR(sid) { + _dirKRs = _dirKRs.filter(k => k.id !== sid); + _renderDirKRs(); +} + +function _krUpdate(sid, field, val) { + const k = _dirKRs.find(x => x.id === sid); + if (k) k[field] = val; +} + +function _renderDirKRs() { + const el = document.getElementById('dir-kr-list'); + if (!el) return; + if (!_dirKRs.length) { + el.innerHTML = '
No key results yet — click + ADD KEY RESULT
'; + return; + } + el.innerHTML = _dirKRs.map(k => ` +
+ + + + + +
+ `).join(''); +} + +async function directiveSave() { + const id = parseInt(document.getElementById('dir-id')?.value || 0) || null; + const title = document.getElementById('dir-title')?.value.trim(); + const desc = document.getElementById('dir-desc')?.value.trim(); + const category = document.getElementById('dir-category')?.value; + const status = document.getElementById('dir-status')?.value; + const priority = parseInt(document.getElementById('dir-priority')?.value || 5); + const target_date = document.getElementById('dir-target-date')?.value || ''; + const stat = document.getElementById('dir-save-status'); + if (!title) { if (stat) stat.textContent = '✗ Title required'; return; } + const key_results = _dirKRs.map(k => ({ + title: k.title, current_value: parseFloat(k.current_value)||0, + target_value: parseFloat(k.target_value)||1, unit: k.unit||'%', + })).filter(k => k.title.trim()); + if (stat) stat.textContent = '◈ SAVING…'; + const d = await fetch(`admin?action=directive_save${id?'&id='+id:''}`, { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({title, description:desc, category, status, priority, target_date, key_results}), + }).then(r => r.json()).catch(() => ({error: 'request failed'})); + if (d.ok) { + if (stat) stat.textContent = '◈ SAVED ✓'; + toast('Directive saved', 'ok'); + loadDirectives(); + } else { + if (stat) stat.textContent = '✗ ' + (d.error || 'Save failed'); + toast('Save failed', 'err'); + } +} + +async function directiveDelete() { + const id = parseInt(document.getElementById('dir-id')?.value || 0); + if (!id || !confirm('Delete this directive and all its key results?')) return; + const d = await api('directive_delete', {id}); + if (d.ok) { + toast('Directive deleted', 'ok'); + document.getElementById('directive-editor').style.display = 'none'; + loadDirectives(); + } else toast('Delete failed', 'err'); +} + +async function directiveReviewAI(id) { + toast('◈ Dispatching AI directive review…', 'ok'); + const payload = id ? {directive_id: id, provider: 'claude'} : {provider: 'claude'}; + const res = await fetch('admin?action=arc_action', { + method: 'POST', + headers: {'Content-Type':'application/json'}, + body: JSON.stringify({action:'job_create', type:'directive_review', payload, priority: 6}), + }).then(r => r.json()).catch(() => ({})); + if (res.job_id) toast('Review job #' + res.job_id + ' started — results will appear in JARVIS chat', 'ok'); + else toast('Failed: ' + (res.error||'Arc offline'), 'err'); +} + +async function directiveReviewSingle(id) { return directiveReviewAI(id); } + // ── PLANNER ───────────────────────────────────────────────────────────────── const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'}; diff --git a/public_html/api.php b/public_html/api.php index 54ddff2..5557fc8 100644 --- a/public_html/api.php +++ b/public_html/api.php @@ -102,6 +102,9 @@ switch ($endpoint) { case "arc": require __DIR__ . "/../api/endpoints/arc.php"; break; + case "directives": + require __DIR__ . "/../api/endpoints/directives.php"; + break; case "calendar": require __DIR__ . '/../api/endpoints/calendar_sync.php'; break; diff --git a/public_html/index.html b/public_html/index.html index 5db7d9b..37ede2c 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -948,6 +948,20 @@ body::after{ .comms-compose-field{width:100%;background:rgba(0,212,255,0.04);border:1px solid var(--panel-border);border-radius:3px;padding:6px 8px;color:var(--text);font-family:var(--font-mono);font-size:0.6rem;box-sizing:border-box;margin-bottom:7px} .comms-compose-field:focus{outline:none;border-color:var(--cyan)} .comms-compose-actions{display:flex;gap:6px;margin-top:8px} +/* ── DIRECTIVES HUD ──────────────────────────────────────────────── */ +.dir-card{background:rgba(0,212,255,0.03);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden} +.dir-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} +.dir-card-head:hover{background:rgba(0,212,255,0.06)} +.dir-card-title{font-family:var(--font-display);font-size:0.6rem;letter-spacing:1px;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.dir-card-body{display:none;padding:0 10px 10px;border-top:1px solid var(--panel-border)} +.dir-card.open .dir-card-body{display:block} +.dir-progress-bar{height:5px;background:rgba(255,255,255,0.08);border-radius:3px;margin:6px 0} +.dir-progress-fill{height:100%;border-radius:3px;transition:width 0.4s ease} +.dir-kr-row{display:flex;align-items:center;gap:6px;margin:4px 0;font-family:var(--font-mono);font-size:0.52rem;color:var(--text-dim)} +.dir-kr-bar{flex:1;height:3px;background:rgba(255,255,255,0.06);border-radius:2px} +.dir-kr-fill{height:100%;border-radius:2px;background:rgba(0,212,255,0.5)} +.dir-admin-btn{width:100%;background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.3);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} +.dir-admin-btn:hover{background:rgba(0,212,255,0.12)} /* ── MISSION OPS HUD ─────────────────────────────────────────────── */ .mission-card{background:rgba(0,212,255,0.03);border:1px solid var(--panel-border);border-radius:var(--r);margin-bottom:7px;overflow:hidden} .mission-card-head{display:flex;align-items:center;gap:8px;padding:8px 10px;cursor:pointer;user-select:none} @@ -1193,6 +1207,7 @@ body::after{
COMMS
GUARDIAN
MISSIONS
+
DIRECTIVES
@@ -1223,6 +1238,9 @@ body::after{
+
+
+
@@ -3143,7 +3161,8 @@ function switchTab(name) { if (name === 'intel') loadIntel(); if (name === 'comms') { loadComms(); loadCommsOutbox(); } if (name === 'guardian') loadGuardian(); - if (name === 'missions') loadMissionsHud(); + if (name === 'missions') loadMissionsHud(); + if (name === 'directives') loadDirectivesHud(); if (name === 'alerts') loadAlerts(); } @@ -4272,6 +4291,75 @@ async function hudRunMission(id) { } } +// ── DIRECTIVES HUD ──────────────────────────────────────────────────────────── +let _dirOpenCards = new Set(); + +async function loadDirectivesHud() { + const el = document.getElementById('directives-hud'); + if (!el) return; + try { + const d = await api('directives/list?status=active'); + const list = (d.directives || []); + + let html = ''; + + if (!list.length) { + html += '
◈ NO ACTIVE DIRECTIVES
Create objectives in Admin → Directives
'; + el.innerHTML = html; + return; + } + + const catColors = {work:'var(--cyan)',personal:'#a78bfa',health:'#00ff88',finance:'#ffd700',home:'var(--panel-border)',other:'var(--text-dim)'}; + for (const dir of list) { + const pct = Math.min(100, Math.round(dir.progress || 0)); + const isOpen = _dirOpenCards.has(dir.id); + const color = catColors[dir.category] || 'var(--cyan)'; + const fillColor = pct >= 80 ? '#00ff88' : pct >= 40 ? '#ffd700' : '#ff6644'; + const daysLeft = dir.target_date + ? Math.ceil((new Date(dir.target_date) - new Date()) / 86400000) : null; + const dueTxt = daysLeft !== null + ? (daysLeft < 0 ? `OVERDUE ${Math.abs(daysLeft)}d` : `${daysLeft}d left`) + : ''; + const dueColor = daysLeft !== null && daysLeft < 0 ? '#ff2244' : daysLeft < 14 ? '#ffd700' : 'var(--text-dim)'; + + html += `
+
+ ${dir.category.toUpperCase()} + ${escHtml(dir.title)} + ${pct}% + ${dueTxt ? `${dueTxt}` : ''} +
+
+
+
${dir.kr_count||0} KEY RESULTS · ${dir.link_count||0} LINKED ITEMS
+ +
+
`; + } + el.innerHTML = html; + } catch(e) { + if (el) el.innerHTML = '
DIRECTIVES OFFLINE
'; + } +} + +function toggleDirCard(id) { + const card = document.getElementById('dir-card-' + id); + if (!card) return; + if (_dirOpenCards.has(id)) _dirOpenCards.delete(id); + else _dirOpenCards.add(id); + card.classList.toggle('open'); +} + +async function hudDirectiveReview(id) { + const res = await api('arc?action=job_create', 'POST', { + type: 'directive_review', payload: {directive_id: id, provider: 'claude'}, priority: 6, + }); + if (res.job_id) { + addMessage('jarvis', `◈ DIRECTIVE REVIEW initiated (Job #${res.job_id}). Analyzing objectives and key results now. Results will appear here shortly.`); + speak(`Directive review underway. I'll brief you on your progress in a moment.`); + } +} + async function loadAgents() { const [listData, metricsData] = await Promise.all([ api('agent/list'),