4a1d949e41
完整 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
248 lines
11 KiB
HTML
248 lines
11 KiB
HTML
{% 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 %} |