175 lines
6.1 KiB
HTML
175 lines
6.1 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Dashboard - CheddahBot{% endblock %}
|
|
{% block nav_dash_active %}active{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="dashboard-layout">
|
|
|
|
<!-- Ops Panel -->
|
|
<section class="panel" id="ops-panel">
|
|
<h2 class="panel-title">Operations</h2>
|
|
|
|
<!-- Active Executions -->
|
|
<div class="panel-section">
|
|
<h3>Active Executions</h3>
|
|
<div id="active-executions" class="exec-list">
|
|
<span class="text-muted">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loop Health -->
|
|
<div class="panel-section">
|
|
<h3>Loop Health</h3>
|
|
<div id="loop-health" class="loop-grid">
|
|
<span class="text-muted">Loading...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="panel-section">
|
|
<h3>Actions</h3>
|
|
<div class="action-buttons">
|
|
<button class="btn btn-sm"
|
|
hx-post="/api/system/loops/force"
|
|
hx-swap="none"
|
|
hx-on::after-request="showFlash('Force pulse sent')">
|
|
Force Pulse
|
|
</button>
|
|
<button class="btn btn-sm"
|
|
hx-post="/api/system/briefing/force"
|
|
hx-swap="none"
|
|
hx-on::after-request="showFlash('Briefing triggered')">
|
|
Force Briefing
|
|
</button>
|
|
<button class="btn btn-sm"
|
|
hx-post="/api/cache/clear"
|
|
hx-swap="none"
|
|
hx-on::after-request="showFlash('Cache cleared')">
|
|
Clear Cache
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification Feed -->
|
|
<div class="panel-section">
|
|
<h3>Notifications</h3>
|
|
<div id="notification-feed" class="notif-feed">
|
|
<span class="text-muted">Waiting for notifications...</span>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Pipeline Panel -->
|
|
<section class="panel" id="pipeline-panel">
|
|
<h2 class="panel-title">Pipeline</h2>
|
|
<div id="pipeline-content"
|
|
hx-get="/dashboard/pipeline"
|
|
hx-trigger="load, every 120s"
|
|
hx-swap="innerHTML">
|
|
<span class="text-muted">Loading pipeline data...</span>
|
|
</div>
|
|
</section>
|
|
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
// Connect to SSE for live loop updates
|
|
const loopSource = new EventSource('/sse/loops');
|
|
loopSource.addEventListener('loops', function(e) {
|
|
const data = JSON.parse(e.data);
|
|
renderLoopHealth(data.loops);
|
|
renderActiveExecutions(data.executions);
|
|
});
|
|
|
|
// Connect to SSE for notifications
|
|
const notifSource = new EventSource('/sse/notifications');
|
|
notifSource.addEventListener('notification', function(e) {
|
|
const notif = JSON.parse(e.data);
|
|
addNotification(notif.message, notif.category);
|
|
});
|
|
|
|
function renderLoopHealth(loops) {
|
|
const container = document.getElementById('loop-health');
|
|
if (!loops || Object.keys(loops).length === 0) {
|
|
container.innerHTML = '<span class="text-muted">No loop data</span>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
const now = new Date();
|
|
for (const [name, ts] of Object.entries(loops)) {
|
|
let statusClass = 'badge-muted';
|
|
let agoText = 'never';
|
|
if (ts) {
|
|
const dt = new Date(ts);
|
|
const secs = Math.floor((now - dt) / 1000);
|
|
if (secs < 120) {
|
|
statusClass = 'badge-ok';
|
|
agoText = secs + 's ago';
|
|
} else if (secs < 600) {
|
|
statusClass = 'badge-warn';
|
|
agoText = Math.floor(secs / 60) + 'm ago';
|
|
} else {
|
|
statusClass = 'badge-err';
|
|
agoText = Math.floor(secs / 60) + 'm ago';
|
|
}
|
|
}
|
|
html += '<div class="loop-badge ' + statusClass + '">' +
|
|
'<span class="loop-name">' + name + '</span>' +
|
|
'<span class="loop-ago">' + agoText + '</span>' +
|
|
'</div>';
|
|
}
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function renderActiveExecutions(execs) {
|
|
const container = document.getElementById('active-executions');
|
|
if (!execs || Object.keys(execs).length === 0) {
|
|
container.innerHTML = '<span class="text-muted">No active executions</span>';
|
|
return;
|
|
}
|
|
let html = '';
|
|
const now = new Date();
|
|
for (const [id, info] of Object.entries(execs)) {
|
|
const started = new Date(info.started_at);
|
|
const durSecs = Math.floor((now - started) / 1000);
|
|
let dur = durSecs + 's';
|
|
if (durSecs >= 60) dur = Math.floor(durSecs / 60) + 'm ' + (durSecs % 60) + 's';
|
|
html += '<div class="exec-item">' +
|
|
'<span class="exec-name">' + info.name + '</span>' +
|
|
'<span class="exec-tool">' + info.tool + '</span>' +
|
|
'<span class="exec-dur">' + dur + '</span>' +
|
|
'</div>';
|
|
}
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
let notifCount = 0;
|
|
function addNotification(message, category) {
|
|
const container = document.getElementById('notification-feed');
|
|
if (notifCount === 0) container.innerHTML = '';
|
|
notifCount++;
|
|
|
|
const div = document.createElement('div');
|
|
div.className = 'notif-item notif-' + (category || 'info');
|
|
div.innerHTML = '<span class="notif-cat">' + (category || 'info') + '</span> ' + message;
|
|
container.insertBefore(div, container.firstChild);
|
|
|
|
// Keep max 30
|
|
while (container.children.length > 30) {
|
|
container.removeChild(container.lastChild);
|
|
}
|
|
}
|
|
|
|
function showFlash(msg) {
|
|
const el = document.createElement('div');
|
|
el.className = 'flash-msg';
|
|
el.textContent = msg;
|
|
document.body.appendChild(el);
|
|
setTimeout(() => el.remove(), 3000);
|
|
}
|
|
</script>
|
|
{% endblock %}
|