197 lines
8.1 KiB
HTML
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 %}
|