mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
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:
@@ -760,6 +760,21 @@ if (!$reply) {
|
|||||||
if ($ov > 0) $parts[] = $ov . ' overdue task' . ($ov > 1 ? 's' : '') . ' need attention';
|
if ($ov > 0) $parts[] = $ov . ' overdue task' . ($ov > 1 ? 's' : '') . ' need attention';
|
||||||
$ai = (int)($email_actions['cnt'] ?? 0);
|
$ai = (int)($email_actions['cnt'] ?? 0);
|
||||||
if ($ai > 0) $parts[] = $ai . ' email' . ($ai > 1 ? 's' : '') . ' require action';
|
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
|
$reply = $parts
|
||||||
? "Good morning, {$userAddr}. " . implode('. ', $parts) . '.'
|
? "Good morning, {$userAddr}. " . implode('. ', $parts) . '.'
|
||||||
: "Good morning, {$userAddr}. Your schedule is clear — no tasks, appointments, or email actions pending today.";
|
: "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) ───────────────────────────────
|
// ── Tier 1: Intent Engine (instant, no LLM) ───────────────────────────────
|
||||||
if (!$reply) {
|
if (!$reply) {
|
||||||
$matched = KBEngine::match($message);
|
$matched = KBEngine::match($message);
|
||||||
|
|||||||
@@ -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}"]);
|
||||||
|
}
|
||||||
@@ -593,6 +593,111 @@ if ($action) {
|
|||||||
$raw = curl_exec($ch); curl_close($ch);
|
$raw = curl_exec($ch); curl_close($ch);
|
||||||
j(json_decode($raw, true) ?: ['error' => 'Arc Reactor unreachable']);
|
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 ──────────────────────────────────────────────────────
|
// ── MISSION OPS ──────────────────────────────────────────────────────
|
||||||
case 'mission_list':
|
case 'mission_list':
|
||||||
$ch = curl_init('http://127.0.0.1:7474/missions');
|
$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="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="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="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-section">INFO</div>
|
||||||
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- DIRECTIVES -->
|
||||||
|
<div class="tab" id="tab-directives">
|
||||||
|
<div class="page-title">◈ MISSION DIRECTIVES — OBJECTIVES & 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><!-- /content -->
|
||||||
</div><!-- /main -->
|
</div><!-- /main -->
|
||||||
</div><!-- /app -->
|
</div><!-- /app -->
|
||||||
@@ -1572,6 +1771,7 @@ function loadTab(tab) {
|
|||||||
triage: loadTriage,
|
triage: loadTriage,
|
||||||
outbox: loadOutbox,
|
outbox: loadOutbox,
|
||||||
missions: loadMissions,
|
missions: loadMissions,
|
||||||
|
directives: loadDirectives,
|
||||||
vision: loadVision,
|
vision: loadVision,
|
||||||
guardian: loadGuardian,
|
guardian: loadGuardian,
|
||||||
tasks: loadTasks,
|
tasks: loadTasks,
|
||||||
@@ -3244,6 +3444,193 @@ async function missionToggle(id, enabled) {
|
|||||||
else toast('Toggle failed', 'err');
|
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 ─────────────────────────────────────────────────────────────────
|
// ── PLANNER ─────────────────────────────────────────────────────────────────
|
||||||
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
|
const _PRI_COLOR = {urgent:'var(--red)',high:'var(--orange)',normal:'var(--text)',low:'var(--border2)'};
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ switch ($endpoint) {
|
|||||||
case "arc":
|
case "arc":
|
||||||
require __DIR__ . "/../api/endpoints/arc.php";
|
require __DIR__ . "/../api/endpoints/arc.php";
|
||||||
break;
|
break;
|
||||||
|
case "directives":
|
||||||
|
require __DIR__ . "/../api/endpoints/directives.php";
|
||||||
|
break;
|
||||||
case "calendar":
|
case "calendar":
|
||||||
require __DIR__ . '/../api/endpoints/calendar_sync.php';
|
require __DIR__ . '/../api/endpoints/calendar_sync.php';
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -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{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-field:focus{outline:none;border-color:var(--cyan)}
|
||||||
.comms-compose-actions{display:flex;gap:6px;margin-top:8px}
|
.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 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{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}
|
.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-comms" onclick="switchTab('comms')">COMMS</div>
|
||||||
<div class="tab" id="tab-btn-guardian" onclick="switchTab('guardian')">GUARDIAN</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-missions" onclick="switchTab('missions')">MISSIONS</div>
|
||||||
|
<div class="tab" id="tab-btn-directives" onclick="switchTab('directives')">DIRECTIVES</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
<div id="tab-vms" class="tab-pane" style="overflow-y:auto;flex:1">
|
||||||
<div id="vm-list"><div class="loading-shimmer"></div></div>
|
<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="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 id="missions-hud"><div class="loading-shimmer"></div></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>
|
||||||
</div>
|
</div>
|
||||||
@@ -3144,6 +3162,7 @@ function switchTab(name) {
|
|||||||
if (name === 'comms') { loadComms(); loadCommsOutbox(); }
|
if (name === 'comms') { loadComms(); loadCommsOutbox(); }
|
||||||
if (name === 'guardian') loadGuardian();
|
if (name === 'guardian') loadGuardian();
|
||||||
if (name === 'missions') loadMissionsHud();
|
if (name === 'missions') loadMissionsHud();
|
||||||
|
if (name === 'directives') loadDirectivesHud();
|
||||||
if (name === 'alerts') loadAlerts();
|
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() {
|
async function loadAgents() {
|
||||||
const [listData, metricsData] = await Promise.all([
|
const [listData, metricsData] = await Promise.all([
|
||||||
api('agent/list'),
|
api('agent/list'),
|
||||||
|
|||||||
Reference in New Issue
Block a user