Phase 8: Mission Directives — OKR/goal tracking with AI review

- DB: directives, directive_key_results, directive_links tables
- reactor.py v8.0.0: directive_review handler — fetches active directives + KRs + links, Claude generates executive progress briefing, injects into conversations
- directives.php: new API endpoint (list/get/save/delete/key_result_update/link/summary)
- api.php: routes directives/* endpoint
- admin/index.php: Directives nav + tab — objective cards with progress bars, editor with multi-KR builder (title/current/target/unit), AI Review button per directive and global
- index.html: DIRECTIVES tab — collapsible objective cards with progress bars, KR counts, AI Review button, link to admin
- chat.php: Tier 0.9i directive review detection; daily briefing now includes active directive progress %

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 11:59:44 +00:00
parent b6c417948e
commit aaf07edacb
5 changed files with 690 additions and 1 deletions
+40
View File
@@ -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);
+171
View File
@@ -0,0 +1,171 @@
<?php
// JARVIS Directives — OKR / mission goal tracking
// Actions: list | get | save | delete | key_result_update | link | unlink | review
switch ($action) {
// GET directives/list — all active directives with key results + progress
case '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
) ?: [];
// Compute progress pct per directive
foreach ($rows as &$r) {
$r['progress'] = ($r['kr_target_sum'] > 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}"]);
}
+387
View File
@@ -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)}
<div class="nav-item" data-tab="vision" onclick="nav(this)"> VISION PROTOCOL</div>
<div class="nav-item" data-tab="guardian" onclick="nav(this)" id="nav-guardian"> GUARDIAN MODE</div>
<div class="nav-item" data-tab="missions" onclick="nav(this)"> MISSION OPS</div>
<div class="nav-item" data-tab="directives" onclick="nav(this)"> DIRECTIVES</div>
<div class="nav-section">INFO</div>
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div>
<div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div>
@@ -1458,6 +1564,99 @@ select.filter-sel:focus{border-color:var(--cyan)}
</div>
</div>
<!-- DIRECTIVES -->
<div class="tab" id="tab-directives">
<div class="page-title"> MISSION DIRECTIVES OBJECTIVES &amp; KEY RESULTS</div>
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
<button class="btn btn-sm btn-green" onclick="directiveNew()">+ NEW DIRECTIVE</button>
<button class="btn btn-sm" onclick="directiveReviewAI()"> AI REVIEW</button>
<button class="btn btn-sm" onclick="loadDirectives()"> REFRESH</button>
<select id="dir-status-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadDirectives()">
<option value="active">ACTIVE</option>
<option value="all">ALL</option>
<option value="paused">PAUSED</option>
<option value="complete">COMPLETE</option>
</select>
<select id="dir-cat-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadDirectives()">
<option value="">ALL CATEGORIES</option>
<option value="work">WORK</option>
<option value="personal">PERSONAL</option>
<option value="health">HEALTH</option>
<option value="finance">FINANCE</option>
<option value="home">HOME</option>
</select>
<div id="directives-count" style="margin-left:auto;font-family:var(--mono);font-size:0.65rem;color:var(--dim)"></div>
</div>
<div id="directives-list"><div class="loading">LOADING DIRECTIVES...</div></div>
<!-- Directive editor panel -->
<div id="directive-editor" style="display:none;margin-top:20px;border:1px solid var(--border);border-radius:6px;padding:16px;background:rgba(0,212,255,0.02)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:14px">
<div id="dir-editor-title" style="font-family:var(--mono);font-size:0.75rem;letter-spacing:2px;color:var(--cyan)"> DIRECTIVE EDITOR</div>
<button class="btn btn-xs" onclick="document.getElementById('directive-editor').style.display='none'"> CLOSE</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:12px">
<div style="grid-column:1/3">
<div class="lbl">OBJECTIVE TITLE</div>
<input id="dir-title" class="inp" placeholder="What do you want to achieve?">
</div>
<div>
<div class="lbl">STATUS</div>
<select id="dir-status" class="inp">
<option value="active">Active</option>
<option value="paused">Paused</option>
<option value="complete">Complete</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:10px;margin-bottom:12px">
<div>
<div class="lbl">CATEGORY</div>
<select id="dir-category" class="inp">
<option value="work">Work</option>
<option value="personal">Personal</option>
<option value="health">Health</option>
<option value="finance">Finance</option>
<option value="home">Home</option>
<option value="other">Other</option>
</select>
</div>
<div>
<div class="lbl">PRIORITY (1-10)</div>
<input id="dir-priority" class="inp" type="number" min="1" max="10" value="5">
</div>
<div>
<div class="lbl">TARGET DATE</div>
<input id="dir-target-date" class="inp" type="date">
</div>
</div>
<div style="margin-bottom:14px">
<div class="lbl">DESCRIPTION</div>
<textarea id="dir-desc" class="inp" rows="2" placeholder="Context, why this matters..."></textarea>
</div>
<div style="margin-bottom:14px">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">
<div class="lbl" style="margin:0">KEY RESULTS</div>
<button class="btn btn-xs btn-green" onclick="dirAddKR()">+ ADD KEY RESULT</button>
</div>
<div id="dir-kr-list"></div>
</div>
<input type="hidden" id="dir-id" value="">
<div style="display:flex;gap:8px">
<button class="btn btn-sm btn-green" onclick="directiveSave()"> SAVE</button>
<button id="dir-del-btn" class="btn btn-sm btn-red" style="display:none" onclick="directiveDelete()"> DELETE</button>
</div>
<div id="dir-save-status" style="font-family:var(--mono);font-size:0.6rem;color:var(--cyan);margin-top:6px;min-height:14px"></div>
</div>
</div>
</div><!-- /content -->
</div><!-- /main -->
</div><!-- /app -->
@@ -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 = '<div class="loading">No directives found. Click + NEW DIRECTIVE to create one.</div>';
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
? `<span style="font-family:var(--mono);font-size:0.55rem;color:${daysLeft<0?'var(--red)':daysLeft<14?'var(--orange)':'var(--text-dim)'}">
${daysLeft<0?'OVERDUE '+Math.abs(daysLeft)+'d':daysLeft+'d left'}</span>`
: '';
const statusBadge = dir.status !== 'active'
? `<span style="font-size:0.55rem;color:var(--dim);margin-left:4px">[${dir.status.toUpperCase()}]</span>`
: '';
return `<tr>
<td style="min-width:200px">
<div style="font-size:0.7rem;font-family:var(--mono)">${esc(dir.title)}${statusBadge}</div>
<div style="font-size:0.58rem;color:${catColor};margin-top:2px">${dir.category.toUpperCase()} · P${dir.priority}</div>
</td>
<td style="min-width:160px">
<div style="display:flex;align-items:center;gap:6px">
<div style="flex:1;height:6px;background:rgba(255,255,255,0.08);border-radius:3px">
<div style="width:${pct}%;height:100%;background:${pct>=80?'var(--green)':pct>=40?'var(--orange)':'var(--red)'};border-radius:3px"></div>
</div>
<span style="font-family:var(--mono);font-size:0.6rem;min-width:32px">${pct}%</span>
</div>
</td>
<td>${dueBadge}</td>
<td style="font-family:var(--mono);font-size:0.58rem;color:var(--dim)">${dir.kr_count||0} KRs · ${dir.link_count||0} links</td>
<td style="white-space:nowrap">
<button class="btn btn-xs btn-green" onclick="directiveEdit(${dir.id})">EDIT</button>
<button class="btn btn-xs" onclick="directiveReviewSingle(${dir.id})"> AI REVIEW</button>
</td>
</tr>`;
}).join('');
el.innerHTML = `<table><thead><tr><th>OBJECTIVE</th><th>PROGRESS</th><th>DUE</th><th>DETAILS</th><th>ACTIONS</th></tr></thead><tbody>${rows}</tbody></table>`;
}
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 = '<div style="font-size:0.6rem;color:var(--dim)">No key results yet — click + ADD KEY RESULT</div>';
return;
}
el.innerHTML = _dirKRs.map(k => `
<div style="display:grid;grid-template-columns:2fr 80px 80px 60px 28px;gap:6px;align-items:center;margin-bottom:6px">
<input class="inp" value="${esc(k.title)}" placeholder="Key result title" oninput="_krUpdate(${k.id},'title',this.value)">
<input class="inp" type="number" step="0.1" value="${k.current_value}" placeholder="Current" title="Current value" oninput="_krUpdate(${k.id},'current_value',parseFloat(this.value)||0)">
<input class="inp" type="number" step="0.1" value="${k.target_value}" placeholder="Target" title="Target value" oninput="_krUpdate(${k.id},'target_value',parseFloat(this.value)||1)">
<input class="inp" value="${esc(k.unit)}" placeholder="Unit" title="Unit (%, $, hrs...)" oninput="_krUpdate(${k.id},'unit',this.value)">
<button class="btn btn-xs btn-red" onclick="dirRemoveKR(${k.id})"></button>
</div>
`).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)'};
+3
View File
@@ -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;
+88
View File
@@ -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{
<div class="tab" id="tab-btn-comms" onclick="switchTab('comms')">COMMS</div>
<div class="tab" id="tab-btn-guardian" onclick="switchTab('guardian')">GUARDIAN</div>
<div class="tab" id="tab-btn-missions" onclick="switchTab('missions')">MISSIONS</div>
<div class="tab" id="tab-btn-directives" onclick="switchTab('directives')">DIRECTIVES</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>
@@ -1223,6 +1238,9 @@ body::after{
<div id="tab-missions" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
<div id="missions-hud"><div class="loading-shimmer"></div></div>
</div>
<div id="tab-directives" class="tab-pane" style="overflow-y:auto;flex:1;padding:4px 0">
<div id="directives-hud"><div class="loading-shimmer"></div></div>
</div>
</div>
</div>
@@ -3144,6 +3162,7 @@ function switchTab(name) {
if (name === 'comms') { loadComms(); loadCommsOutbox(); }
if (name === 'guardian') loadGuardian();
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 = '<button class="dir-admin-btn" onclick="window.open(\'/admin#directives\',\'_blank\')">◈ MANAGE IN ADMIN</button>';
if (!list.length) {
html += '<div class="comms-empty">◈ NO ACTIVE DIRECTIVES<br><span style="opacity:0.5">Create objectives in Admin → Directives</span></div>';
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 += `<div class="dir-card${isOpen?' open':''}" id="dir-card-${dir.id}">
<div class="dir-card-head" onclick="toggleDirCard(${dir.id})">
<span style="font-family:var(--font-mono);font-size:0.55rem;color:${color};flex-shrink:0">${dir.category.toUpperCase()}</span>
<span class="dir-card-title" style="color:${color}">${escHtml(dir.title)}</span>
<span style="font-family:var(--font-mono);font-size:0.55rem;color:${fillColor};flex-shrink:0">${pct}%</span>
${dueTxt ? `<span style="font-family:var(--font-mono);font-size:0.48rem;color:${dueColor};flex-shrink:0">${dueTxt}</span>` : ''}
</div>
<div class="dir-card-body">
<div class="dir-progress-bar"><div class="dir-progress-fill" style="width:${pct}%;background:${fillColor}"></div></div>
<div style="font-family:var(--font-mono);font-size:0.5rem;color:var(--text-dim);margin-bottom:6px">${dir.kr_count||0} KEY RESULTS · ${dir.link_count||0} LINKED ITEMS</div>
<button onclick="hudDirectiveReview(${dir.id})" style="background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.2);border-radius:3px;padding:3px 8px;color:var(--cyan);font-family:var(--font-display);font-size:0.48rem;letter-spacing:1px;cursor:pointer">◈ AI REVIEW</button>
</div>
</div>`;
}
el.innerHTML = html;
} catch(e) {
if (el) el.innerHTML = '<div class="comms-empty">DIRECTIVES OFFLINE</div>';
}
}
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'),