Files
squid-manager/templates/performance.html
T
Hermes Agent 4a1d949e41 feat: 完整 Squid Web Manager 平台 - P0-P3 全功能
完整 Web 管理平台,涵盖:

P0 生产加固:
- 会话超时(空闲+绝对) + CSRF 防护 + 登录失败限流
- HTTPS/TLS 证书管理(自签生成/上传/删除/过期提醒)
- SSL Bump (HTTPS 透明代理) 可视化配置
- 配置文件 diff 预览 + 二次确认才写入
- 服务控制二次确认 + 操作期间按钮锁定

P1 运维能力:
- 日志轮转管理(logrotate) + 日志状态监控
- 告警规则(5xx/命中率/磁盘/客户端流量)
- 多 Squid 实例管理 + 一键切换
- squidclient mgr 实时性能指标
- 流量异常检测(突增/大文件/高频小包/新客户端)

P2 日志分析增强:
- 日志持久化到 SQLite + 历史时间范围查询
- 导出 CSV/JSON(日志/KPI/异常/审计)
- GeoIP 地图(Leaflet + ip-api.com 离线可选 GeoLite2)
- 每客户端 24h 趋势图
- 自定义 Squid logformat 解析器

P3 体验锦上添花:
- 暗/亮主题切换(cookie 持久化)
- WebSSH 终端(xterm.js + paramiko)
- 完整审计日志 + 用户管理

技术栈: Flask 3.0 + SQLAlchemy 2.0 + SQLite + Chart.js + Leaflet
总代码量: ~16500 行 (15 Python 模块 + 34 模板 + CSS)
路由数: 73
2026-07-15 10:19:21 +08:00

248 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}实时性能 - Squid Manager{% endblock %}
{% block content %}
<div class="page-header">
<h1>📊 实时性能监控</h1>
<div class="page-actions">
<span class="meta-text">
采集于 {{ summary.fetched_at }} · 来源 {{ conn.host }}:{{ conn.port }}
</span>
<button id="refresh-btn" class="btn btn-small btn-secondary">🔄 刷新</button>
</div>
</div>
{% if not summary.fetched_ok %}
<div class="card" style="border-left: 4px solid #ef4444;">
<h3 class="card-title">⚠️ 无法获取 Squid 性能数据</h3>
<p class="meta-text">
<strong>错误信息:</strong> {{ summary.error or "未知错误" }}
</p>
<p class="meta-text">
请检查:
<ul style="margin: 6px 0 0 18px;">
<li><code>squidclient</code> 工具是否已安装且在 <code>PATH</code> 中 (运行 <code>which squidclient</code> 验证)</li>
<li>Squid <code>cache_mgr</code> 管理接口是否启用 (默认端口 {{ conn.port }})</li>
<li>主机 <code>{{ conn.host }}:{{ conn.port }}</code> 是否可以从 Web 服务器访问</li>
<li>如启用了 <code>cachemgr_passwd</code>, 请在 <a href="{{ url_for('config_paths') }}">路径设置</a> 配置 <code>squid_mgr_password</code></li>
</ul>
</p>
</div>
{% endif %}
<!-- Top metric grid: 8 dense cards (4 cols × 2 rows on desktop) -->
<div class="kpi-grid" id="metric-grid">
<div class="kpi-card kpi-blue">
<div class="kpi-label">运行时长</div>
<div class="kpi-value" data-field="squid_uptime_human">{{ summary.squid_uptime_human or "—" }}</div>
<div class="kpi-meta">Squid 版本 {{ summary.squid_version or "?" }}</div>
</div>
<div class="kpi-card kpi-purple">
<div class="kpi-label">CPU 时间</div>
<div class="kpi-value" data-field="cpu_time_human">{{ summary.cpu_time_human or "—" }}</div>
<div class="kpi-meta">累计用户态+内核态</div>
</div>
<div class="kpi-card kpi-cyan">
<div class="kpi-label">最大内存占用</div>
<div class="kpi-value" data-field="max_rss_human">{{ summary.max_rss_human or "—" }}</div>
<div class="kpi-meta">RSS 峰值</div>
</div>
<div class="kpi-card kpi-orange">
<div class="kpi-label">活跃客户端</div>
<div class="kpi-value" data-field="current_clients">{{ summary.current_clients if summary.current_clients is not none else "—" }}</div>
<div class="kpi-meta">正在访问缓存的 IP 数</div>
</div>
<div class="kpi-card kpi-green">
<div class="kpi-label">缓存命中率</div>
<div class="kpi-value" data-field="cache_hits_pct">
{% if summary.cache_hits_pct is not none %}{{ '%.1f' % summary.cache_hits_pct }}%{% else %}—{% endif %}
</div>
<div class="kpi-meta">Hits as % of bytes sent</div>
</div>
<div class="kpi-card kpi-yellow">
<div class="kpi-label">磁盘缓存</div>
<div class="kpi-value" data-field="storage_swap_human">{{ summary.storage_swap_human or "—" }}</div>
<div class="kpi-meta">Storage Swap size</div>
</div>
<div class="kpi-card kpi-pink">
<div class="kpi-label">内存缓存</div>
<div class="kpi-value" data-field="storage_mem_human">{{ summary.storage_mem_human or "—" }}</div>
<div class="kpi-meta">Storage Mem size</div>
</div>
<div class="kpi-card kpi-red">
<div class="kpi-label">5min 请求率</div>
<div class="kpi-value" data-field="request_rate_5min">
{% if summary.request_rate_5min is not none %}{{ '%.1f' % summary.request_rate_5min }} req/s{% else %}—{% endif %}
</div>
<div class="kpi-meta">
流量:
<span data-field="byte_rate_5min_human">
{% if summary.byte_rate_5min is not none %}{{ format_bytes(summary.byte_rate_5min) }}/s{% else %}—{% endif %}
</span>
</div>
</div>
</div>
<!-- Status bar -->
<div class="card" style="margin-top: 16px;">
<h3 class="card-title">🛰️ Squid 状态</h3>
<div class="grid-2">
<div>
<table class="data-table small">
<tbody>
<tr>
<th>启动时间</th>
<td data-field="start_time">{{ summary.start_time or "—" }}</td>
</tr>
<tr>
<th>当前时间</th>
<td data-field="current_time">{{ summary.current_time or "—" }}</td>
</tr>
<tr>
<th>作者信息</th>
<td>{{ summary.squid_author or "—" }}</td>
</tr>
</tbody>
</table>
</div>
<div>
<table class="data-table small">
<tbody>
<tr>
<th>采集状态</th>
<td>
{% if summary.fetched_ok %}
<span class="badge badge-green">✅ 成功</span>
{% else %}
<span class="badge badge-red">❌ 失败</span>
{% endif %}
</td>
</tr>
<tr>
<th>采集时间</th>
<td data-field="fetched_at">{{ summary.fetched_at }}</td>
</tr>
<tr>
<th>采集源</th>
<td><code>{{ conn.host }}:{{ conn.port }}</code></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Fold-out raw output -->
<div class="card" style="margin-top: 16px;">
<h3 class="card-title">🔍 原始输出 (可折叠)</h3>
<details>
<summary>查看 mgr:info 完整文本</summary>
<pre id="raw-info" style="max-height: 400px; overflow: auto; background: var(--bg); padding: 12px; border-radius: 6px; font-size: 12px;">{{ summary.raw_info or summary.error or "无数据" }}</pre>
</details>
<details style="margin-top: 12px;">
<summary>查看 mgr:5min 完整文本 (懒加载)</summary>
<div id="raw-5min" data-loaded="false" class="meta-text" style="padding: 8px 0;">点击展开后按需加载...</div>
</details>
<details style="margin-top: 12px;">
<summary>查看 mgr:counters 完整文本 (懒加载)</summary>
<div id="raw-counters" data-loaded="false" class="meta-text" style="padding: 8px 0;">点击展开后按需加载...</div>
</details>
</div>
{% endblock %}
{% block scripts %}
<script>
(function() {
const btn = document.getElementById('refresh-btn');
const grid = document.getElementById('metric-grid');
if (!btn || !grid) return;
// ---- Auto-refresh: 30s if user hasn't disabled it ----
let autoTimer = null;
function scheduleAuto() {
if (autoTimer) clearInterval(autoTimer);
autoTimer = setInterval(refreshNow, 30000);
}
function fmtBytes(b) {
if (b === null || b === undefined) return '—';
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
let i = 0;
while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
return b.toFixed(b >= 100 ? 0 : (b >= 10 ? 1 : 2)) + ' ' + u[i];
}
function applySummary(s) {
// Update each card by data-field
const fields = {
squid_uptime_human: s.squid_uptime_human || '—',
cpu_time_human: s.cpu_time_human || '—',
max_rss_human: s.max_rss_human || '—',
current_clients: (s.current_clients === null || s.current_clients === undefined) ? '—' : s.current_clients,
cache_hits_pct: (s.cache_hits_pct === null || s.cache_hits_pct === undefined) ? '—' : s.cache_hits_pct.toFixed(1) + '%',
storage_swap_human: s.storage_swap_human || '—',
storage_mem_human: s.storage_mem_human || '—',
request_rate_5min: (s.request_rate_5min === null || s.request_rate_5min === undefined) ? '—' : s.request_rate_5min.toFixed(1) + ' req/s',
byte_rate_5min_human: (s.byte_rate_5min === null || s.byte_rate_5min === undefined) ? '—' : fmtBytes(s.byte_rate_5min) + '/s',
start_time: s.start_time || '—',
current_time: s.current_time || '—',
fetched_at: s.fetched_at || '—',
};
for (const [k, v] of Object.entries(fields)) {
const el = document.querySelector('[data-field="' + k + '"]');
if (el) el.textContent = v;
}
// Visual feedback on success
btn.textContent = '✅ ' + (s.fetched_ok ? '已刷新' : '刷新失败');
btn.classList.toggle('btn-primary', !!s.fetched_ok);
setTimeout(() => { btn.textContent = '🔄 刷新'; btn.classList.remove('btn-primary'); }, 1500);
}
async function refreshNow() {
btn.disabled = true;
btn.textContent = '⏳ 刷新中…';
try {
const resp = await fetch('{{ url_for("performance_refresh") }}', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
});
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const data = await resp.json();
applySummary(data);
} catch (err) {
btn.textContent = '❌ ' + (err.message || '失败');
setTimeout(() => { btn.textContent = '🔄 刷新'; }, 2000);
} finally {
btn.disabled = false;
}
}
btn.addEventListener('click', refreshNow);
scheduleAuto();
// ---- Lazy load raw sections when their <details> is opened ----
document.querySelectorAll('details').forEach(d => {
const target = d.querySelector('[data-loaded="false"]');
if (!target) return;
d.addEventListener('toggle', async () => {
if (!d.open || target.dataset.loaded === 'true') return;
target.dataset.loaded = 'loading';
target.textContent = '加载中...';
const section = target.id.replace('raw-', '');
try {
const resp = await fetch('{{ url_for("performance_raw", section="PLACEHOLDER") }}'.replace('PLACEHOLDER', section));
const data = await resp.json();
target.dataset.loaded = 'true';
target.textContent = data.raw || data.error || '无数据';
target.style.whiteSpace = 'pre-wrap';
target.style.fontFamily = 'monospace';
target.style.fontSize = '12px';
} catch (err) {
target.dataset.loaded = 'true';
target.textContent = '加载失败: ' + err.message;
}
});
});
})();
</script>
{% endblock %}