Files
squid-manager/templates/service.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

175 lines
7.2 KiB
HTML

{% extends "base.html" %}
{% block title %}服务控制 - Squid Manager{% endblock %}
{% block content %}
{% set tokens = confirm_tokens() %}
<div class="page-header">
<h1>🛠️ 服务控制</h1>
</div>
<div class="grid-2">
<div class="card">
<h3 class="card-title">📋 服务状态</h3>
<table class="data-table">
<tbody>
<tr>
<th>状态</th>
<td>
{% if status.active %}
<span class="badge badge-green">● 运行中</span>
{% elif status.state == 'stopped' %}
<span class="badge badge-gray">● 已停止</span>
{% else %}
<span class="badge badge-orange">● {{ status.state }}</span>
{% endif %}
</td>
</tr>
<tr><th>服务名</th><td><code>{{ cfg.squid_service if cfg is defined else 'squid' }}</code></td></tr>
<tr><th>主进程 PID</th><td>{{ status.pid or '-' }}</td></tr>
<tr><th>启动时间</th><td>{{ status.since or '-' }}</td></tr>
<tr><th>Squid 版本</th><td>{{ status.version or '-' }}</td></tr>
<tr><th>Unit 文件</th><td><code>{{ status.unit_file or '-' }}</code></td></tr>
<tr><th>二进制可用</th><td>{% if status.binary_available %}<span class="badge badge-green"></span>{% else %}<span class="badge badge-red"></span> (无法执行 squid 命令){% endif %}</td></tr>
</tbody>
</table>
</div>
<div class="card">
<h3 class="card-title">🎛️ 操作</h3>
<p class="card-desc">以下操作会通过 systemctl 调用 squid 服务。reload 会优先尝试平滑重载,失败则回退到 restart。</p>
<div class="btn-group-vertical">
<button onclick="svc('start')" class="btn btn-primary">▶ 启动</button>
<button onclick="showConfirm('stop')" class="btn btn-danger">⏹ 停止</button>
<button onclick="showConfirm('restart')" class="btn btn-warning">🔄 重启</button>
<button onclick="svc('reload')" class="btn btn-secondary">♻️ 平滑重载 (reload)</button>
</div>
<div id="svc-result" class="svc-result" style="display:none;"></div>
</div>
</div>
<div class="card">
<h3 class="card-title">📁 关键路径</h3>
<table class="data-table">
<thead><tr><th>项目</th><th>路径</th><th>操作</th></tr></thead>
<tbody>
<tr>
<td>主配置文件</td>
<td><code>{{ conf }}</code></td>
<td><a href="{{ url_for('config_raw') }}" class="btn btn-small">编辑</a></td>
</tr>
<tr>
<td>访问日志</td>
<td><code>{{ access_log }}</code></td>
<td><a href="{{ url_for('logs_view') }}" class="btn btn-small">查看</a></td>
</tr>
<tr>
<td>缓存日志</td>
<td><code>{{ cache_log }}</code></td>
<td>-</td>
</tr>
</tbody>
</table>
</div>
<!-- P0-5: confirm-modal for dangerous service operations -->
<div id="confirm-modal" class="modal" style="display:none;"
data-token-start="{{ tokens.start }}"
data-token-stop="{{ tokens.stop }}"
data-token-restart="{{ tokens.restart }}"
data-token-reload="{{ tokens.reload }}">
<div class="modal-card">
<h3 id="confirm-title">⚠️ 确认操作</h3>
<p>这是一个危险操作。Squid 服务的所有连接会立即中断。</p>
<p>请输入: <code id="confirm-phrase"></code> 来确认。</p>
<input type="text" id="confirm-input" class="form-input" autocomplete="off">
<input type="hidden" id="confirm-action" value="">
<input type="hidden" id="confirm-token" value="">
<div class="form-actions">
<button class="btn btn-primary" onclick="submitConfirm()">确认执行</button>
<button class="btn btn-secondary" onclick="hideConfirm()">取消</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
/* P0-5: read tokens from data-* attributes to avoid Jinja set scope limits */
const TOKENS = {
start: document.getElementById('confirm-modal').dataset.tokenStart,
stop: document.getElementById('confirm-modal').dataset.tokenStop,
restart:document.getElementById('confirm-modal').dataset.tokenRestart,
reload: document.getElementById('confirm-modal').dataset.tokenReload,
};
let locked = false;
function showConfirm(action) {
if (locked) return;
/* Phrase format: STOP-SQUID / RESTART-SQUID — see tests */
const phrase = action.toUpperCase() + '-SQUID';
document.getElementById('confirm-phrase').textContent = phrase;
document.getElementById('confirm-action').value = action;
document.getElementById('confirm-token').value = TOKENS[action];
document.getElementById('confirm-input').value = '';
document.getElementById('confirm-modal').style.display = 'flex';
document.getElementById('confirm-input').focus();
}
function hideConfirm() {
document.getElementById('confirm-modal').style.display = 'none';
}
function submitConfirm() {
const action = document.getElementById('confirm-action').value;
const token = document.getElementById('confirm-token').value;
const phrase = action.toUpperCase() + '-SQUID';
const input = document.getElementById('confirm-input').value.trim();
if (input !== phrase) {
alert('输入的确认码不正确');
return;
}
hideConfirm();
svc(action, token);
}
function svc(action, token) {
if (locked) return;
if (!confirm(`最后确认: 执行 ${action}?`)) return;
locked = true;
/* lock all buttons */
document.querySelectorAll('.btn-group-vertical .btn').forEach(b => b.disabled = true);
// show countdown
const box = document.getElementById('svc-result');
box.style.display = 'block';
box.className = 'svc-result svc-loading';
let countdown = 30;
box.innerHTML = `执行中: ${action} ... 按钮锁定 ${countdown}s`;
const t = setInterval(() => {
countdown--;
if (countdown > 0) {
box.innerHTML = `执行中: ${action} ... 按钮锁定 ${countdown}s`;
} else {
clearInterval(t);
}
}, 1000);
const form = new FormData();
form.append('confirm_token', token || '');
fetch(`/api/service/${action}`, { method: 'POST', body: form })
.then(r => r.json())
.then(d => {
clearInterval(t);
box.className = 'svc-result ' + (d.ok ? 'svc-ok' : 'svc-err');
box.innerHTML = `<strong>${d.ok ? '✅ 成功' : '❌ 失败'}</strong> (耗时 ${d.elapsed || '?'}s)<br><pre>${d.message || ''}</pre>`;
setTimeout(() => {
locked = false;
document.querySelectorAll('.btn-group-vertical .btn').forEach(b => b.disabled = false);
location.reload();
}, 2000);
})
.catch(e => {
clearInterval(t);
box.className = 'svc-result svc-err';
box.textContent = '请求失败: ' + e;
locked = false;
document.querySelectorAll('.btn-group-vertical .btn').forEach(b => b.disabled = false);
});
}
</script>
{% endblock %}