Files
Your Name 6726524c50 fix: db.get_or_404 -> query.get_or_404 + rename to Palladium Monitor
- 修复删除快照时 AttributeError: SQLAlchemy.get_or_404 (低版本兼容)
- UI/标题/邮件主题从 'Palladium Z1 Monitor' 改为 'Palladium Monitor'
2026-07-08 14:46:18 +08:00

395 lines
14 KiB
HTML

{% extends "base.html" %}
{% block title %}仪表板 — Palladium 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 %}