diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html index 8273cac0..928dd489 100644 --- a/src/dashboard/templates/base.html +++ b/src/dashboard/templates/base.html @@ -6,7 +6,7 @@ - {% block title %}Mission Control{% endblock %} + {% block title %}Timmy Time — Mission Control{% endblock %} @@ -34,35 +34,56 @@ MISSION CONTROL MISSION CONTROL - - OFFLINE + + CONNECTING - +
- CALM - TASKS - BRIEFING - THINKING - MISSION CTRL - SWARM - BUGS + HOME +
+ + +
+
+ + +
+
+ +
+ SPARK + MEMORY + MARKET +
+
+
+ +
+ TOOLS + EVENTS + ROUTER + GROK +
+
- -
-
-
Loading...
+ +
+
+
Loading...
@@ -123,7 +144,7 @@ MOBILE LOCAL AI @@ -159,9 +180,10 @@ hamburger.addEventListener('click', toggleMenu); overlay.addEventListener('click', toggleMenu); - // Highlight current page in mobile menu - document.querySelectorAll('.mc-mobile-link').forEach(function(a) { - if (a.getAttribute('href') === window.location.pathname) { + // Highlight current page in mobile menu and desktop dropdowns + var currentPath = window.location.pathname; + document.querySelectorAll('.mc-mobile-link, .mc-dropdown-menu .mc-test-link').forEach(function(a) { + if (a.getAttribute('href') === currentPath) { a.classList.add('active'); } }); @@ -200,17 +222,25 @@ .then(function(data) { var list = document.getElementById('notif-list'); if (!data.length) { - list.innerHTML = '
No recent notifications
'; + list.innerHTML = '
No recent notifications
'; return; } - list.innerHTML = data.map(function(n) { - return '
' - + '
' + (n.title || n.event_type || 'Event') + '
' - + '
' + (n.timestamp || '') + '
' - + '
'; - }).join(''); + list.innerHTML = ''; + data.forEach(function(n) { + var item = document.createElement('div'); + item.className = 'mc-notif-item'; + var title = document.createElement('div'); + title.className = 'mc-notif-title'; + title.textContent = n.title || n.event_type || 'Event'; + var ts = document.createElement('div'); + ts.className = 'mc-notif-ts'; + ts.textContent = n.timestamp || ''; + item.appendChild(title); + item.appendChild(ts); + list.appendChild(item); + }); var badge = document.getElementById('notif-badge'); - if (badge) { badge.style.display = 'none'; } + if (badge) { badge.classList.add('hidden'); } }) .catch(function() {}); } diff --git a/src/dashboard/templates/index.html b/src/dashboard/templates/index.html index 4f3b2d15..721a188e 100644 --- a/src/dashboard/templates/index.html +++ b/src/dashboard/templates/index.html @@ -11,7 +11,7 @@ {% call panel("AGENTS", hx_get="/swarm/agents/sidebar", hx_trigger="every 10s") %} -
LOADING...
+
LOADING...
{% endcall %} diff --git a/src/dashboard/templates/mission_control.html b/src/dashboard/templates/mission_control.html index 8e5b64ef..7447a243 100644 --- a/src/dashboard/templates/mission_control.html +++ b/src/dashboard/templates/mission_control.html @@ -1,44 +1,44 @@ {% extends "base.html" %} -{% block title %}Mission Control — Timmy Time{% endblock %} +{% block title %}System Overview — Timmy Time{% endblock %} {% block content %}
-

🎛️ Mission Control

+

System Overview

Loading...
- + -
-
-
-
+
+
+
-
-
Sovereignty Score
-
Calculating...
+
Sovereignty Score
+
Calculating...
-
-
+
+
- + -

Dependencies

-
-

Loading...

+

Dependencies

+
+

Loading...

- + -

Recommendations

-
-

Loading...

+

Recommendations

+
+

Loading...

- + -

System Metrics

+

System Metrics

-
@@ -60,11 +60,11 @@
-
+

Grok Mode

- STANDBY + STANDBY
-
-
+
+
-
+
GROK MODE: LOADING...
-
+
xAI frontier reasoning augmentation
-
+
0
Grok Queries
@@ -111,14 +108,14 @@
-
+

Heartbeat Monitor

Checking...
- +
-
@@ -133,25 +130,25 @@
Model
- -
-
-
Waiting for heartbeat...
+ +
+
+
Waiting for heartbeat...
-
+
-

💬 Chat History

+

Chat History

- -
-

Loading chat history...

+ +
+

Loading chat history...

@@ -161,22 +158,24 @@ async function loadSovereignty() { try { const response = await fetch('/health/sovereignty'); const data = await response.json(); - + // Update score - document.getElementById('sov-score').textContent = data.overall_score.toFixed(1); - document.getElementById('sov-score').style.color = data.overall_score >= 9 ? 'var(--success)' : - data.overall_score >= 7 ? 'var(--warning)' : 'var(--danger)'; - document.getElementById('sov-bar').style.width = (data.overall_score * 10) + '%'; - document.getElementById('sov-bar').style.background = data.overall_score >= 9 ? 'var(--success)' : - data.overall_score >= 7 ? 'var(--warning)' : 'var(--danger)'; - + var scoreEl = document.getElementById('sov-score'); + scoreEl.textContent = data.overall_score.toFixed(1); + scoreEl.style.color = data.overall_score >= 9 ? 'var(--success)' : + data.overall_score >= 7 ? 'var(--warning)' : 'var(--danger)'; + var barEl = document.getElementById('sov-bar'); + barEl.style.width = (data.overall_score * 10) + '%'; + barEl.style.background = data.overall_score >= 9 ? 'var(--success)' : + data.overall_score >= 7 ? 'var(--warning)' : 'var(--danger)'; + // Update label let label = 'Poor'; if (data.overall_score >= 9) label = 'Excellent'; else if (data.overall_score >= 8) label = 'Good'; else if (data.overall_score >= 6) label = 'Fair'; - document.getElementById('sov-label').textContent = `${label} — ${data.dependencies.length} dependencies checked`; - + document.getElementById('sov-label').textContent = label + ' \u2014 ' + data.dependencies.length + ' dependencies checked'; + // Update system status const systemStatus = document.getElementById('system-status'); if (data.overall_score >= 9) { @@ -189,67 +188,67 @@ async function loadSovereignty() { systemStatus.textContent = 'Degraded'; systemStatus.className = 'badge badge-danger'; } - + // Update dependency grid const grid = document.getElementById('dependency-grid'); grid.innerHTML = ''; - data.dependencies.forEach(dep => { - const card = document.createElement('div'); + data.dependencies.forEach(function(dep) { + var card = document.createElement('div'); card.className = 'card'; card.style.padding = '12px'; - - const statusColor = dep.status === 'healthy' ? 'var(--success)' : - dep.status === 'degraded' ? 'var(--warning)' : 'var(--danger)'; - const scoreColor = dep.sovereignty_score >= 9 ? 'var(--success)' : - dep.sovereignty_score >= 7 ? 'var(--warning)' : 'var(--danger)'; - - // Securely build card content using textContent for dynamic data - const header = document.createElement('div'); - header.style = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;'; - - const name = document.createElement('strong'); + + var statusColor = dep.status === 'healthy' ? 'var(--success)' : + dep.status === 'degraded' ? 'var(--warning)' : 'var(--danger)'; + var scoreColor = dep.sovereignty_score >= 9 ? 'var(--success)' : + dep.sovereignty_score >= 7 ? 'var(--warning)' : 'var(--danger)'; + + var header = document.createElement('div'); + header.className = 'dep-card-header'; + + var name = document.createElement('strong'); name.textContent = dep.name; - - const badge = document.createElement('span'); + + var badge = document.createElement('span'); badge.className = 'badge'; badge.style.background = statusColor; badge.textContent = dep.status; - + header.appendChild(name); header.appendChild(badge); - - const details = document.createElement('div'); - details.style = 'font-size: 0.875rem; color: var(--text-muted); margin-bottom: 8px;'; + + var details = document.createElement('div'); + details.className = 'dep-card-details'; details.textContent = dep.details.error || dep.details.note || 'Operating normally'; - - const score = document.createElement('div'); - score.style = `font-size: 0.75rem; color: ${scoreColor};`; - score.textContent = `Sovereignty: ${dep.sovereignty_score}/10`; - + + var score = document.createElement('div'); + score.className = 'dep-card-score'; + score.style.color = scoreColor; + score.textContent = 'Sovereignty: ' + dep.sovereignty_score + '/10'; + card.appendChild(header); card.appendChild(details); card.appendChild(score); grid.appendChild(card); }); - + // Update recommendations securely const recs = document.getElementById('recommendations'); recs.innerHTML = ''; if (data.recommendations && data.recommendations.length > 0) { - const ul = document.createElement('ul'); - data.recommendations.forEach(r => { - const li = document.createElement('li'); + var ul = document.createElement('ul'); + data.recommendations.forEach(function(r) { + var li = document.createElement('li'); li.textContent = r; ul.appendChild(li); }); recs.appendChild(ul); } else { - const p = document.createElement('p'); - p.style.color = 'var(--text-muted)'; - p.textContent = 'No recommendations — system optimal'; + var p = document.createElement('p'); + p.className = 'chat-history-placeholder'; + p.textContent = 'No recommendations \u2014 system optimal'; recs.appendChild(p); } - + } catch (error) { console.error('Failed to load sovereignty:', error); document.getElementById('system-status').textContent = 'Error'; @@ -264,15 +263,14 @@ async function loadHealth() { const data = await response.json(); // Format uptime - const uptime = data.uptime_seconds; - let uptimeStr; + var uptime = data.uptime_seconds; + var uptimeStr; if (uptime < 60) uptimeStr = Math.floor(uptime) + 's'; else if (uptime < 3600) uptimeStr = Math.floor(uptime / 60) + 'm'; else uptimeStr = Math.floor(uptime / 3600) + 'h ' + Math.floor((uptime % 3600) / 60) + 'm'; document.getElementById('metric-uptime').textContent = uptimeStr; - // LLM backend and model from /health response if (data.llm_backend) { document.getElementById('hb-backend').textContent = data.llm_backend; } @@ -292,11 +290,10 @@ async function loadSwarmStats() { const data = await response.json(); var agentCount = data.agents || 0; - // Fallback: if /swarm returns 0, try /swarm/agents for a direct count if (agentCount === 0) { try { - const agentResp = await fetch('/swarm/agents'); - const agentData = await agentResp.json(); + var agentResp = await fetch('/swarm/agents'); + var agentData = await agentResp.json(); if (Array.isArray(agentData.agents)) { agentCount = agentData.agents.length; } @@ -316,32 +313,36 @@ async function loadLightningStats() { try { const response = await fetch('/serve/status'); const data = await response.json(); - document.getElementById('metric-earned').textContent = data.total_earned_sats || 0; - } catch (error) { - // /serve may not be running — default to 0 instead of '-' document.getElementById('metric-earned').textContent = '0'; } } // Heartbeat simulation -let tickCount = 0; +var tickCount = 0; function updateHeartbeat() { tickCount++; - const now = new Date().toLocaleTimeString(); + var now = new Date().toLocaleTimeString(); document.getElementById('hb-tick').textContent = now; document.getElementById('heartbeat-status').textContent = 'Active'; document.getElementById('heartbeat-status').className = 'badge badge-success'; - - const log = document.getElementById('heartbeat-log'); - const entry = document.createElement('div'); - entry.style.marginBottom = '2px'; - entry.innerHTML = `[${now}] Tick ${tickCount}`; + + var log = document.getElementById('heartbeat-log'); + var entry = document.createElement('div'); + entry.className = 'hb-entry'; + var ts = document.createElement('span'); + ts.className = 'hb-entry-ts'; + ts.textContent = '[' + now + ']'; + var check = document.createElement('span'); + check.className = 'hb-entry-ok'; + check.textContent = ' \u2713 '; + entry.appendChild(ts); + entry.appendChild(check); + entry.appendChild(document.createTextNode('Tick ' + tickCount)); log.appendChild(entry); log.scrollTop = log.scrollHeight; - - // Keep only last 50 entries + while (log.children.length > 50) { log.removeChild(log.firstChild); } @@ -349,55 +350,63 @@ function updateHeartbeat() { // Load chat history async function loadChatHistory() { - const container = document.getElementById('chat-history'); - container.innerHTML = '

Loading...

'; - + var container = document.getElementById('chat-history'); + container.innerHTML = ''; + var loadingP = document.createElement('p'); + loadingP.className = 'chat-history-placeholder'; + loadingP.textContent = 'Loading...'; + container.appendChild(loadingP); + try { - // Try to load from the message log endpoint if available - const response = await fetch('/dashboard/messages'); - const messages = await response.json(); - + var response = await fetch('/dashboard/messages'); + var messages = await response.json(); + if (messages.length === 0) { - container.innerHTML = '

No messages yet

'; + container.innerHTML = ''; + var emptyP = document.createElement('p'); + emptyP.className = 'chat-history-placeholder'; + emptyP.textContent = 'No messages yet'; + container.appendChild(emptyP); return; } - + container.innerHTML = ''; - messages.slice(-20).forEach(msg => { - const div = document.createElement('div'); - div.style.marginBottom = '12px'; - div.style.padding = '8px'; - div.style.background = msg.role === 'user' ? 'var(--bg-tertiary)' : 'transparent'; - div.style.borderRadius = '4px'; - - const role = document.createElement('strong'); + messages.slice(-20).forEach(function(msg) { + var div = document.createElement('div'); + div.className = msg.role === 'user' ? 'chat-history-msg chat-history-msg--user' : 'chat-history-msg'; + + var role = document.createElement('strong'); role.textContent = msg.role === 'user' ? 'You: ' : 'Timmy: '; role.style.color = msg.role === 'user' ? 'var(--accent)' : 'var(--success)'; - - const content = document.createElement('span'); + + var content = document.createElement('span'); content.textContent = msg.content; - + div.appendChild(role); div.appendChild(content); container.appendChild(div); }); - + } catch (error) { - // Fallback: show placeholder - container.innerHTML = ` -
-

Chat history persistence coming soon

-

Messages are currently in-memory only

-
- `; + container.innerHTML = ''; + var fallback = document.createElement('div'); + fallback.className = 'chat-history-fallback'; + var p1 = document.createElement('p'); + p1.textContent = 'Chat history persistence coming soon'; + var p2 = document.createElement('p'); + p2.className = 'chat-history-fallback-sub'; + p2.textContent = 'Messages are currently in-memory only'; + fallback.appendChild(p1); + fallback.appendChild(p2); + container.appendChild(fallback); } } // Load Grok stats async function loadGrokStats() { try { - const response = await fetch('/grok/status'); - const data = await response.json(); + var response = await fetch('/grok/status'); + var data = await response.json(); if (data.stats) { document.getElementById('grok-requests').textContent = data.stats.total_requests || 0; @@ -406,15 +415,13 @@ async function loadGrokStats() { document.getElementById('grok-cost').textContent = data.stats.estimated_cost_sats || 0; } - const badge = document.getElementById('grok-badge'); + var badge = document.getElementById('grok-badge'); if (data.active) { badge.textContent = 'ACTIVE'; - badge.style.background = '#00ff88'; - badge.style.color = '#000'; + badge.className = 'badge grok-badge-active'; } else { badge.textContent = 'STANDBY'; - badge.style.background = '#666'; - badge.style.color = '#fff'; + badge.className = 'badge grok-badge-standby'; } } catch (error) { // Grok endpoint may not respond — silent fallback @@ -430,10 +437,10 @@ loadGrokStats(); loadChatHistory(); // Periodic updates -setInterval(loadSovereignty, 30000); // Every 30s -setInterval(loadHealth, 10000); // Every 10s -setInterval(loadSwarmStats, 5000); // Every 5s -setInterval(updateHeartbeat, 5000); // Heartbeat every 5s -setInterval(loadGrokStats, 10000); // Grok stats every 10s +setInterval(loadSovereignty, 30000); +setInterval(loadHealth, 10000); +setInterval(loadSwarmStats, 5000); +setInterval(updateHeartbeat, 5000); +setInterval(loadGrokStats, 10000); {% endblock %} diff --git a/static/css/mission-control.css b/static/css/mission-control.css index 170450b9..4ef7d8ed 100644 --- a/static/css/mission-control.css +++ b/static/css/mission-control.css @@ -1924,3 +1924,235 @@ margin-top: 10px; border-radius: var(--radius-md); } + + +/* ── Notification dropdown ──────────────────────────── */ +.mc-notif-btn { + background: none; + cursor: pointer; + position: relative; +} +.mc-notif-menu { + right: 0; + left: auto; + min-width: 280px; + max-height: 350px; + overflow-y: auto; +} +.mc-notif-list { + padding: 6px; +} +.mc-notif-empty { + color: var(--text-dim); + font-size: 0.8rem; + text-align: center; + padding: 12px; +} +.mc-notif-item { + padding: 6px 8px; + border-bottom: 1px solid var(--border); + font-size: 0.8rem; +} +.mc-notif-title { + color: var(--text-bright); +} +.mc-notif-ts { + color: var(--text-dim); + font-size: 0.7rem; +} +.notif-badge { + display: inline-block; + width: 8px; + height: 8px; + background: var(--red); + border-radius: 50%; + position: absolute; + top: 0; + right: 0; +} +.notif-badge.hidden { + display: none; +} + +/* ── Mobile notification button ─────────────────────── */ +.mc-mobile-notif-btn { + background: none; + border: none; + cursor: pointer; + width: 100%; + text-align: left; + font: inherit; + color: inherit; + padding: inherit; +} + +/* ── Index page: sovereignty score ───────────────────── */ +.sov-section { + margin-bottom: 24px; +} +.sov-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 12px; +} +.sov-score-value { + font-size: 3rem; + font-weight: 700; +} +.sov-score-label { + font-weight: 600; +} +.sov-score-detail { + font-size: 0.875rem; + color: var(--text-muted); +} +.sov-bar-track { + background: var(--bg-tertiary); + height: 8px; + border-radius: 4px; + overflow: hidden; +} +.sov-bar-fill { + height: 100%; + width: 0%; + transition: width 0.5s; +} + +/* ── Index page: section headings ────────────────────── */ +.mc-section-heading { + margin-bottom: 12px; +} +.mc-section-gap { + margin-bottom: 24px; +} + +/* ── Index page: grok mode ───────────────────────────── */ +.mc-card-spaced { + margin-top: 24px; +} +.grok-badge-standby { + background: #666; +} +.grok-toggle-box { + border: 2px solid #666; + border-radius: 12px; + padding: 16px; + background: var(--bg-secondary); +} +.grok-toggle-row { + display: flex; + justify-content: space-between; + align-items: center; +} +.grok-toggle-title { + font-weight: 700; + font-size: 1.1rem; + color: #666; +} +.grok-toggle-desc { + font-size: 0.8rem; + color: var(--text-muted); + margin-top: 4px; +} +.grok-activate-btn { + background: #666; + color: #000; + border: none; + border-radius: 8px; + padding: 8px 20px; + cursor: pointer; + font-weight: 700; + font-family: inherit; +} +.grok-active .grok-toggle-box { + border-color: #00ff88; +} +.grok-active .grok-toggle-title { + color: #00ff88; +} +.grok-active .grok-activate-btn { + background: #00ff88; +} + +/* ── Index page: heartbeat ───────────────────────────── */ +.hb-log-wrap { + margin-top: 16px; +} +.hb-log { + height: 100px; + overflow-y: auto; + background: var(--bg-tertiary); + padding: 12px; + border-radius: 8px; + font-family: monospace; + font-size: 0.75rem; +} +.hb-log-placeholder { + color: var(--text-muted); +} + +/* ── Index page: chat history ────────────────────────── */ +.chat-history-container { + max-height: 300px; + overflow-y: auto; +} +.chat-history-msg { + margin-bottom: 12px; + padding: 8px; + border-radius: 4px; +} +.chat-history-msg--user { + background: var(--bg-tertiary); +} +.chat-history-placeholder { + color: var(--text-muted); +} +.chat-history-fallback { + color: var(--text-muted); + text-align: center; + padding: 20px; +} +.chat-history-fallback-sub { + font-size: 0.875rem; +} + +/* ── Index page: dependency cards (JS-built) ─────────── */ +.dep-card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} +.dep-card-details { + font-size: 0.875rem; + color: var(--text-muted); + margin-bottom: 8px; +} +.dep-card-score { + font-size: 0.75rem; +} + +/* ── Heartbeat log entries ───────────────────────────── */ +.hb-entry { + margin-bottom: 2px; +} +.hb-entry-ts { + color: var(--text-muted); +} +.hb-entry-ok { + color: var(--success); +} + +/* ── Grok badge states ───────────────────────────────── */ +.grok-badge-active { + background: #00ff88; + color: #000; +} + +/* ── Loading placeholder (shared) ────────────────────── */ +.mc-loading-placeholder { + font-size: 11px; + color: var(--text-dim); + letter-spacing: 0.08em; +} diff --git a/static/style.css b/static/style.css index 58c2c971..06adec47 100644 --- a/static/style.css +++ b/static/style.css @@ -187,6 +187,11 @@ a:hover { color: var(--orange); } .mc-dropdown-menu .mc-test-link:hover { background: rgba(124, 58, 237, 0.12); } +.mc-dropdown-menu .mc-test-link.active { + color: var(--orange); + border-left: 3px solid var(--orange); + background: rgba(124, 58, 237, 0.08); +} /* ── Mobile section labels ───────────────────────── */ .mc-mobile-section-label { @@ -767,6 +772,34 @@ a:hover { color: var(--orange); } } +/* ════════════════════════════════════════════════════ + TABLET (max-width: 1024px) — iPad / Apple Pencil + ════════════════════════════════════════════════════ */ +@media (max-width: 1024px) { + /* Larger touch targets for nav dropdown toggles */ + .mc-test-link { + font-size: 10px; + padding: 6px 12px; + min-height: 36px; + display: inline-flex; + align-items: center; + } + .mc-dropdown-menu .mc-test-link { + padding: 10px 16px; + min-height: 40px; + } + + /* Prevent iPad floating toolbar from obscuring chat input */ + .mc-chat-footer { + padding-bottom: max(10px, env(safe-area-inset-bottom, 10px)); + } + + /* Ensure chat input is above iPad keyboard toolbar */ + .mc-main { + padding-bottom: max(16px, env(safe-area-inset-bottom)); + } +} + /* ════════════════════════════════════════════════════ MOBILE (max-width: 768px) — iPhone optimized ════════════════════════════════════════════════════ */