Files
Your Name 8ca1728ec5 feat: 导出增加用户名 + 用户LD板数 (快照级两列, 新增 group=users 用户级导出)
- 快照级 CSV/JSON 追加 用户名/用户LD板数 列 (DomainDetail 逐域聚合, 板键=rack+cluster+ld)
- 新增 group=users: 每快照x每用户一行, 含板号/Rack/域数/设计/T-Pod/运行时间
- 导出弹窗加导出粒度选项; README 同步
2026-09-01 16:37:31 +08:00

207 lines
8.9 KiB
HTML

{% extends "base.html" %}
{% block title %}历史记录 — Palladium 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 group = document.getElementById('expGroup').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);
params.set('group', group);
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="expGroup" class="form-control">
<option value="snapshots" selected>按快照 — 每条快照一行 (含用户名 + 各用户 LD 板数)</option>
<option value="users">按用户 — 每条快照 × 每个用户一行 (用户名 / LD 板数 / 板号)</option>
</select>
</div>
<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>
按快照: 21 列 — 时间(本地+UTC)、仿真器/硬件/ConfigMgr、系统状态、来源、板/域计数、利用率、作业数、用户名、各用户 LD 板数 (多用户逗号分隔, 顺序对应)。<br>
按用户: 12 列 — 快照时间/来源 + 用户名、该用户 LD 板数、板号、Rack、占用域数、设计名、T-Pod、运行时间。用户名/LD 板数来自详细版 test_server 逐域归属数据。
</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 %}