Files
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

195 lines
9.9 KiB
HTML
Raw Permalink 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">
<button onclick="document.getElementById('add-form').style.display='block'" class="btn btn-primary btn-small"> 添加实例</button>
</div>
</div>
<p class="meta-text">
在数据库中保存多个 Squid 实例。切换"当前活动实例"后,所有配置、日志、服务控制页面都会基于该实例的路径与服务名。<br>
当前活动实例: <span class="badge badge-blue">{{ active_instance_name }}</span>
{% if active %}<span class="meta-text">(ID #{{ active.id }})</span>{% endif %}
</p>
{# -------- add new instance form (hidden by default) -------- #}
<div class="card" id="add-form" style="display:none;">
<h3 class="card-title">添加新实例</h3>
<form method="post" action="{{ url_for('instances_add') }}" class="inline-form">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<div class="filter-grid">
<div class="form-group">
<label>名称 <span class="hint">(唯一)</span></label>
<input type="text" name="name" class="form-input" required maxlength="80" placeholder="例如: prod-edge-01">
</div>
<div class="form-group">
<label>描述</label>
<input type="text" name="description" class="form-input" maxlength="200" placeholder="可选">
</div>
<div class="form-group">
<label>本机实例</label>
<select name="is_local" class="form-input">
<option value="1" selected>是 (本地)</option>
<option value="0">否 (远程 SSH)</option>
</select>
</div>
</div>
<div class="config-section">
<h3 class="config-section-title">🦑 Squid 路径</h3>
<div class="filter-grid">
<div class="form-group">
<label>squid.conf 路径</label>
<input type="text" name="squid_conf_path" class="form-input" value="/etc/squid/squid.conf">
</div>
<div class="form-group">
<label>access.log 路径</label>
<input type="text" name="access_log_path" class="form-input" value="/var/log/squid/access.log">
</div>
<div class="form-group">
<label>cache.log 路径</label>
<input type="text" name="cache_log_path" class="form-input" value="/var/log/squid/cache.log">
</div>
<div class="form-group">
<label>squid 二进制</label>
<input type="text" name="squid_binary" class="form-input" value="squid">
</div>
<div class="form-group">
<label>systemctl</label>
<input type="text" name="systemctl" class="form-input" value="systemctl">
</div>
<div class="form-group">
<label>Systemd 服务名</label>
<input type="text" name="squid_service" class="form-input" value="squid">
</div>
</div>
</div>
<div class="config-section">
<h3 class="config-section-title">🔐 SSH (仅远程实例)</h3>
<div class="filter-grid">
<div class="form-group">
<label>SSH 主机</label>
<input type="text" name="ssh_host" class="form-input" placeholder="例如: 10.0.0.5">
</div>
<div class="form-group">
<label>SSH 用户</label>
<input type="text" name="ssh_user" class="form-input" placeholder="root">
</div>
<div class="form-group">
<label>SSH 端口</label>
<input type="number" name="ssh_port" class="form-input" value="22" min="1" max="65535">
</div>
<div class="form-group">
<label>SSH 私钥路径</label>
<input type="text" name="ssh_key_path" class="form-input" placeholder="/root/.ssh/id_rsa">
</div>
</div>
</div>
<div class="form-actions">
<label style="display:inline-flex; align-items:center; gap:6px; margin-right:auto;">
<input type="checkbox" name="is_active" value="1">
添加后立即激活
</label>
<button type="button" onclick="document.getElementById('add-form').style.display='none'" class="btn btn-secondary btn-small">取消</button>
<button type="submit" class="btn btn-primary btn-small">💾 添加</button>
</div>
</form>
</div>
{# -------- existing instances table -------- #}
<div class="card">
<h3 class="card-title">📋 已配置实例 ({{ instances|length }})</h3>
{% if not instances %}
<p class="card-empty">尚未配置任何实例。点击右上角"添加实例"开始。</p>
{% else %}
<div class="table-scroll">
<form method="post" action="{{ url_for('instances_save') }}">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<table class="data-table">
<thead>
<tr>
<th>名称</th>
<th>描述</th>
<th>主机</th>
<th>配置文件</th>
<th>access.log</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for inst in instances %}
{% set is_current = (active and active.id == inst.id) %}
<tr {% if is_current %}style="background: rgba(59,130,246,0.08);"{% endif %}>
<td>
<input type="text" name="name_{{ inst.id }}" value="{{ inst.name }}" class="form-input" required maxlength="80">
</td>
<td>
<input type="text" name="description_{{ inst.id }}" value="{{ inst.description or '' }}" class="form-input" maxlength="200" placeholder="(无)">
</td>
<td>
{% if inst.is_local %}
<span class="badge badge-blue">本地</span>
{% else %}
<code>{{ inst.ssh_host or '?' }}:{{ inst.ssh_port }}</code>
<span class="meta-text">({{ inst.ssh_user or '?' }})</span>
{% endif %}
</td>
<td><code class="meta-text">{{ inst.squid_conf_path }}</code></td>
<td><code class="meta-text">{{ inst.access_log_path }}</code></td>
<td>
{% if is_current %}
<span class="badge badge-green">★ 当前活动</span>
{% elif inst.is_active %}
<span class="badge badge-yellow">默认</span>
{% else %}
<span class="meta-text"></span>
{% endif %}
</td>
<td style="white-space:nowrap;">
{% if not is_current %}
<form method="post" action="{{ url_for('instances_activate', inst_id=inst.id) }}" style="display:inline;">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<input type="hidden" name="next" value="{{ request.path }}">
<button type="submit" class="btn btn-primary btn-small">▶ 激活</button>
</form>
{% endif %}
<a href="{{ url_for('instances_test', inst_id=inst.id) }}" class="btn btn-secondary btn-small" target="_blank">🔍 测试</a>
<form method="post" action="{{ url_for('instances_delete') }}" style="display:inline;" onsubmit="return confirm('确定要删除实例 {{ inst.name }} 吗?');">
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
<input type="hidden" name="id" value="{{ inst.id }}">
<button type="submit" class="btn btn-danger btn-small">🗑</button>
</form>
{# hidden checkbox used by /instances/save to set the active row #}
{% if not is_current %}
<input type="checkbox" name="is_active_{{ inst.id }}" value="1" style="display:none;">
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="form-actions" style="margin-top: 12px;">
<span class="meta-text">提示: 表格中可编辑名称/描述/路径。修改后点击"保存所有修改"。</span>
<button type="submit" class="btn btn-primary">💾 保存所有修改</button>
</div>
</form>
</div>
{% endif %}
</div>
<div class="card">
<h3 class="card-title">️ 说明</h3>
<ul>
<li>当前活动实例的路径/服务名会覆盖 <code>/config/paths</code> 中的全局设置</li>
<li>切换实例后,所有现有页面 (config/*、logs、service) 自动基于新实例的路径</li>
<li>应用级参数 (日志解析上限、缓存秒数等) 仍然是全局的,不会因实例切换而变化</li>
<li>远程实例的 SSH 凭据当前仅记录,配置读写仍需通过本地文件系统或 SSHFS 挂载</li>
<li>"测试"按钮检查 conf_path 是否可读、access/cache log 是否存在</li>
</ul>
</div>
{% endblock %}