forked from Rockachopa/Timmy-time-dashboard
polish: streamline nav, extract inline styles, improve tablet UX (#168)
This commit is contained in:
committed by
GitHub
parent
b028b768c9
commit
708c8a2477
@@ -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 ▾</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 ▾</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 ▾</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 ▾</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 ▾</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">🔔<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">🔔<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;">🔔 NOTIFICATIONS</button>
|
||||
<button id="enable-notifications-mobile" class="mc-mobile-link mc-mobile-notif-btn">🔔 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() {});
|
||||
}
|
||||
|
||||
@@ -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) -->
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
════════════════════════════════════════════════════ */
|
||||
|
||||
Reference in New Issue
Block a user