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
221 lines
10 KiB
HTML
221 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
{{ csrf_meta()|safe }}
|
|
<title>{% block title %}Squid Web Manager{% endblock %}</title>
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body class="{% if theme == 'light' %}theme-light{% endif %}">
|
|
{% if current_user %}
|
|
<aside class="sidebar">
|
|
<div class="brand">
|
|
<div class="brand-logo">🦑</div>
|
|
<div class="brand-name">Squid Manager</div>
|
|
<div class="brand-actions">
|
|
{% include '_theme_toggle.html' %}
|
|
</div>
|
|
</div>
|
|
{# P1-3: instance switcher - shows the currently active Squid instance
|
|
and lets the user swap to a different one without leaving the page. #}
|
|
{% if instances %}
|
|
<div class="instance-switcher">
|
|
<label for="instance-select" class="instance-switcher-label">当前实例</label>
|
|
<select id="instance-select" class="instance-switcher-select"
|
|
onchange="if(this.value){window.location='{{ url_for('instances_activate', inst_id=0) }}'.replace('/0','/'+this.value);}">
|
|
{% for inst in instances %}
|
|
<option value="{{ inst.id }}" {% if active_instance_id == inst.id %}selected{% endif %}>
|
|
{% if active_instance_id == inst.id %}★ {% endif %}{{ inst.name }}{% if not inst.is_local %} (远程){% endif %}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
{# Quick-switch buttons: each is its own POST form (HTML doesn't allow
|
|
nested forms, so we don't wrap them in another <form>). #}
|
|
<div class="instance-switcher-quick">
|
|
{% for inst in instances %}
|
|
{% if active_instance_id != inst.id %}
|
|
<form method="post" action="{{ url_for('instances_activate', inst_id=inst.id) }}" style="display:inline; margin:0;">
|
|
<input type="hidden" name="_csrf" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="next" value="{{ request.full_path }}">
|
|
<button type="submit" class="btn btn-secondary btn-tiny" title="切换到 {{ inst.name }}">→ {{ inst.name }}</button>
|
|
</form>
|
|
{% endif %}
|
|
{% endfor %}
|
|
<a href="{{ url_for('instances_view') }}" class="btn btn-secondary btn-tiny">⚙ 管理</a>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="instance-switcher">
|
|
<label class="instance-switcher-label">当前实例</label>
|
|
<div class="instance-switcher-empty">
|
|
<span class="meta-text">未配置实例 (使用全局设置)</span>
|
|
<a href="{{ url_for('instances_view') }}" class="btn btn-secondary btn-tiny" style="margin-top:6px;">⚙ 添加实例</a>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
<nav>
|
|
<a href="{{ url_for('dashboard') }}" class="{% if request.endpoint == 'dashboard' %}active{% endif %}">
|
|
📊 <span>仪表盘</span>
|
|
</a>
|
|
<a href="{{ url_for('anomalies_view') }}" class="{% if request.endpoint == 'anomalies_view' %}active{% endif %}">
|
|
⚠️ <span>流量异常</span>
|
|
</a>
|
|
<a href="{{ url_for('logs_view') }}" class="{% if request.endpoint in ('logs_view',) %}active{% endif %}">
|
|
📋 <span>日志查询</span>
|
|
</a>
|
|
<a href="{{ url_for('logs_live') }}" class="{% if request.endpoint == 'logs_live' %}active{% endif %}">
|
|
⚡ <span>实时日志</span>
|
|
</a>
|
|
<a href="{{ url_for('logs_import') }}" class="{% if request.endpoint == 'logs_import' %}active{% endif %}">
|
|
📥 <span>日志导入</span>
|
|
</a>
|
|
<a href="{{ url_for('service_view') }}" class="{% if request.endpoint == 'service_view' %}active{% endif %}">
|
|
🛠️ <span>服务控制</span>
|
|
</a>
|
|
<div class="nav-section">配置管理</div>
|
|
<a href="{{ url_for('config_main') }}" class="{% if request.endpoint == 'config_main' %}active{% endif %}">
|
|
⚙️ <span>主配置</span>
|
|
</a>
|
|
<a href="{{ url_for('config_acls') }}" class="{% if request.endpoint == 'config_acls' %}active{% endif %}">
|
|
🚦 <span>ACL 规则表</span>
|
|
</a>
|
|
<a href="{{ url_for('config_rules') }}" class="{% if request.endpoint == 'config_rules' %}active{% endif %}">
|
|
🔐 <span>访问控制</span>
|
|
</a>
|
|
<a href="{{ url_for('config_cache') }}" class="{% if request.endpoint == 'config_cache' %}active{% endif %}">
|
|
💾 <span>缓存目录</span>
|
|
</a>
|
|
<a href="{{ url_for('config_refresh') }}" class="{% if request.endpoint == 'config_refresh' %}active{% endif %}">
|
|
♻️ <span>刷新规则</span>
|
|
</a>
|
|
<a href="{{ url_for('config_auth') }}" class="{% if request.endpoint == 'config_auth' %}active{% endif %}">
|
|
🔑 <span>认证配置</span>
|
|
</a>
|
|
<a href="{{ url_for('config_ssl_bump') }}" class="{% if request.endpoint == 'config_ssl_bump' %}active{% endif %}">
|
|
🔐 <span>SSL Bump</span>
|
|
</a>
|
|
<a href="{{ url_for('config_peers') }}" class="{% if request.endpoint == 'config_peers' %}active{% endif %}">
|
|
🌐 <span>缓存对等</span>
|
|
</a>
|
|
<a href="{{ url_for('config_raw') }}" class="{% if request.endpoint == 'config_raw' %}active{% endif %}">
|
|
📝 <span>原始编辑</span>
|
|
</a>
|
|
<a href="{{ url_for('config_tls') }}" class="{% if request.endpoint == 'config_tls' %}active{% endif %}">
|
|
🔐 <span>TLS 证书</span>
|
|
</a>
|
|
<a href="{{ url_for('config_paths') }}" class="{% if request.endpoint == 'config_paths' %}active{% endif %}">
|
|
🗂️ <span>路径设置</span>
|
|
</a>
|
|
<div class="nav-section">运维</div>
|
|
<a href="{{ url_for('backups_view') }}" class="{% if request.endpoint == 'backups_view' %}active{% endif %}">
|
|
📦 <span>配置备份</span>
|
|
</a>
|
|
<a href="{{ url_for('audit_view') }}" class="{% if request.endpoint == 'audit_view' %}active{% endif %}">
|
|
📜 <span>审计日志</span>
|
|
</a>
|
|
<a href="{{ url_for('ops_logs') }}" class="{% if request.endpoint == 'ops_logs' %}active{% endif %}">
|
|
📂 <span>日志状态</span>
|
|
</a>
|
|
<a href="{{ url_for('ops_logrotate') }}" class="{% if request.endpoint == 'ops_logrotate' %}active{% endif %}">
|
|
🔁 <span>日志轮转</span>
|
|
</a>
|
|
<a href="{{ url_for('alerts_view') }}" class="{% if request.endpoint == 'alerts_view' %}active{% endif %}">
|
|
🚨 <span>告警规则</span>
|
|
</a>
|
|
<a href="{{ url_for('instances_view') }}" class="{% if request.endpoint == 'instances_view' %}active{% endif %}">
|
|
🖥️ <span>多实例管理</span>
|
|
</a>
|
|
<a href="{{ url_for('performance_view') }}" class="{% if request.endpoint == 'performance_view' %}active{% endif %}">
|
|
📊 <span>实时性能</span>
|
|
</a>
|
|
{% if current_user and current_user.role == 'admin' %}
|
|
<a href="{{ url_for('terminal_view') }}" class="{% if request.endpoint == 'terminal_view' %}active{% endif %}">
|
|
💻 <span>Web 终端</span>
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('change_password') }}" class="{% if request.endpoint == 'change_password' %}active{% endif %}">
|
|
🔒 <span>修改密码</span>
|
|
</a>
|
|
<a href="{{ url_for('logout') }}" class="nav-danger">
|
|
🚪 <span>退出登录</span>
|
|
</a>
|
|
</nav>
|
|
</aside>
|
|
{% endif %}
|
|
|
|
<main class="{% if not current_user %}content content-full{% else %}content{% endif %}">
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<div class="flash-container">
|
|
{% for category, msg in messages %}
|
|
<div class="flash flash-{{ category }}">{{ msg }}</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endwith %}
|
|
{% block content %}{% endblock %}
|
|
</main>
|
|
|
|
{% if current_user %}
|
|
<!-- Session idle warning toast -->
|
|
<div id="session-warn" class="session-warn" style="display:none;">
|
|
<div class="session-warn-inner">
|
|
<span>⏰ 会话即将过期 (<span id="session-warn-sec">60</span>s),点击任意位置续期</span>
|
|
<button onclick="event.stopPropagation(); document.body.click();" class="btn btn-small btn-primary">续期</button>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% block scripts %}{% endblock %}
|
|
<script>
|
|
// CSRF helper: auto-attach token to every fetch() with unsafe method
|
|
(function() {
|
|
const meta = document.querySelector('meta[name="csrf-token"]');
|
|
if (!meta) return;
|
|
const token = meta.getAttribute('content');
|
|
const origFetch = window.fetch;
|
|
window.fetch = function(url, opts) {
|
|
opts = opts || {};
|
|
const method = (opts.method || 'GET').toUpperCase();
|
|
if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
|
|
opts.headers = opts.headers || {};
|
|
// Don't overwrite if user already set it
|
|
if (!opts.headers['X-CSRF-Token'] && !opts.headers['x-csrf-token']) {
|
|
opts.headers['X-CSRF-Token'] = token;
|
|
}
|
|
}
|
|
return origFetch(url, opts);
|
|
};
|
|
})();
|
|
|
|
// Session idle warning: warn user 60s before session expires
|
|
(function() {
|
|
const idleSec = {{ session_idle_to|int }};
|
|
if (!idleSec || idleSec < 120) return;
|
|
let lastActivity = Date.now();
|
|
['click','keydown','mousemove','scroll','touchstart'].forEach(ev =>
|
|
document.addEventListener(ev, () => { lastActivity = Date.now(); }, { passive: true })
|
|
);
|
|
const warn = document.getElementById('session-warn');
|
|
const secEl = document.getElementById('session-warn-sec');
|
|
if (!warn) return;
|
|
setInterval(() => {
|
|
const idle = (Date.now() - lastActivity) / 1000;
|
|
const remaining = idleSec - idle;
|
|
if (remaining <= 60 && remaining > 0) {
|
|
warn.style.display = 'block';
|
|
if (secEl) secEl.textContent = Math.ceil(remaining);
|
|
} else if (remaining <= 0) {
|
|
// hard reload to trigger session timeout redirect
|
|
location.reload();
|
|
} else {
|
|
warn.style.display = 'none';
|
|
}
|
|
}, 5000);
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|