Files
palladium-monitor/templates/history.html
T
Your Name 8945073ec6 del
2026-07-08 11:25:50 +08:00

197 lines
8.1 KiB
HTML

{% 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 %}