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
This commit is contained in:
Hermes Agent
2026-07-15 10:19:21 +08:00
commit 4a1d949e41
54 changed files with 16895 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
{% extends "base.html" %}
{% block title %}日志轮转 - Squid Manager{% endblock %}
{% block content %}
<div class="page-header">
<h1>🔁 日志轮转 (logrotate)</h1>
<div class="page-actions">
<span class="meta-text">配置: <code>{{ logrotate_path }}</code></span>
</div>
</div>
<div class="card">
<h3 class="card-title">📘 logrotate 怎么配合 Squid</h3>
<p class="card-desc">本页面会生成 <code>/etc/logrotate.d/squid</code>,内容由 Squid Manager 维护。流程:</p>
<ol>
<li>logrotate 检测到当前日志达到 <code>size</code> 阈值(或到 <code>daily</code> 时间)时,把当前文件改名为 <code>.1</code>;</li>
<li>logrotate 用 <code>create 0640 proxy proxy</code> 创建新空文件(原属主为 Squid 用户);</li>
<li>logrotate 执行 <code>postrotate</code> 块里的 <code>squid -k rotate</code>,让 Squid 关闭旧 fd 并重开新文件 — 这是核心联动;</li>
<li>老日志按 <code>rotate N</code> 数量保留,多余的会被删除/压缩 (<code>compress</code>);</li>
<li>当本页面「卸载」配置时,只删除 <code>/etc/logrotate.d/squid</code>,不会动 Squid 本身。</li>
</ol>
<p class="meta-text">
{% if logrotate_available %}
<span class="badge badge-green">● logrotate 可用</span>
{% else %}
<span class="badge badge-red">● logrotate 未安装</span> 请先 <code>apt install logrotate</code>
{% endif %}
</p>
</div>
<div class="grid-2">
<div class="card">
<h3 class="card-title">⚙️ 安装 / 重新生成配置</h3>
<p class="card-desc">目标文件: <code>{{ logrotate_path }}</code></p>
{% if not paths %}
<div class="card-empty">
<div class="empty-icon">📂</div>
<p>未在「<a href="{{ url_for('config_paths') }}">路径设置</a>」里配置任何日志路径,无法生成。</p>
</div>
{% else %}
<form method="post" action="{{ url_for('ops_logrotate_install') }}">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<div class="filter-grid">
<div class="form-group">
<label>轮转阈值 (size) <span class="hint">单文件达到此大小立即轮转,如 <code>100M</code> / <code>500K</code></span></label>
<input type="text" name="rotate_size" value="{{ install_size }}" class="form-input"
placeholder="100M" pattern="^\d+[KMG]?$">
</div>
<div class="form-group">
<label>保留份数 (rotate) <span class="hint">最多保留 N 份旧日志,超出自动删除</span></label>
<input type="number" name="rotate_count" value="{{ install_count }}" class="form-input"
min="1" max="365" step="1">
</div>
<div class="form-group">
<label>压缩 (compress)</label>
<select name="compress" class="form-input">
<option value="1" {% if install_compress %}selected{% endif %}>启用 — 旧日志用 gzip 压缩</option>
<option value="0" {% if not install_compress %}selected{% endif %}>禁用 — 保留为明文</option>
</select>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">💾 写入配置</button>
{% if is_installed %}
<button type="button" class="btn btn-danger" id="btn-uninstall">🗑 卸载配置</button>
{% endif %}
</div>
</form>
{% if is_installed %}
<form id="uninstall-form" method="post" action="{{ url_for('ops_logrotate_uninstall') }}" style="display:none;">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
</form>
{% endif %}
{% endif %}
</div>
<div class="card">
<h3 class="card-title">📋 当前已安装</h3>
{% if is_installed %}
<p class="meta-text">{{ current_msg }}</p>
<table class="data-table">
<tbody>
<tr><th style="width: 160px;">状态</th>
<td><span class="badge badge-green">● 已安装</span></td>
</tr>
<tr><th>路径</th><td><code>{{ logrotate_path }}</code></td></tr>
<tr><th>管理日志</th>
<td>
{% for p in paths %}
<div><code>{{ p }}</code></div>
{% endfor %}
</td>
</tr>
</tbody>
</table>
{% else %}
<p class="meta-text">
{% if not current_msg %}{% endif %}
尚未安装 logrotate 配置 ({{ current_msg or '文件不存在' }}),点击左侧表单写入。
</p>
{% endif %}
</div>
</div>
<div class="card">
<h3 class="card-title">📄 当前 {{ logrotate_path }} 内容</h3>
{% if current_content %}
<pre style="background: var(--bg-elevated); padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 12px;"><code>{{ current_content }}</code></pre>
{% else %}
<p class="meta-text">暂无内容 (尚未安装)。</p>
{% endif %}
</div>
{% if preview %}
<div class="card">
<h3 class="card-title">🧪 预览 (尚未写入磁盘)</h3>
<p class="card-desc">下面是根据当前表单参数生成的预览,仅供检查。点「写入配置」才会落盘。</p>
<pre style="background: var(--bg-elevated); padding: 12px; border-radius: 6px; overflow-x: auto; font-size: 12px;"><code>{{ preview }}</code></pre>
</div>
{% endif %}
<div class="card">
<h3 class="card-title">️ 提示</h3>
<ul>
<li>权限不足时,写入会自动尝试 <code>sudo tee</code> — 需要本平台进程具备无密码 sudo,否则会回显失败原因。</li>
<li><code>size</code> 接受 logrotate 单位后缀 <code>K</code> / <code>M</code> / <code>G</code>;可与 <code>daily</code> 共存(任一条件满足即触发)。</li>
<li>卸载配置不会触碰日志文件本身 — 已存在的旧日志文件 <code>access.log.1.gz</code> 等仍保留。</li>
<li>每次「写入配置」都会记录到 <a href="{{ url_for('audit_view') }}">审计日志</a></li>
</ul>
</div>
{% endblock %}
{% block scripts %}
{% if is_installed %}
<script>
(function() {
const btn = document.getElementById('btn-uninstall');
const form = document.getElementById('uninstall-form');
if (!btn || !form) return;
btn.addEventListener('click', function() {
if (!confirm('确认卸载 logrotate 配置?\\n文件 {{ logrotate_path }} 将被删除,日志本身不会被影响。')) {
return;
}
form.submit();
});
})();
</script>
{% endif %}
{% endblock %}