polish: streamline nav, extract inline styles, improve tablet UX (#168)

This commit is contained in:
Alexander Whitestone
2026-03-11 11:32:56 -04:00
committed by GitHub
parent b028b768c9
commit 708c8a2477
5 changed files with 484 additions and 182 deletions

View File

@@ -6,7 +6,7 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="theme-color" content="#080412" />
<title>{% block title %}Mission Control{% endblock %}</title>
<title>{% block title %}Timmy Time — Mission Control{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
@@ -34,35 +34,56 @@
<a href="/" class="mc-title">MISSION CONTROL</a>
<span class="mc-subtitle">MISSION CONTROL</span>
<span class="mc-conn-status" id="conn-status">
<span class="mc-conn-dot red" id="conn-dot"></span>
<span id="conn-label">OFFLINE</span>
<span class="mc-conn-dot amber" id="conn-dot"></span>
<span id="conn-label">CONNECTING</span>
</span>
</div>
<!-- Desktop nav -->
<!-- Desktop nav — grouped dropdowns matching mobile sections -->
<div class="mc-header-right mc-desktop-nav">
<a href="/calm" class="mc-test-link">CALM</a>
<a href="/tasks" class="mc-test-link">TASKS</a>
<a href="/briefing" class="mc-test-link">BRIEFING</a>
<a href="/thinking" class="mc-test-link mc-link-thinking">THINKING</a>
<a href="/swarm/mission-control" class="mc-test-link">MISSION CTRL</a>
<a href="/swarm/live" class="mc-test-link">SWARM</a>
<a href="/bugs" class="mc-test-link mc-link-bugs">BUGS</a>
<a href="/" class="mc-test-link">HOME</a>
<div class="mc-nav-dropdown">
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">CORE &#x25BE;</button>
<div class="mc-dropdown-menu">
<a href="/calm" class="mc-test-link">CALM</a>
<a href="/tasks" class="mc-test-link">TASKS</a>
<a href="/briefing" class="mc-test-link">BRIEFING</a>
<a href="/thinking" class="mc-test-link mc-link-thinking">THINKING</a>
<a href="/swarm/mission-control" class="mc-test-link">MISSION CTRL</a>
<a href="/swarm/live" class="mc-test-link">SWARM</a>
<a href="/bugs" class="mc-test-link mc-link-bugs">BUGS</a>
</div>
</div>
<div class="mc-nav-dropdown">
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">AGENTS &#x25BE;</button>
<div class="mc-dropdown-menu">
<a href="/hands" class="mc-test-link">HANDS</a>
<a href="/work-orders/queue" class="mc-test-link">WORK ORDERS</a>
<a href="/self-modify/queue" class="mc-test-link">UPGRADES</a>
<a href="/self-coding" class="mc-test-link">SELF-CODING</a>
</div>
</div>
<div class="mc-nav-dropdown">
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">INTEL &#x25BE;</button>
<div class="mc-dropdown-menu">
<a href="/spark/ui" class="mc-test-link">SPARK</a>
<a href="/memory" class="mc-test-link">MEMORY</a>
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
</div>
</div>
<div class="mc-nav-dropdown">
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">SYSTEM &#x25BE;</button>
<div class="mc-dropdown-menu">
<a href="/tools" class="mc-test-link">TOOLS</a>
<a href="/swarm/events" class="mc-test-link">EVENTS</a>
<a href="/router/status" class="mc-test-link">ROUTER</a>
<a href="/grok/status" class="mc-test-link mc-link-grok">GROK</a>
</div>
</div>
<div class="mc-nav-dropdown">
<button class="mc-test-link mc-dropdown-toggle" aria-expanded="false">MORE &#x25BE;</button>
<div class="mc-dropdown-menu">
<a href="/spark/ui" class="mc-test-link">SPARK</a>
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
<a href="/tools" class="mc-test-link">TOOLS</a>
<a href="/swarm/events" class="mc-test-link">EVENTS</a>
<a href="/lightning/ledger" class="mc-test-link">LEDGER</a>
<a href="/memory" class="mc-test-link">MEMORY</a>
<a href="/router/status" class="mc-test-link">ROUTER</a>
<a href="/grok/status" class="mc-test-link mc-link-grok">GROK</a>
<a href="/self-modify/queue" class="mc-test-link">UPGRADES</a>
<a href="/self-coding" class="mc-test-link">SELF-CODING</a>
<a href="/hands" class="mc-test-link">HANDS</a>
<a href="/work-orders/queue" class="mc-test-link">WORK ORDERS</a>
<a href="/creative/ui" class="mc-test-link">CREATIVE</a>
<a href="/voice/button" class="mc-test-link">VOICE</a>
<a href="/mobile" class="mc-test-link" title="Mobile-optimized view">MOBILE</a>
@@ -70,10 +91,10 @@
</div>
</div>
<div class="mc-nav-dropdown" id="notif-dropdown">
<button id="enable-notifications" class="mc-test-link mc-dropdown-toggle" style="background:none;cursor:pointer;position:relative;" title="Notifications" aria-expanded="false">&#x1F514;<span id="notif-badge" class="notif-badge" style="display:none;"></span></button>
<div class="mc-dropdown-menu" style="right:0;left:auto;min-width:280px;max-height:350px;overflow-y:auto;">
<div id="notif-list" style="padding:6px;">
<div style="color:var(--text-dim);font-size:0.8rem;text-align:center;padding:12px;">Loading...</div>
<button id="enable-notifications" class="mc-test-link mc-dropdown-toggle mc-notif-btn" title="Notifications" aria-expanded="false">&#x1F514;<span id="notif-badge" class="notif-badge"></span></button>
<div class="mc-dropdown-menu mc-notif-menu">
<div id="notif-list" class="mc-notif-list">
<div class="mc-notif-empty">Loading...</div>
</div>
</div>
</div>
@@ -123,7 +144,7 @@
<a href="/mobile" class="mc-mobile-link">MOBILE</a>
<a href="/mobile/local" class="mc-mobile-link">LOCAL AI</a>
<div class="mc-mobile-menu-footer">
<button id="enable-notifications-mobile" class="mc-mobile-link" style="background:none;border:none;cursor:pointer;width:100%;text-align:left;font:inherit;color:inherit;padding:inherit;">&#x1F514; NOTIFICATIONS</button>
<button id="enable-notifications-mobile" class="mc-mobile-link mc-mobile-notif-btn">&#x1F514; NOTIFICATIONS</button>
</div>
</nav>
@@ -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 = '<div style="color:var(--text-dim);font-size:0.8rem;text-align:center;padding:12px;">No recent notifications</div>';
list.innerHTML = '<div class="mc-notif-empty">No recent notifications</div>';
return;
}
list.innerHTML = data.map(function(n) {
return '<div style="padding:6px 8px;border-bottom:1px solid var(--border);font-size:0.8rem;">'
+ '<div style="color:var(--text-bright);">' + (n.title || n.event_type || 'Event') + '</div>'
+ '<div style="color:var(--text-dim);font-size:0.7rem;">' + (n.timestamp || '') + '</div>'
+ '</div>';
}).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() {});
}

View File

@@ -11,7 +11,7 @@
<!-- Agents (HTMX-polled from registry) -->
{% call panel("AGENTS", hx_get="/swarm/agents/sidebar", hx_trigger="every 10s") %}
<div style="font-size:11px; color:var(--text-dim); letter-spacing:.08em;">LOADING...</div>
<div class="mc-loading-placeholder">LOADING...</div>
{% endcall %}
<!-- System Health (HTMX polled) -->

View File

@@ -1,44 +1,44 @@
{% extends "base.html" %}
{% block title %}Mission Control — Timmy Time{% endblock %}
{% block title %}System Overview — Timmy Time{% endblock %}
{% block content %}
<div class="card">
<div class="card-header">
<h2 class="card-title">🎛️ Mission Control</h2>
<h2 class="card-title">System Overview</h2>
<div>
<span class="badge badge-success" id="system-status">Loading...</span>
</div>
</div>
<!-- Sovereignty Score -->
<div style="margin-bottom: 24px;">
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;">
<div style="font-size: 3rem; font-weight: 700;" id="sov-score">-</div>
<div class="sov-section">
<div class="sov-header">
<div class="sov-score-value" id="sov-score">-</div>
<div>
<div style="font-weight: 600;">Sovereignty Score</div>
<div style="font-size: 0.875rem; color: var(--text-muted);" id="sov-label">Calculating...</div>
<div class="sov-score-label">Sovereignty Score</div>
<div class="sov-score-detail" id="sov-label">Calculating...</div>
</div>
</div>
<div style="background: var(--bg-tertiary); height: 8px; border-radius: 4px; overflow: hidden;">
<div id="sov-bar" style="background: var(--success); height: 100%; width: 0%; transition: width 0.5s;"></div>
<div class="sov-bar-track">
<div id="sov-bar" class="sov-bar-fill"></div>
</div>
</div>
<!-- Dependency Grid -->
<h3 style="margin-bottom: 12px;">Dependencies</h3>
<div class="grid grid-2" id="dependency-grid" style="margin-bottom: 24px;">
<p style="color: var(--text-muted);">Loading...</p>
<h3 class="mc-section-heading">Dependencies</h3>
<div class="grid grid-2 mc-section-gap" id="dependency-grid">
<p class="chat-history-placeholder">Loading...</p>
</div>
<!-- Recommendations -->
<h3 style="margin-bottom: 12px;">Recommendations</h3>
<div id="recommendations" style="margin-bottom: 24px;">
<p style="color: var(--text-muted);">Loading...</p>
<h3 class="mc-section-heading">Recommendations</h3>
<div id="recommendations" class="mc-section-gap">
<p class="chat-history-placeholder">Loading...</p>
</div>
<!-- System Metrics -->
<h3 style="margin-bottom: 12px;">System Metrics</h3>
<h3 class="mc-section-heading">System Metrics</h3>
<div class="grid grid-4" id="metrics-grid">
<div class="stat">
<div class="stat-value" id="metric-uptime">-</div>
@@ -60,11 +60,11 @@
</div>
<!-- Grok Mode Toggle -->
<div class="card" style="margin-top: 24px;">
<div class="card mc-card-spaced">
<div class="card-header">
<h2 class="card-title">Grok Mode</h2>
<div>
<span class="badge" id="grok-badge" style="background: #666;">STANDBY</span>
<span class="badge grok-badge-standby" id="grok-badge">STANDBY</span>
</div>
</div>
<div id="grok-toggle-card"
@@ -72,29 +72,26 @@
hx-trigger="load"
hx-target="#grok-toggle-card"
hx-swap="innerHTML">
<div style="border: 2px solid #666; border-radius: 12px; padding: 16px;
background: var(--bg-secondary);">
<div style="display: flex; justify-content: space-between; align-items: center;">
<div class="grok-toggle-box">
<div class="grok-toggle-row">
<div>
<div style="font-weight: 700; font-size: 1.1rem; color: #666;">
<div class="grok-toggle-title">
GROK MODE: LOADING...
</div>
<div style="font-size: 0.8rem; color: var(--text-muted); margin-top: 4px;">
<div class="grok-toggle-desc">
xAI frontier reasoning augmentation
</div>
</div>
<button hx-post="/grok/toggle"
hx-target="#grok-toggle-card"
hx-swap="outerHTML"
style="background: #666; color: #000; border: none;
border-radius: 8px; padding: 8px 20px; cursor: pointer;
font-weight: 700; font-family: inherit;">
class="grok-activate-btn">
ACTIVATE
</button>
</div>
</div>
</div>
<div class="grid grid-3" style="margin-top: 12px;">
<div class="grid grid-3 mc-section-heading">
<div class="stat">
<div class="stat-value" id="grok-requests">0</div>
<div class="stat-label">Grok Queries</div>
@@ -111,14 +108,14 @@
</div>
<!-- Heartbeat Monitor -->
<div class="card" style="margin-top: 24px;">
<div class="card mc-card-spaced">
<div class="card-header">
<h2 class="card-title">Heartbeat Monitor</h2>
<div>
<span class="badge" id="heartbeat-status">Checking...</span>
</div>
</div>
<div class="grid grid-3">
<div class="stat">
<div class="stat-value" id="hb-tick">-</div>
@@ -133,25 +130,25 @@
<div class="stat-label">Model</div>
</div>
</div>
<div style="margin-top: 16px;">
<div id="heartbeat-log" style="height: 100px; overflow-y: auto; background: var(--bg-tertiary); padding: 12px; border-radius: 8px; font-family: monospace; font-size: 0.75rem;">
<div style="color: var(--text-muted);">Waiting for heartbeat...</div>
<div class="hb-log-wrap">
<div id="heartbeat-log" class="hb-log">
<div class="hb-log-placeholder">Waiting for heartbeat...</div>
</div>
</div>
</div>
<!-- Chat History -->
<div class="card" style="margin-top: 24px;">
<div class="card mc-card-spaced">
<div class="card-header">
<h2 class="card-title">💬 Chat History</h2>
<h2 class="card-title">Chat History</h2>
<div>
<button class="btn btn-sm" onclick="loadChatHistory()">Refresh</button>
</div>
</div>
<div id="chat-history" style="max-height: 300px; overflow-y: auto;">
<p style="color: var(--text-muted);">Loading chat history...</p>
<div id="chat-history" class="chat-history-container">
<p class="chat-history-placeholder">Loading chat history...</p>
</div>
</div>
@@ -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 = `<span style="color: var(--text-muted);">[${now}]</span> <span style="color: var(--success);">✓</span> 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 = '<p style="color: var(--text-muted);">Loading...</p>';
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 = '<p style="color: var(--text-muted);">No messages yet</p>';
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 = `
<div style="color: var(--text-muted); text-align: center; padding: 20px;">
<p>Chat history persistence coming soon</p>
<p style="font-size: 0.875rem;">Messages are currently in-memory only</p>
</div>
`;
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);
</script>
{% endblock %}

View File

@@ -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;
}

View File

@@ -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
════════════════════════════════════════════════════ */