del
This commit is contained in:
@@ -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>
|
||||
@@ -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://<监控服务器>: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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user