Files
foodlinkk-command-center/cockpit/cto-dashboard.html
T

196 lines
8.9 KiB
HTML
Raw Normal View History

{% extends "base.html" %}
{% block extra_head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<link rel="stylesheet" href="/static/css/herman-dashboard.css?v=17" />
<link rel="stylesheet" href="/static/css/ops-topology.css?v=3" />
{% endblock %}
{% block content %}
<div class="herman-shell hm-neo" x-data="ctoDashboard()" x-init="init()">
<div class="herman-hero">
<h1>CTO Dashboard <span class="live-badge">Live</span></h1>
<p class="hero-sub">Platform · agents · infra · DevOps · goedkeuringen</p>
<div class="hm-hero-links beurs-mini-row" style="margin-top:0.85rem">
<a href="/ops" class="beurs-mini-chip">🖥️ IT Ops</a>
<a href="/agents?tab=mesh" class="beurs-mini-chip">🕸️ Agent mesh</a>
<a href="/agents?tab=approvals" class="beurs-mini-chip">✓ Goedkeuringen</a>
<a href="/browser" class="beurs-mini-chip">🌐 Browser agent</a>
<a href="/settings?tab=social" class="beurs-mini-chip">⚙️ Social API's</a>
<a href="http://10.4.7.18:3001/aissa/foodlinkk-command-center" target="_blank" rel="noopener" class="beurs-mini-chip">📦 Gitea</a>
<a href="http://10.4.7.18:8700/docs" target="_blank" rel="noopener" class="beurs-mini-chip">📡 Tools API</a>
<a href="http://10.4.7.18:3002" target="_blank" rel="noopener" class="beurs-mini-chip">📈 Grafana</a>
</div>
</div>
<section class="panel herman-briefing">
<header class="herman-briefing-header">
<div>
<h2>Platform status</h2>
<div class="briefing-meta" x-text="opsMeta">Laden…</div>
</div>
<div class="herman-briefing-actions">
<button type="button" class="btn btn-pulse" @click="refresh()" :disabled="loading">↻ Verversen</button>
<a href="/ops" class="btn btn-pulse-green">Volledige IT Ops →</a>
</div>
</header>
<div class="hm-neo-kpi-row" style="margin-bottom:1.25rem">
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-label">Health</div>
<div class="hm-neo-kpi-value" x-text="summary.health || '—'"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-label">Online nodes</div>
<div class="hm-neo-kpi-value" x-text="summary.counts?.online ?? '—'"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-label">Services</div>
<div class="hm-neo-kpi-value" x-text="meta.services_online ?? '—'"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-label">Open goedkeuringen</div>
<div class="hm-neo-kpi-value" x-text="pendingApprovals"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-label">Agent events (24u)</div>
<div class="hm-neo-kpi-value" x-text="agentEvents24h"></div>
</div>
<div class="hm-neo-kpi">
<div class="hm-neo-kpi-label">Browser sessies (24u)</div>
<div class="hm-neo-kpi-value">{{ kpis.browser_sessions_24h }}</div>
</div>
</div>
<div class="hm-dash-two-col">
<section class="hm-dash-section" data-widget="approvals">
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Agent goedkeuringen</h3>
<div class="panel" style="background:transparent;border:none;padding:0">
<p class="page-tip">SysOps vraagt toestemming voor <strong>update-scan</strong> en <strong>Gitea backup</strong>.</p>
<div id="cto-approvals-list"></div>
</div>
</section>
<section class="hm-dash-section">
<h3 class="hm-section-title"><span class="hm-live-dot"></span> Maintenance</h3>
<template x-if="!maintenance.length">
<p class="empty-state">Geen open maintenance items 🎉</p>
</template>
<div class="ops-maint-list">
<template x-for="n in maintenance.slice(0, 6)" :key="n.id">
<div class="ops-maint-item" :class="'sev-'+(n.severity||'info')">
<strong x-text="n.title"></strong>
<small x-text="(n.created_at||'').substring(0,16).replace('T',' ')"></small>
</div>
</template>
</div>
<p class="page-tip" style="margin-top:0.75rem"><a href="/ops">Alle maintenance →</a></p>
</section>
</div>
<div class="hub-grid hm-dash-feed" style="margin-top:1.25rem">
<section class="panel">
<h2>Agent feed <span class="live-badge" id="cto-feed-badge">Live</span></h2>
<div id="cto-agent-feed">
{% for ev in agent_feed[:12] %}
<div class="hub-feed-item">
<span>{{ ev.created_at[11:16] if ev.created_at else '' }}</span>
<span class="badge badge-agent-{{ ev.agent_name|lower }}">{{ ev.agent_name }}</span>
{{ ev.title or ev.event_type }}
</div>
{% else %}<p class="empty-state">Nog geen events.</p>{% endfor %}
</div>
</section>
<section class="panel">
<h2>Browser sessies</h2>
{% if browser_sessions %}
{% for s in browser_sessions[:6] %}
<div class="hub-feed-item">
<span>{{ s.created_at[11:16] if s.created_at else '' }}</span>
<span class="badge badge-agent-browser">browser</span>
{{ s.title or s.url or s.task }}
</div>
{% endfor %}
{% else %}
<p class="empty-state">Geen recente browser-sessies.</p>
{% endif %}
<p class="page-tip" style="margin-top:0.75rem"><a href="/browser">Browser agent →</a></p>
</section>
</div>
</section>
</div>
{% endblock %}
{% block scripts %}
<script>
function ctoDashboard() {
return {
loading: false,
summary: {},
meta: {},
maintenance: [],
opsMeta: 'Laden…',
pendingApprovals: {{ kpis.pending_approvals }},
agentEvents24h: 0,
_pollStop: null,
_wsStop: null,
async init() {
localStorage.setItem('foodlinkk-persona', 'cto');
await this.refresh(false);
await this.loadApprovals();
this._pollStop = CockpitLive.startPolling(() => { this.refresh(false); this.loadApprovals(); }, 30000);
this._wsStop = CockpitLive.connectFeed((data) => {
if (data.events) CockpitLive.renderAgentFeed(document.getElementById('cto-agent-feed'), data.events);
CockpitLive.updateLiveBadge(document.getElementById('cto-feed-badge'), new Date().toLocaleTimeString('nl-NL', {hour:'2-digit',minute:'2-digit', timeZone:'Europe/Amsterdam'}));
});
},
async refresh(toast) {
this.loading = true;
try {
const [status, maint, analytics] = await Promise.all([
fetch('/api/ops/status').then(x => x.json()).catch(() => ({})),
fetch('/api/ops/maintenance?limit=12').then(x => x.json()).catch(() => ({ items: [] })),
fetch('/analytics/api/data').then(x => x.json()).catch(() => ({})),
]);
this.summary = status || { counts: {} };
this.meta = (status.topology && status.topology.meta) || status.sources || {};
this.maintenance = (maint.items || []).filter(n => !n.resolved);
this.opsMeta = 'Sync: ' + new Date().toLocaleTimeString('nl-NL', { timeZone: 'Europe/Amsterdam' });
if (analytics.kpis) {
this.pendingApprovals = analytics.kpis.pending_approvals ?? this.pendingApprovals;
this.agentEvents24h = analytics.kpis.agent_events ?? 0;
}
if (toast !== false) Cockpit.toast('Platform data bijgewerkt', 'success');
} catch (e) {
if (toast !== false) Cockpit.toast(e.message || 'Fout', 'error');
}
this.loading = false;
},
async loadApprovals() {
try {
const r = await fetch('/api/agents/approvals?status=pending&limit=10').then(x => x.json());
const el = document.getElementById('cto-approvals-list');
if (!el) return;
const items = r.items || [];
this.pendingApprovals = items.length;
const actionLabel = (t) => ({ maintenance_scan: '🔍 Update-scan', config_backup: '💾 Gitea backup' }[t] || t);
if (!items.length) { el.innerHTML = '<p class="empty-state">Geen open goedkeuringen 🎉</p>'; return; }
el.innerHTML = '<table class="data-table"><thead><tr><th>Agent</th><th>Type</th><th>Verzoek</th><th></th></tr></thead><tbody>' +
items.map(i => '<tr><td>@'+i.agent_key+'</td><td>'+actionLabel(i.action_type)+'</td><td>'+i.title+'</td><td class="approval-dash-row">' +
'<button class="btn btn-sm btn-approve" data-aid="'+i.id+'">✓</button>' +
'<button class="btn btn-sm btn-reject" data-rid="'+i.id+'">✗</button></td></tr>').join('') +
'</tbody></table>';
el.querySelectorAll('[data-aid]').forEach(btn => btn.addEventListener('click', async () => {
await Cockpit.api('/api/agents/approvals/' + btn.dataset.aid + '/approve', { method: 'POST', body: '{}' });
Cockpit.toast('Goedgekeurd', 'success'); await this.loadApprovals();
}));
el.querySelectorAll('[data-rid]').forEach(btn => btn.addEventListener('click', async () => {
await Cockpit.api('/api/agents/approvals/' + btn.dataset.rid + '/reject', { method: 'POST', body: JSON.stringify({ reason: '' }) });
Cockpit.toast('Afgewezen', 'info'); await this.loadApprovals();
}));
} catch (e) {}
}
};
}
</script>
{% endblock %}