This commit is contained in:
Your Name
2026-07-08 11:25:50 +08:00
parent 535e5832ab
commit 8945073ec6
25 changed files with 0 additions and 2497 deletions
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Palladium Z1 Monitor{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
{% block head %}{% endblock %}
</head>
<body>
<nav class="topnav">
<div class="nav-brand">
<span class="nav-logo"></span>
<span class="nav-title">Palladium Z1 Monitor</span>
</div>
<div class="nav-links">
<a href="{{ url_for('dashboard') }}" class="{% if request.endpoint == 'dashboard' %}active{% endif %}">仪表板</a>
<a href="{{ url_for('history') }}" class="{% if request.endpoint == 'history' %}active{% endif %}">历史记录</a>
<button class="nav-btn" onclick="openPasteModal()">📥 粘贴数据</button>
</div>
</nav>
<main class="container">
{% block content %}{% endblock %}
</main>
<!-- 粘贴数据弹窗 -->
<div id="pasteModal" class="modal-overlay" style="display:none;">
<div class="modal">
<div class="modal-header">
<h2>📥 粘贴 test_server -short 输出</h2>
<button class="modal-close" onclick="closePasteModal()"></button>
</div>
<form action="{{ url_for('paste_data') }}" method="POST">
<div class="modal-body">
<p class="modal-hint">在 Palladium 主机执行 <code>test_server -short</code>,将输出粘贴到下方:</p>
<textarea name="raw_output" rows="20" class="paste-textarea"
placeholder="Emulator: sc01_emu Hardware: Palladium Z1 ..."></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closePasteModal()">取消</button>
<button type="submit" class="btn btn-primary">解析并存储</button>
</div>
</form>
</div>
</div>
<script>
function openPasteModal() {
document.getElementById('pasteModal').style.display = 'flex';
}
function closePasteModal() {
document.getElementById('pasteModal').style.display = 'none';
}
document.getElementById('pasteModal').addEventListener('click', function(e) {
if (e.target === this) closePasteModal();
});
</script>
{% block scripts %}{% endblock %}
</body>
</html>
+394
View File
@@ -0,0 +1,394 @@
{% extends "base.html" %}
{% block title %}仪表板 — Palladium Z1 Monitor{% endblock %}
{% block head %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
{% endblock %}
{% block content %}
{% if snapshot %}
<!-- ═══ 系统信息栏 ═══ -->
<div class="system-bar">
<div class="sys-item">
<span class="sys-label">仿真器</span>
<span class="sys-value">{{ snapshot.emulator }}</span>
</div>
<div class="sys-item">
<span class="sys-label">硬件</span>
<span class="sys-value">{{ snapshot.hardware }}</span>
</div>
<div class="sys-item">
<span class="sys-label">ConfigMgr</span>
<span class="sys-value">{{ snapshot.configmgr }}</span>
</div>
<div class="sys-item">
<span class="sys-label">系统状态</span>
{% set ss = snapshot.system_status.upper() %}
{% if ss == 'OK' %}
<span class="status-badge status-ok">● {{ snapshot.system_status }}</span>
{% elif 'PARTIAL' in ss %}
<span class="status-badge status-warning">● {{ snapshot.system_status }}</span>
{% elif 'DOWN' in ss or 'ERROR' in ss %}
<span class="status-badge status-error">● {{ snapshot.system_status }}</span>
{% else %}
<span class="status-badge status-unknown">● {{ snapshot.system_status }}</span>
{% endif %}
</div>
<div class="sys-item">
<span class="sys-label">最后更新</span>
<span class="sys-value" id="lastUpdate">{{ snapshot.timestamp | fmt_time }}</span>
</div>
<div class="sys-item">
<span class="sys-label">数据来源</span>
<span class="sys-value">{{ snapshot.source }}</span>
</div>
<div class="sys-item refresh-controls">
<label><input type="checkbox" id="autoRefresh" checked> 自动刷新</label>
<select id="refreshInterval">
<option value="15">15s</option>
<option value="30" selected>30s</option>
<option value="60">1m</option>
<option value="300">5m</option>
</select>
<button class="btn-sm" onclick="location.reload()">🔄</button>
</div>
</div>
<!-- ═══ 指标卡片 ═══ -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">🖥️</div>
<div class="metric-body">
<div class="metric-label">逻辑板总数</div>
<div class="metric-value">{{ stats.total_boards }}</div>
<div class="metric-sub">
<span class="text-green">在线 {{ stats.online_boards }}</span> ·
<span class="text-red">离线 {{ stats.offline_boards }}</span>
</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">📊</div>
<div class="metric-body">
<div class="metric-label">逻辑板利用率</div>
<div class="metric-value {{ 'metric-warn' if stats.utilization > 80 else '' }}">{{ stats.utilization }}%</div>
<div class="metric-sub">
使用 {{ stats.used_boards }} / 空闲 {{ stats.online_boards - stats.used_boards }}
</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">🔲</div>
<div class="metric-body">
<div class="metric-label">逻辑板状态</div>
<div class="metric-value">{{ stats.total_boards }}</div>
<div class="metric-sub">
<span class="text-green">● {{ stats.online_boards - stats.used_boards }} 空闲</span>
<span class="text-blue">● {{ stats.used_boards }} 使用</span>
<span class="text-red">● {{ stats.offline_boards }} 离线</span>
</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">⚙️</div>
<div class="metric-body">
<div class="metric-label">活跃作业</div>
<div class="metric-value">{{ stats.active_jobs }}</div>
<div class="metric-sub">
{% if jobs %}
{{ jobs[0].owner }} · {{ jobs[0].elap_time }}
{% else %}
无活跃作业
{% endif %}
</div>
</div>
</div>
</div>
<!-- ═══ Rack / Cluster / Board 可视化 ═══ -->
<div class="section-header">
<h2>🏗️ 逻辑板状态</h2>
<div class="legend">
<span class="legend-item"><span class="domain-cell domain-available">-</span> 可用</span>
<span class="legend-item"><span class="domain-cell domain-downloaded">1</span> 已加载</span>
<span class="legend-item"><span class="domain-cell domain-disabled">D</span> 已禁用</span>
<span class="legend-item"><span class="domain-cell domain-not_exist">X</span> 不存在</span>
</div>
</div>
{% for rack in racks %}
<div class="rack-card">
<div class="rack-header">
<span class="rack-title">Rack {{ rack.rack }}</span>
<span class="rack-count">{{ rack.clusters | length }} clusters</span>
</div>
{% for cluster in rack.clusters %}
<div class="cluster-section">
<div class="cluster-header">
<span>Cluster {{ cluster.cluster }}</span>
{% set ccd = cluster.ccd.upper() %}
{% if ccd == 'ONLINE' %}
<span class="ccd-badge ccd-online">CCD: {{ cluster.ccd }}</span>
{% else %}
<span class="ccd-badge ccd-offline">CCD: {{ cluster.ccd }}</span>
{% endif %}
</div>
<div class="boards-grid">
{% for board in cluster.boards %}
<div class="board-card board-{{ board.status | lower }}">
<div class="board-top">
<span class="board-ld">LD {{ board.ld }}</span>
<span class="board-status-badge status-{{ board.status | lower }}">{{ board.status }}</span>
</div>
<div class="domains-row">
{% for d in board.domains %}
<div class="domain-cell domain-{{ d | domain_state }}"
title="D{{ loop.index0 }}: {{ d | domain_label }}">{{ d }}</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
<!-- ═══ 作业信息 ═══ -->
<div class="section-header">
<h2>📋 作业信息</h2>
</div>
{% if jobs %}
<div class="card">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>用户</th>
<th>PID</th>
<th>T-Pod</th>
<th>设计名</th>
<th>已运行</th>
<th>预留Key</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td>{{ job.job_index }}</td>
<td class="text-mono">{{ job.owner }}</td>
<td class="text-mono">{{ job.pid }}</td>
<td class="text-mono">{{ job.t_pod }}</td>
<td>{{ job.design }}</td>
<td class="text-mono">{{ job.elap_time }}</td>
<td>{{ job.reserved_key }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card empty-state">当前无活跃作业</div>
{% endif %}
<!-- ═══ T-Pod 可用性 ═══ -->
<div class="section-header">
<h2>🎯 T-Pod 可用性</h2>
</div>
<div class="pods-grid">
{% for pod in pods %}
<div class="pod-card">
<div class="pod-header">Rack {{ pod.rack }}</div>
<table class="pod-table">
{% set all_h = pod.all_hdsb | from_json %}
{% set all_t = pod.all_t_pods | from_json %}
{% if all_h %}
<tr>
<td class="pod-label" colspan="2"><b>全部 HDSB</b></td>
</tr>
{% for length, val in all_h.items() %}
<tr>
<td class="pod-sublabel">Length {{ length }}</td>
<td class="pod-value">{{ val }}</td>
</tr>
{% endfor %}
{% endif %}
{% if all_t %}
<tr>
<td class="pod-label" colspan="2"><b>全部 T-Pods</b></td>
</tr>
{% for length, val in all_t.items() %}
<tr>
<td class="pod-sublabel">Length {{ length }}</td>
<td class="pod-value">{{ val }}</td>
</tr>
{% endfor %}
{% endif %}
<tr class="pod-row-available">
<td class="pod-label">可用 HDSB</td>
<td class="pod-value">{{ pod.available_hdsb }}</td>
</tr>
<tr class="pod-row-available">
<td class="pod-label">可用 T-Pods</td>
<td class="pod-value">{{ pod.available_t_pods }}</td>
</tr>
<tr class="pod-row-locked">
<td class="pod-label">锁定 HDSB</td>
<td class="pod-value">{{ pod.locked_hdsb }}</td>
</tr>
<tr class="pod-row-locked">
<td class="pod-label">锁定 T-Pods</td>
<td class="pod-value">{{ pod.locked_t_pods }}</td>
</tr>
<tr class="pod-row-reserved">
<td class="pod-label">预留 HDSB</td>
<td class="pod-value">{{ pod.reserved_hdsb }}</td>
</tr>
<tr class="pod-row-reserved">
<td class="pod-label">预留 T-Pods</td>
<td class="pod-value">{{ pod.reserved_t_pods }}</td>
</tr>
<tr class="pod-row-unavailable">
<td class="pod-label">不可用 HDSB</td>
<td class="pod-value">{{ pod.unavailable_hdsb }}</td>
</tr>
<tr class="pod-row-unavailable">
<td class="pod-label">不可用 T-Pods</td>
<td class="pod-value">{{ pod.unavailable_t_pods }}</td>
</tr>
</table>
</div>
{% endfor %}
</div>
<!-- ═══ 利用率趋势图 ═══ -->
{% if history | length > 1 %}
<div class="section-header">
<h2>📈 利用率趋势</h2>
</div>
<div class="card">
<canvas id="utilChart" height="80"></canvas>
</div>
{% endif %}
{% else %}
<!-- 无数据状态 -->
<div class="empty-state-full">
<div class="empty-icon">📊</div>
<h2>暂无监控数据</h2>
<p>请通过以下方式导入数据:</p>
<div class="empty-actions">
<button class="btn btn-primary btn-lg" onclick="openPasteModal()">📥 粘贴 test_server -short 输出</button>
</div>
<p class="empty-hint">或在 Palladium 主机运行:<code>python3 collect.py --url http://&lt;监控服务器&gt;:5100/api/collect</code></p>
</div>
{% endif %}
{% endblock %}
{% block scripts %}
{% if snapshot and history | length > 1 %}
<script>
const ctx = document.getElementById('utilChart').getContext('2d');
const labels = [
{% for h in history %}
'{{ h.timestamp | fmt_time_short }}'{% if not loop.last %}, {% endif %}
{% endfor %}
];
const utilData = [
{% for h in history %}
{{ h.utilization }}{% if not loop.last %}, {% endif %}
{% endfor %}
];
const usedData = [
{% for h in history %}
{{ h.used_boards }}{% if not loop.last %}, {% endif %}
{% endfor %}
];
new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: '利用率 (%)',
data: utilData,
borderColor: '#007bff',
backgroundColor: 'rgba(0, 123, 255, 0.1)',
yAxisID: 'y',
tension: 0.3,
fill: true,
},
{
label: '使用逻辑板数',
data: usedData,
borderColor: '#28a745',
backgroundColor: 'rgba(40, 167, 69, 0.1)',
yAxisID: 'y1',
tension: 0.3,
fill: false,
}
]
},
options: {
responsive: true,
interaction: { mode: 'index', intersect: false },
scales: {
x: { ticks: { color: '#9ba1b3', maxRotation: 45 } },
y: {
type: 'linear', position: 'left',
title: { display: true, text: '利用率 %', color: '#007bff' },
min: 0, max: 100,
ticks: {
color: '#007bff',
// 关键: 显示 1 位小数, stepSize 0.5 让 0/0.5/1/... 都画出来
stepSize: 0.5,
callback: (v) => v.toFixed(1) + '%'
},
},
y1: {
type: 'linear', position: 'right',
title: { display: true, text: '使用逻辑板', color: '#28a745' },
ticks: { color: '#28a745', precision: 0 },
grid: { drawOnChartArea: false },
}
},
plugins: {
legend: { labels: { color: '#e4e6eb' } },
tooltip: {
callbacks: {
// tooltip 也显示 1 位小数 + 单位
label: (ctx) => {
const v = ctx.parsed.y;
if (ctx.dataset.yAxisID === 'y') {
return ` ${ctx.dataset.label}: ${v.toFixed(1)}%`;
}
return ` ${ctx.dataset.label}: ${v}`;
}
}
}
}
}
});
</script>
{% endif %}
<script>
// 自动刷新
let refreshTimer = null;
function setupRefresh() {
if (refreshTimer) clearTimeout(refreshTimer);
const cb = document.getElementById('autoRefresh');
const sel = document.getElementById('refreshInterval');
if (!cb || !sel) return;
if (cb.checked) {
const sec = parseInt(sel.value);
refreshTimer = setTimeout(() => location.reload(), sec * 1000);
}
}
document.addEventListener('DOMContentLoaded', setupRefresh);
document.getElementById('autoRefresh')?.addEventListener('change', setupRefresh);
document.getElementById('refreshInterval')?.addEventListener('change', setupRefresh);
</script>
{% endblock %}
+196
View File
@@ -0,0 +1,196 @@
{% extends "base.html" %}
{% block title %}历史记录 — Palladium Z1 Monitor{% endblock %}
{% block content %}
<div class="section-header">
<h2>📅 历史快照</h2>
<div class="header-actions">
<button class="btn btn-sm btn-secondary" onclick="openExportModal()">📤 导出</button>
<button class="btn btn-sm btn-secondary" onclick="cleanupOld()">🧹 清理30天前数据</button>
</div>
</div>
{% if pagination.items %}
<div class="card">
<table class="data-table">
<thead>
<tr>
<th>ID</th>
<th>时间</th>
<th>仿真器</th>
<th>系统状态</th>
<th>逻辑板</th>
<th>逻辑板利用率</th>
<th>使用/总数</th>
<th>作业数</th>
<th>来源</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for snap in pagination.items %}
<tr>
<td>{{ snap.id }}</td>
<td class="text-mono">{{ snap.timestamp | fmt_time }}</td>
<td>{{ snap.emulator }}</td>
<td>
{% set ss = snap.system_status.upper() %}
{% if ss == 'OK' %}
<span class="status-badge status-ok">{{ snap.system_status }}</span>
{% elif 'PARTIAL' in ss %}
<span class="status-badge status-warning">{{ snap.system_status }}</span>
{% elif 'DOWN' in ss or 'ERROR' in ss %}
<span class="status-badge status-error">{{ snap.system_status }}</span>
{% else %}
<span class="status-badge status-unknown">{{ snap.system_status }}</span>
{% endif %}
</td>
<td>
{{ snap.total_boards }}
<span class="text-green">({{ snap.online_boards }}</span>/<span class="text-red">{{ snap.offline_boards }})</span>
</td>
<td>
<div class="util-bar">
<div class="util-fill" style="width: {{ snap.utilization }}%"></div>
<span class="util-text">{{ snap.utilization }}%</span>
</div>
</td>
<td class="text-mono">{{ snap.used_boards }} / {{ snap.total_boards }}</td>
<td>{{ snap.active_jobs }}</td>
<td><span class="tag">{{ snap.source }}</span></td>
<td>
<a href="{{ url_for('snapshot_detail', sid=snap.id) }}" class="btn-sm">查看</a>
<form method="POST" action="{{ url_for('delete_snapshot', sid=snap.id) }}"
style="display:inline;" onsubmit="return confirm('确认删除快照 #{{ snap.id }}?')">
<button type="submit" class="btn-sm btn-danger">删除</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- 分页 -->
<div class="pagination">
{% if pagination.has_prev %}
<a href="{{ url_for('history', page=pagination.prev_num) }}" class="page-btn">← 上一页</a>
{% endif %}
<span class="page-info">第 {{ pagination.page }} / {{ pagination.pages }} 页 (共 {{ pagination.total }} 条)</span>
{% if pagination.has_next %}
<a href="{{ url_for('history', page=pagination.next_num) }}" class="page-btn">下一页 →</a>
{% endif %}
</div>
{% else %}
<div class="card empty-state">暂无历史数据</div>
{% endif %}
<script>
function cleanupOld() {
if (!confirm('确认清理30天前的历史数据?此操作不可撤销。')) return;
fetch('/api/cleanup?days=30', { method: 'POST' })
.then(r => r.json())
.then(data => {
alert('已清理 ' + data.deleted + ' 条快照');
location.reload();
})
.catch(e => alert('清理失败: ' + e));
}
// ─── 导出 ──────────────────────────────────────────────────────
function openExportModal() {
const m = document.getElementById('exportModal');
if (!m) return;
m.style.display = 'flex';
}
function closeExportModal() {
const m = document.getElementById('exportModal');
if (m) m.style.display = 'none';
}
function doExport() {
const fmt = document.getElementById('expFormat').value;
const range = document.getElementById('expRange').value;
const since = document.getElementById('expSince').value;
const until = document.getElementById('expUntil').value;
const source = document.getElementById('expSource').value.trim();
const params = new URLSearchParams();
params.set('format', fmt);
if (range !== 'all') params.set('days', range);
if (range === 'custom') {
if (since) params.set('since', since);
if (until) params.set('until', until);
}
if (source) params.set('source', source);
const url = '/api/export?' + params.toString();
// 触发浏览器下载
window.location.href = url;
setTimeout(closeExportModal, 300);
}
function toggleRangeFields() {
const v = document.getElementById('expRange').value;
const custom = document.getElementById('expCustomRange');
if (custom) custom.style.display = (v === 'custom') ? 'flex' : 'none';
}
</script>
<!-- 导出弹窗 -->
<div id="exportModal" class="modal-overlay" style="display:none;">
<div class="modal" style="max-width: 480px;">
<div class="modal-header">
<h2>📤 导出历史快照</h2>
<button class="modal-close" onclick="closeExportModal()"></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>格式</label>
<select id="expFormat" class="form-control">
<option value="csv">CSV (Excel 友好, 带 UTF-8 BOM)</option>
<option value="json">JSON (程序处理)</option>
</select>
</div>
<div class="form-group">
<label>时间范围</label>
<select id="expRange" class="form-control" onchange="toggleRangeFields()">
<option value="all">全部</option>
<option value="1">最近 1 天</option>
<option value="7" selected>最近 7 天</option>
<option value="30">最近 30 天</option>
<option value="90">最近 90 天</option>
<option value="custom">自定义…</option>
</select>
</div>
<div id="expCustomRange" class="form-row" style="display:none;">
<div class="form-group">
<label>起始 (含)</label>
<input type="date" id="expSince" class="form-control">
</div>
<div class="form-group">
<label>截止 (含)</label>
<input type="date" id="expUntil" class="form-control">
</div>
</div>
<div class="form-group">
<label>来源过滤 (模糊匹配, 可空)</label>
<input type="text" id="expSource" class="form-control"
placeholder="例如: collect.py、scmp03、sample">
</div>
<p class="modal-hint">
导出上限 10 万条; 超过会自动截断。<br>
CSV 默认包含 19 列: 时间(本地+UTC)、仿真器/硬件/ConfigMgr、系统状态、来源、板/域计数、利用率(1 位小数)、作业数等。
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeExportModal()">取消</button>
<button type="button" class="btn btn-primary" onclick="doExport()">📥 下载</button>
</div>
</div>
</div>
<script>
document.getElementById('exportModal')?.addEventListener('click', function(e) {
if (e.target === this) closeExportModal();
});
</script>
{% endblock %}
+183
View File
@@ -0,0 +1,183 @@
{% extends "base.html" %}
{% block title %}快照 #{{ snapshot.id }} — Palladium Z1 Monitor{% endblock %}
{% block content %}
<div class="snapshot-header">
<div>
<h2>📸 快照 #{{ snapshot.id }}</h2>
<div class="snapshot-meta">
{{ snapshot.timestamp | fmt_time }} · 来源: {{ snapshot.source }} ·
{{ snapshot.emulator }} / {{ snapshot.hardware }}
</div>
</div>
<a href="{{ url_for('dashboard') }}" class="btn btn-secondary">← 返回仪表板</a>
</div>
<!-- 系统信息 -->
<div class="system-bar">
<div class="sys-item">
<span class="sys-label">仿真器</span>
<span class="sys-value">{{ snapshot.emulator }}</span>
</div>
<div class="sys-item">
<span class="sys-label">硬件</span>
<span class="sys-value">{{ snapshot.hardware }}</span>
</div>
<div class="sys-item">
<span class="sys-label">ConfigMgr</span>
<span class="sys-value">{{ snapshot.configmgr }}</span>
</div>
<div class="sys-item">
<span class="sys-label">系统状态</span>
{% set ss = snapshot.system_status.upper() %}
{% if ss == 'OK' %}
<span class="status-badge status-ok">{{ snapshot.system_status }}</span>
{% elif 'PARTIAL' in ss %}
<span class="status-badge status-warning">{{ snapshot.system_status }}</span>
{% elif 'DOWN' in ss or 'ERROR' in ss %}
<span class="status-badge status-error">{{ snapshot.system_status }}</span>
{% else %}
<span class="status-badge status-unknown">{{ snapshot.system_status }}</span>
{% endif %}
</div>
</div>
<!-- 指标卡片 -->
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-icon">🖥️</div>
<div class="metric-body">
<div class="metric-label">逻辑板</div>
<div class="metric-value">{{ stats.total_boards }}</div>
<div class="metric-sub">
<span class="text-green">在线 {{ stats.online_boards }}</span> ·
<span class="text-red">离线 {{ stats.offline_boards }}</span>
</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">📊</div>
<div class="metric-body">
<div class="metric-label">逻辑板利用率</div>
<div class="metric-value">{{ stats.utilization }}%</div>
<div class="metric-sub">使用 {{ stats.used_boards }} / 空闲 {{ stats.online_boards - stats.used_boards }}</div>
</div>
</div>
<div class="metric-card">
<div class="metric-icon">⚙️</div>
<div class="metric-body">
<div class="metric-label">活跃作业</div>
<div class="metric-value">{{ stats.active_jobs }}</div>
</div>
</div>
</div>
<!-- Rack / Cluster / Board -->
<div class="section-header">
<h2>🏗️ 逻辑板状态</h2>
<div class="legend">
<span class="legend-item"><span class="domain-cell domain-available">-</span> 可用</span>
<span class="legend-item"><span class="domain-cell domain-downloaded">1</span> 已加载</span>
<span class="legend-item"><span class="domain-cell domain-disabled">D</span> 已禁用</span>
<span class="legend-item"><span class="domain-cell domain-not_exist">X</span> 不存在</span>
</div>
</div>
{% for rack in racks %}
<div class="rack-card">
<div class="rack-header">
<span class="rack-title">Rack {{ rack.rack }}</span>
</div>
{% for cluster in rack.clusters %}
<div class="cluster-section">
<div class="cluster-header">
<span>Cluster {{ cluster.cluster }}</span>
{% set ccd = cluster.ccd.upper() %}
{% if ccd == 'ONLINE' %}
<span class="ccd-badge ccd-online">CCD: {{ cluster.ccd }}</span>
{% else %}
<span class="ccd-badge ccd-offline">CCD: {{ cluster.ccd }}</span>
{% endif %}
</div>
<div class="boards-grid">
{% for board in cluster.boards %}
<div class="board-card board-{{ board.status | lower }}">
<div class="board-top">
<span class="board-ld">LD {{ board.ld }}</span>
<span class="board-status-badge status-{{ board.status | lower }}">{{ board.status }}</span>
</div>
<div class="domains-row">
{% for d in board.domains %}
<div class="domain-cell domain-{{ d | domain_state }}"
title="D{{ loop.index0 }}: {{ d | domain_label }}">{{ d }}</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
<!-- 作业信息 -->
<div class="section-header"><h2>📋 作业信息</h2></div>
{% if jobs %}
<div class="card">
<table class="data-table">
<thead>
<tr><th>#</th><th>用户</th><th>PID</th><th>T-Pod</th><th>设计名</th><th>已运行</th><th>预留Key</th></tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
<td>{{ job.job_index }}</td>
<td class="text-mono">{{ job.owner }}</td>
<td class="text-mono">{{ job.pid }}</td>
<td class="text-mono">{{ job.t_pod }}</td>
<td>{{ job.design }}</td>
<td class="text-mono">{{ job.elap_time }}</td>
<td>{{ job.reserved_key }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="card empty-state">该快照无活跃作业</div>
{% endif %}
<!-- T-Pod -->
<div class="section-header"><h2>🎯 T-Pod 可用性</h2></div>
<div class="pods-grid">
{% for pod in pods %}
<div class="pod-card">
<div class="pod-header">Rack {{ pod.rack }}</div>
<table class="pod-table">
{% set all_h = pod.all_hdsb | from_json %}
{% set all_t = pod.all_t_pods | from_json %}
{% for length, val in all_h.items() %}
<tr><td class="pod-sublabel">HDSB L{{ length }}</td><td class="pod-value">{{ val }}</td></tr>
{% endfor %}
{% for length, val in all_t.items() %}
<tr><td class="pod-sublabel">T-Pods L{{ length }}</td><td class="pod-value">{{ val }}</td></tr>
{% endfor %}
<tr class="pod-row-available"><td class="pod-label">可用 HDSB</td><td class="pod-value">{{ pod.available_hdsb }}</td></tr>
<tr class="pod-row-available"><td class="pod-label">可用 T-Pods</td><td class="pod-value">{{ pod.available_t_pods }}</td></tr>
<tr class="pod-row-locked"><td class="pod-label">锁定 HDSB</td><td class="pod-value">{{ pod.locked_hdsb }}</td></tr>
<tr class="pod-row-locked"><td class="pod-label">锁定 T-Pods</td><td class="pod-value">{{ pod.locked_t_pods }}</td></tr>
<tr class="pod-row-reserved"><td class="pod-label">预留 HDSB</td><td class="pod-value">{{ pod.reserved_hdsb }}</td></tr>
<tr class="pod-row-reserved"><td class="pod-label">预留 T-Pods</td><td class="pod-value">{{ pod.reserved_t_pods }}</td></tr>
<tr class="pod-row-unavailable"><td class="pod-label">不可用 HDSB</td><td class="pod-value">{{ pod.unavailable_hdsb }}</td></tr>
<tr class="pod-row-unavailable"><td class="pod-label">不可用 T-Pods</td><td class="pod-value">{{ pod.unavailable_t_pods }}</td></tr>
</table>
</div>
{% endfor %}
</div>
<!-- 原始输出 -->
<details class="raw-output-section">
<summary>📄 原始 test_server 输出</summary>
<pre class="raw-output">{{ snapshot.raw_output }}</pre>
</details>
{% endblock %}