New modules/kubernetes.py lists pods and deployments across the 8 app namespaces via the in-cluster management-platform-viewer-sa (read-only, no secrets/exec/log), with metrics-server usage as a best-effort extra that degrades silently when unreachable. New /cluster page and /api/cluster endpoint, nav entry, and templates render pods/deployments grouped by namespace using the existing card/badge design system. Additive only — no existing Docker routes or views touched. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
101 lines
4.3 KiB
HTML
101 lines
4.3 KiB
HTML
{% extends "base.html" %}
|
|
{% block content %}
|
|
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<div class="card-title"><i class="fas fa-diagram-project"></i> Cluster Overview</div>
|
|
<button class="btn btn-ghost btn-sm" onclick="loadCluster()"><i class="fas fa-sync-alt"></i> Refresh</button>
|
|
</div>
|
|
<div class="cluster-summary" id="cluster-summary">
|
|
<div class="stat-card"><div class="stat-number" id="cs-pods">—</div><div class="stat-label">Total Pods</div></div>
|
|
<div class="stat-card"><div class="stat-number" style="color:var(--green);" id="cs-healthy">—</div><div class="stat-label">Healthy</div></div>
|
|
<div class="stat-card"><div class="stat-number" style="color:var(--yellow);" id="cs-degraded">—</div><div class="stat-label">Degraded</div></div>
|
|
<div class="stat-card"><div class="stat-number" style="color:var(--red);" id="cs-failed">—</div><div class="stat-label">Failed</div></div>
|
|
<div class="stat-card"><div class="stat-number is-text" id="cs-deploys">—</div><div class="stat-label">Deployments Healthy</div></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="cluster-note-wrap"></div>
|
|
|
|
<div class="ns-grid" id="ns-grid">
|
|
<div class="empty-state"><i class="fas fa-spinner fa-spin"></i> Loading cluster state…</div>
|
|
</div>
|
|
|
|
<script>
|
|
const NAMESPACES = ['n8n', 'odoo', 'mautic', 'erpnext', 'nextcloud', 'jenkins-agents', 'jenkins', 'management-platform'];
|
|
|
|
function statusClass(health) {
|
|
return health === 'healthy' ? 'status-healthy' : health === 'failed' ? 'status-failed' : 'status-degraded';
|
|
}
|
|
|
|
function renderNamespace(ns, data) {
|
|
const pods = data.pods || [];
|
|
const deploys = data.deployments || [];
|
|
|
|
const deploysHtml = deploys.length ? `
|
|
<div class="ns-deploys-row">
|
|
${deploys.map(d => `
|
|
<span class="deploy-chip">
|
|
<span class="dot ${statusClass(d.health)}"></span>
|
|
${d.name} <span style="color:var(--text3);">${d.ready}/${d.desired}</span>
|
|
</span>
|
|
`).join('')}
|
|
</div>` : '';
|
|
|
|
const podsHtml = pods.length ? `
|
|
<div class="ns-pods-list">
|
|
${pods.map(p => `
|
|
<div class="pod-row">
|
|
<span class="pod-status-dot ${statusClass(p.health)}"></span>
|
|
<span class="pod-name" title="${p.name}">${p.name}</span>
|
|
<span class="pod-ready">${p.ready}</span>
|
|
<span class="pod-restarts ${p.restarts > 0 ? 'warn' : ''}">${p.restarts} restart${p.restarts === 1 ? '' : 's'}</span>
|
|
<span class="pod-age">${p.age}</span>
|
|
</div>
|
|
`).join('')}
|
|
</div>` : '<div class="empty-state" style="padding:20px;"><i class="fas fa-circle-nodes"></i> No pods</div>';
|
|
|
|
return `
|
|
<div class="ns-card">
|
|
<div class="ns-card-header">
|
|
<span class="ns-name">${ns}</span>
|
|
<span class="ns-counts">${pods.length} pod${pods.length === 1 ? '' : 's'} · ${deploys.length} deploy${deploys.length === 1 ? '' : 's'}</span>
|
|
</div>
|
|
${deploysHtml}
|
|
${podsHtml}
|
|
</div>`;
|
|
}
|
|
|
|
async function loadCluster() {
|
|
const grid = document.getElementById('ns-grid');
|
|
const noteWrap = document.getElementById('cluster-note-wrap');
|
|
try {
|
|
const res = await fetch('/api/cluster');
|
|
const data = await res.json();
|
|
|
|
if (!data.available) {
|
|
grid.innerHTML = '<div class="empty-state"><i class="fas fa-triangle-exclamation"></i> Kubernetes API not reachable from this pod</div>';
|
|
noteWrap.innerHTML = '';
|
|
return;
|
|
}
|
|
|
|
const s = data.summary || {};
|
|
document.getElementById('cs-pods').textContent = s.total_pods ?? '—';
|
|
document.getElementById('cs-healthy').textContent = s.healthy ?? '—';
|
|
document.getElementById('cs-degraded').textContent = s.degraded ?? '—';
|
|
document.getElementById('cs-failed').textContent = s.failed ?? '—';
|
|
document.getElementById('cs-deploys').textContent = `${s.deployments_healthy ?? '—'}/${s.total_deployments ?? '—'}`;
|
|
|
|
noteWrap.innerHTML = data.metrics_available ? '' :
|
|
'<div class="cluster-note"><i class="fas fa-info-circle"></i> Resource metrics unavailable — metrics-server not reachable.</div>';
|
|
|
|
grid.innerHTML = NAMESPACES.map(ns => renderNamespace(ns, data.namespaces[ns] || {})).join('');
|
|
} catch (e) {
|
|
grid.innerHTML = `<div class="empty-state" style="color:var(--red);"><i class="fas fa-circle-exclamation"></i> ${e.message}</div>`;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', loadCluster);
|
|
</script>
|
|
{% endblock %}
|