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
175 lines
7.2 KiB
HTML
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 %}
|