provisioning-platform/crates/provisioning-daemon/ui/templates/pages/workspace_clusters.html

326 lines
14 KiB
HTML

{% extends "base.html" %}
{% import "macros/ui.html" as m %}
{% block title %}{{ ws_name }} · Clusters{% endblock %}
{% block nav_workspaces %}btn-active{% endblock %}
{% block content %}
{{ m::ws_tabs(ws_name=ws_name, active=ws_active_tab, ws_envs=ws_envs, ws_has_clusters=ws_has_clusters, ws_has_workflows=ws_has_workflows, current_env=current_env) }}
{# ── Controls bar ──────────────────────────────────────────────────────────── #}
<div class="flex items-center gap-2 mb-4">
<input type="text" id="search-input" placeholder="search…"
oninput="applySearch()"
class="input input-sm input-bordered w-48 font-mono text-xs" />
<button id="auto-btn" class="btn btn-sm btn-ghost font-mono text-xs"
onclick="toggleAuto()" title="Auto-refresh every 30 s">auto</button>
<a href="/ui/workspaces/{{ ws_name }}/clusters/services"
class="btn btn-sm btn-ghost font-mono text-xs gap-1">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M5 12h14M12 5l7 7-7 7"/>
</svg>
services
</a>
<span class="text-xs text-base-content/40 ml-auto" id="refresh-ts"></span>
</div>
{# ── Live node metrics (cluster top) ──────────────────────────────────────── #}
{% if top %}
{% if top.nodes %}
<div class="mb-6">
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-1">Node Metrics</div>
<div class="overflow-x-auto">
<table class="table table-xs font-mono" id="nodes-table">
<thead>
<tr class="text-base-content/40 text-xs uppercase tracking-wider">
<th>Node</th>
<th class="text-right">CPU</th>
<th class="text-right">CPU%</th>
<th class="text-right">Mem</th>
<th class="text-right">Mem%</th>
</tr>
</thead>
<tbody>
{% for n in top.nodes %}
<tr class="hover" data-name="{{ n.node }}">
<td class="font-semibold text-primary">{{ n.node }}</td>
<td class="text-right tabular-nums">{{ n.cpu }}</td>
<td class="text-right tabular-nums">
{% set pct = n.cpu_pct | replace(from="%", to="") | int(default=0) %}
<span class="{% if pct >= 80 %}text-error{% elif pct >= 60 %}text-warning{% else %}text-success{% endif %}">
{{ n.cpu_pct }}
</span>
</td>
<td class="text-right tabular-nums">{{ n.mem }}</td>
<td class="text-right tabular-nums">
{% set mpct = n.mem_pct | replace(from="%", to="") | int(default=0) %}
<span class="{% if mpct >= 80 %}text-error{% elif mpct >= 60 %}text-warning{% else %}text-success{% endif %}">
{{ n.mem_pct }}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="text-xs text-base-content/30 mt-1">
{{ top.nodes | length }} node(s) — kubectl top (live)
&nbsp;·&nbsp;
<button class="link link-accent text-xs" onclick="loadTopPods()">show top pods</button>
</p>
</div>
{% endif %}
{% else %}
<div class="alert alert-ghost mb-4 py-2">
<span class="text-xs text-base-content/40">Node metrics unavailable — metrics-server may not be reachable or no control-plane resolved.</span>
</div>
{% endif %}
{# ── Pod top panel (loaded on demand) ─────────────────────────────────────── #}
<div id="pod-top-panel" class="hidden mb-6">
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-1">Top Pods by Memory</div>
<div class="overflow-x-auto">
<table class="table table-xs font-mono">
<thead>
<tr class="text-base-content/40 text-xs uppercase">
<th>Namespace</th><th>Pod</th>
<th class="text-right">CPU</th><th class="text-right">Mem</th>
</tr>
</thead>
<tbody id="pod-top-body"></tbody>
</table>
</div>
</div>
{# ── Cluster summary badge (from .cluster-state.json cache) ──────────────── #}
{% if cluster_summary %}
<div class="flex items-center gap-3 mb-4 font-mono text-xs text-base-content/50 bg-base-200 rounded-box px-4 py-2 flex-wrap">
{% if cluster_summary.cp_ip %}<span class="text-primary font-semibold">CP: {{ cluster_summary.cp_ip }}</span>{% endif %}
{% if cluster_summary.nodes_total and cluster_summary.nodes_total > 0 %}
<span>{{ cluster_summary.nodes_ready }}/{{ cluster_summary.nodes_total }} nodes</span>
{% endif %}
{% if cluster_summary.pods_total and cluster_summary.pods_total > 0 %}
<span>{{ cluster_summary.pods_running }}/{{ cluster_summary.pods_total }} pods</span>
{% endif %}
{% if cluster_summary.namespaces and cluster_summary.namespaces > 0 %}
<span>{{ cluster_summary.namespaces }} ns</span>
{% endif %}
{% if cluster_summary.deployments and cluster_summary.deployments > 0 %}
<span>{{ cluster_summary.deployments }} dply</span>
{% endif %}
{% if cluster_summary.pvcs_bound and cluster_summary.pvcs_bound > 0 %}
<span>{{ cluster_summary.pvcs_bound }} pvcs</span>
{% endif %}
{% if cluster_summary.version %}<span class="text-base-content/30">{{ cluster_summary.version }}</span>{% endif %}
{% if cluster_summary.last_sync %}<span class="text-base-content/20 ml-auto">cache {{ cluster_summary.last_sync | truncate(length=16, end="") }}</span>{% endif %}
</div>
{% endif %}
{# ── Cluster-mode components — matches `prvng cl l` ───────────────────────── #}
{% if cluster_components %}
<div class="mb-6">
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-1">Cluster Components</div>
<div class="overflow-x-auto">
<table class="table table-sm font-mono" id="comp-table">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>#</th>
<th>Name</th>
<th>Namespace</th>
<th>Version</th>
<th>Class</th>
<th>Resources</th>
<th>Placement</th>
<th>Deploy</th>
<th title="Live: pod count or hcloud status">Live</th>
</tr>
</thead>
<tbody>
{% for comp in cluster_components %}
<tr class="hover" data-name="{{ comp.name }}">
<td class="text-base-content/30 tabular-nums text-xs">{{ loop.index0 }}</td>
<td>
<a href="/ui/workspaces/{{ ws_name }}/components/{{ comp.name }}"
class="text-primary font-semibold hover:underline">{{ comp.name }}</a>
</td>
<td class="text-base-content/60 text-xs">
{% if comp.namespace %}{{ comp.namespace }}{% else %}<span class="text-base-content/20"></span>{% endif %}
</td>
<td class="text-base-content/60 text-xs">
{% if comp.version %}{{ comp.version }}{% else %}<span class="text-base-content/20"></span>{% endif %}
</td>
<td class="text-base-content/70 text-xs">
{% if comp.class %}
<span class="badge badge-ghost badge-xs">{{ comp.class }}</span>
{% else %}<span class="text-base-content/20"></span>{% endif %}
</td>
<td class="text-base-content/60 text-xs whitespace-nowrap">
{% if comp.resources and comp.resources != "—" %}{{ comp.resources }}{% else %}<span class="text-base-content/20"></span>{% endif %}
</td>
<td class="text-base-content/60 text-xs">
{% if comp.placement and comp.placement != "—" %}{{ comp.placement }}{% else %}<span class="text-base-content/20"></span>{% endif %}
</td>
<td>
{% if comp.deploy == "completed" %}
<span class="badge badge-success badge-xs">completed</span>
{% elif comp.deploy == "pending" %}
<span class="badge badge-ghost badge-xs">pending</span>
{% elif comp.deploy == "failed" %}
<span class="badge badge-error badge-xs">failed</span>
{% elif comp.deploy %}
<span class="badge badge-outline badge-xs">{{ comp.deploy }}</span>
{% else %}
<span class="text-base-content/20"></span>
{% endif %}
</td>
<td>
{% if comp.live and comp.live != "" %}
<span class="badge badge-xs font-mono badge-success">{{ comp.live }}</span>
{% elif comp.ncl_state and comp.ncl_state != "" %}
{% if comp.ncl_state == "completed" %}
<span class="badge badge-success badge-xs">{{ comp.ncl_state }}</span>
{% elif comp.ncl_state == "running" %}
<span class="badge badge-warning badge-xs">{{ comp.ncl_state }}</span>
{% elif comp.ncl_state == "failed" %}
<span class="badge badge-error badge-xs">{{ comp.ncl_state }}</span>
{% else %}
<span class="badge badge-ghost badge-xs">{{ comp.ncl_state }}</span>
{% endif %}
{% else %}
<span class="text-base-content/20"></span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="text-xs text-base-content/30 mt-2">{{ cluster_components | length }} component(s)</p>
</div>
{% endif %}
{# ── Cluster definitions (clusters/*.ncl) ─────────────────────────────────── #}
{% if clusters %}
{% if clusters.items %}
<div class="mb-2">
<div class="text-xs font-semibold text-base-content/50 uppercase tracking-wider mb-1">Cluster Definitions</div>
<div class="overflow-x-auto">
<table class="table table-sm font-mono" id="clusters-table">
<thead>
<tr class="text-base-content/50 text-xs uppercase tracking-wider">
<th>Name</th>
<th>Workspace</th>
<th>Env</th>
<th>Labels</th>
</tr>
</thead>
<tbody>
{% for cl in clusters.items %}
<tr class="hover" data-name="{{ cl.name }}">
<td class="font-semibold text-primary">{{ cl.name }}</td>
<td class="text-base-content/60 text-xs">{{ cl.workspace | default(value="—") }}</td>
<td class="text-base-content/50 text-xs">{{ cl.env | default(value="—") }}</td>
<td>
{% if cl.config.labels %}
{% for k, v in cl.config.labels %}
<span class="badge badge-ghost badge-xs">{{ k }}={{ v }}</span>
{% endfor %}
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="text-xs text-base-content/30 mt-1">{{ clusters.total | default(value=clusters.items | length) }} cluster definition(s)</p>
</div>
{% endif %}
{% endif %}
{% if not cluster_components and not clusters %}
<div class="alert alert-warning">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-2.194-.833-2.964 0L3.232 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<span>Cluster data unavailable — no cluster provider configured.</span>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
<script>
var WS_NAME = {{ ws_name | json_encode() | safe }};
var _autoTimer = null;
function applySearch() {
const q = document.getElementById('search-input').value.toLowerCase();
for (const tbl of ['nodes-table', 'comp-table', 'clusters-table']) {
const t = document.getElementById(tbl);
if (!t) continue;
t.querySelectorAll('tbody tr').forEach(row => {
const name = (row.dataset.name || '').toLowerCase();
row.style.display = (!q || name.includes(q)) ? '' : 'none';
});
}
}
function toggleAuto() {
const btn = document.getElementById('auto-btn');
if (_autoTimer) {
clearInterval(_autoTimer);
_autoTimer = null;
btn.classList.remove('btn-accent');
} else {
_autoTimer = setInterval(refreshTop, 30000);
btn.classList.add('btn-accent');
refreshTop();
}
}
async function refreshTop() {
try {
const res = await fetch(`/ui/workspaces/${WS_NAME}/clusters/top`);
if (!res.ok) return;
const data = await res.json();
const tbody = document.querySelector('#nodes-table tbody');
if (!tbody || !data.nodes) return;
tbody.innerHTML = data.nodes.map(n => {
const cp = parseInt(n.cpu_pct) || 0;
const mp = parseInt(n.mem_pct) || 0;
const cpCls = cp >= 80 ? 'text-error' : cp >= 60 ? 'text-warning' : 'text-success';
const mpCls = mp >= 80 ? 'text-error' : mp >= 60 ? 'text-warning' : 'text-success';
return `<tr class="hover" data-name="${n.node}">
<td class="font-semibold text-primary">${n.node}</td>
<td class="text-right tabular-nums">${n.cpu}</td>
<td class="text-right tabular-nums"><span class="${cpCls}">${n.cpu_pct}</span></td>
<td class="text-right tabular-nums">${n.mem}</td>
<td class="text-right tabular-nums"><span class="${mpCls}">${n.mem_pct}</span></td>
</tr>`;
}).join('');
document.getElementById('refresh-ts').textContent = new Date().toLocaleTimeString();
applySearch();
} catch (_) {}
}
async function loadTopPods() {
const panel = document.getElementById('pod-top-panel');
const tbody = document.getElementById('pod-top-body');
try {
const res = await fetch(`/ui/workspaces/${WS_NAME}/clusters/top?pods=true`);
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="4" class="text-error">failed</td></tr>'; return; }
const data = await res.json();
tbody.innerHTML = (data.pods || []).map(p =>
`<tr class="hover">
<td class="text-base-content/60 text-xs">${p.namespace}</td>
<td class="font-mono text-xs">${p.pod}</td>
<td class="text-right tabular-nums text-xs">${p.cpu}</td>
<td class="text-right tabular-nums text-xs">${p.mem}</td>
</tr>`
).join('') || '<tr><td colspan="4" class="text-base-content/30">no pods</td></tr>';
panel.classList.remove('hidden');
} catch (_) {}
}
</script>
{% endblock %}