6726524c50
- 修复删除快照时 AttributeError: SQLAlchemy.get_or_404 (低版本兼容) - UI/标题/邮件主题从 'Palladium Z1 Monitor' 改为 'Palladium Monitor'
395 lines
14 KiB
HTML
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://<监控服务器>: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 %}
|