forked from Rockachopa/Timmy-time-dashboard
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
181 lines
7.4 KiB
HTML
181 lines
7.4 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Timmy Time — Tower{% endblock %}
|
|
|
|
{% block extra_styles %}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid tower-container py-3">
|
|
|
|
<div class="tower-header">
|
|
<div class="tower-title">TOWER</div>
|
|
<div class="tower-subtitle">
|
|
Real-time Spark visualization —
|
|
<span id="tower-conn" class="tower-conn-badge tower-conn-connecting">CONNECTING</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
|
|
<!-- Left: THINKING (events) -->
|
|
<div class="col-12 col-lg-4 d-flex flex-column gap-3">
|
|
<div class="card mc-panel tower-phase-card">
|
|
<div class="card-header mc-panel-header tower-phase-thinking">// THINKING</div>
|
|
<div class="card-body p-3 tower-scroll" id="tower-events">
|
|
<div class="tower-empty">Waiting for Spark data…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Middle: PREDICTING (EIDOS) -->
|
|
<div class="col-12 col-lg-4 d-flex flex-column gap-3">
|
|
<div class="card mc-panel tower-phase-card">
|
|
<div class="card-header mc-panel-header tower-phase-predicting">// PREDICTING</div>
|
|
<div class="card-body p-3" id="tower-predictions">
|
|
<div class="tower-empty">Waiting for Spark data…</div>
|
|
</div>
|
|
</div>
|
|
<div class="card mc-panel">
|
|
<div class="card-header mc-panel-header">// EIDOS STATS</div>
|
|
<div class="card-body p-3">
|
|
<div class="tower-stat-grid" id="tower-stats">
|
|
<div class="tower-stat"><span class="tower-stat-label">EVENTS</span><span class="tower-stat-value" id="ts-events">0</span></div>
|
|
<div class="tower-stat"><span class="tower-stat-label">MEMORIES</span><span class="tower-stat-value" id="ts-memories">0</span></div>
|
|
<div class="tower-stat"><span class="tower-stat-label">PREDICTIONS</span><span class="tower-stat-value" id="ts-preds">0</span></div>
|
|
<div class="tower-stat"><span class="tower-stat-label">ACCURACY</span><span class="tower-stat-value" id="ts-accuracy">—</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: ADVISING -->
|
|
<div class="col-12 col-lg-4 d-flex flex-column gap-3">
|
|
<div class="card mc-panel tower-phase-card">
|
|
<div class="card-header mc-panel-header tower-phase-advising">// ADVISING</div>
|
|
<div class="card-body p-3 tower-scroll" id="tower-advisories">
|
|
<div class="tower-empty">Waiting for Spark data…</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
var ws = null;
|
|
var badge = document.getElementById('tower-conn');
|
|
|
|
function setConn(state) {
|
|
badge.textContent = state.toUpperCase();
|
|
badge.className = 'tower-conn-badge tower-conn-' + state;
|
|
}
|
|
|
|
function esc(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
|
|
|
function renderEvents(events) {
|
|
var el = document.getElementById('tower-events');
|
|
if (!events || !events.length) { el.innerHTML = '<div class="tower-empty">No events captured yet.</div>'; return; }
|
|
var html = '';
|
|
for (var i = 0; i < events.length; i++) {
|
|
var ev = events[i];
|
|
var dots = ev.importance >= 0.8 ? '\u25cf\u25cf\u25cf' : ev.importance >= 0.5 ? '\u25cf\u25cf' : '\u25cf';
|
|
html += '<div class="tower-event tower-etype-' + esc(ev.event_type) + '">'
|
|
+ '<div class="tower-ev-head">'
|
|
+ '<span class="tower-ev-badge">' + esc(ev.event_type.replace(/_/g, ' ').toUpperCase()) + '</span>'
|
|
+ '<span class="tower-ev-dots">' + dots + '</span>'
|
|
+ '</div>'
|
|
+ '<div class="tower-ev-desc">' + esc(ev.description) + '</div>'
|
|
+ '<div class="tower-ev-time">' + esc((ev.created_at || '').slice(0, 19)) + '</div>'
|
|
+ '</div>';
|
|
}
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
function renderPredictions(preds) {
|
|
var el = document.getElementById('tower-predictions');
|
|
if (!preds || !preds.length) { el.innerHTML = '<div class="tower-empty">No predictions yet.</div>'; return; }
|
|
var html = '';
|
|
for (var i = 0; i < preds.length; i++) {
|
|
var p = preds[i];
|
|
var cls = p.evaluated ? 'tower-pred-done' : 'tower-pred-pending';
|
|
var accTxt = p.accuracy != null ? Math.round(p.accuracy * 100) + '%' : 'PENDING';
|
|
var accCls = p.accuracy != null ? (p.accuracy >= 0.7 ? 'text-success' : p.accuracy < 0.4 ? 'text-danger' : 'text-warning') : '';
|
|
html += '<div class="tower-pred ' + cls + '">'
|
|
+ '<div class="tower-pred-head">'
|
|
+ '<span class="tower-pred-task">' + esc(p.task_id) + '</span>'
|
|
+ '<span class="tower-pred-acc ' + accCls + '">' + accTxt + '</span>'
|
|
+ '</div>';
|
|
if (p.predicted) {
|
|
var pr = p.predicted;
|
|
html += '<div class="tower-pred-detail">';
|
|
if (pr.likely_winner) html += '<span>Winner: ' + esc(pr.likely_winner.slice(0, 8)) + '</span> ';
|
|
if (pr.success_probability != null) html += '<span>Success: ' + Math.round(pr.success_probability * 100) + '%</span> ';
|
|
html += '</div>';
|
|
}
|
|
html += '<div class="tower-ev-time">' + esc((p.created_at || '').slice(0, 19)) + '</div>'
|
|
+ '</div>';
|
|
}
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
function renderAdvisories(advs) {
|
|
var el = document.getElementById('tower-advisories');
|
|
if (!advs || !advs.length) { el.innerHTML = '<div class="tower-empty">No advisories yet.</div>'; return; }
|
|
var html = '';
|
|
for (var i = 0; i < advs.length; i++) {
|
|
var a = advs[i];
|
|
var prio = a.priority >= 0.7 ? 'high' : a.priority >= 0.4 ? 'medium' : 'low';
|
|
html += '<div class="tower-advisory tower-adv-' + prio + '">'
|
|
+ '<div class="tower-adv-head">'
|
|
+ '<span class="tower-adv-cat">' + esc(a.category.replace(/_/g, ' ').toUpperCase()) + '</span>'
|
|
+ '<span class="tower-adv-prio">' + Math.round(a.priority * 100) + '%</span>'
|
|
+ '</div>'
|
|
+ '<div class="tower-adv-title">' + esc(a.title) + '</div>'
|
|
+ '<div class="tower-adv-detail">' + esc(a.detail) + '</div>'
|
|
+ '<div class="tower-adv-action">' + esc(a.suggested_action) + '</div>'
|
|
+ '</div>';
|
|
}
|
|
el.innerHTML = html;
|
|
}
|
|
|
|
function renderStats(status) {
|
|
if (!status) return;
|
|
document.getElementById('ts-events').textContent = status.events_captured || 0;
|
|
document.getElementById('ts-memories').textContent = status.memories_stored || 0;
|
|
var p = status.predictions || {};
|
|
document.getElementById('ts-preds').textContent = p.total_predictions || 0;
|
|
var acc = p.avg_accuracy;
|
|
var accEl = document.getElementById('ts-accuracy');
|
|
if (acc != null) {
|
|
accEl.textContent = Math.round(acc * 100) + '%';
|
|
accEl.className = 'tower-stat-value ' + (acc >= 0.7 ? 'text-success' : acc < 0.4 ? 'text-danger' : 'text-warning');
|
|
} else {
|
|
accEl.textContent = '\u2014';
|
|
}
|
|
}
|
|
|
|
function handleMsg(data) {
|
|
if (data.type !== 'spark_state') return;
|
|
renderEvents(data.events);
|
|
renderPredictions(data.predictions);
|
|
renderAdvisories(data.advisories);
|
|
renderStats(data.status);
|
|
}
|
|
|
|
function connect() {
|
|
var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
ws = new WebSocket(proto + '//' + location.host + '/tower/ws');
|
|
ws.onopen = function() { setConn('live'); };
|
|
ws.onclose = function() { setConn('offline'); setTimeout(connect, 3000); };
|
|
ws.onerror = function() { setConn('offline'); };
|
|
ws.onmessage = function(e) {
|
|
try { handleMsg(JSON.parse(e.data)); } catch(err) { console.error('Tower WS parse error', err); }
|
|
};
|
|
}
|
|
|
|
connect();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|