feat: 重构为 SOCKS5 代理管理平台 v2.0

架构:
- engine/server.py: asyncio SOCKS5 服务器引擎(含隧道/认证/限速)
- engine/instances.py: 多实例管理器(独立事件循环/热启动/热停止)
- services/user_service.py: 用户认证/流量统计/IP白名单/防爆破
- services/stats_service.py: 实时监控/流量趋势/连接追踪
- services/backup_service.py: 数据库备份与恢复
- api/v1.py: 完整 RESTful API
- web/routes.py: Web 管理面板(暗色 Bootstrap 5 主题)

功能覆盖:
 多实例 SOCKS5 管理(创建/启动/停止/重启/热加载)
 用户认证(无认证/账号密码/流量上限/限速/并发限制)
 IP 白名单/黑名单/防爆破
 实时仪表盘(CPU/内存/网络/活跃连接)
 流量统计(30天趋势图/用户排名/实例统计)
 活跃连接追踪
 审计日志(事件/用户/IP 筛选)
 数据库备份与恢复
 管理员密码保护
 16 项功能测试全部通过
This commit is contained in:
Your Name
2026-07-16 17:31:53 +08:00
parent d59699b5ce
commit 558bd8153f
35 changed files with 3245 additions and 817 deletions
+221 -75
View File
@@ -1,86 +1,232 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{% block title %}SOCKS Manager{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<style>
body { background:#0f1117; color:#e2e8f0; }
.card { background:#1a1d27; border:1px solid #2a2d3a; }
.navbar { background:#151822 !important; border-bottom:1px solid #2a2d3a !important; }
.navbar-brand { color:#818cf8 !important; font-weight:700; }
.nav-link { color:#a0aec0 !important; }
.nav-link:hover, .nav-link.active { color:#818cf8 !important; }
.table { color:#e2e8f0; }
.table-dark { background:#1a1d27; }
.form-control, .form-select { background:#0f1117; border-color:#2a2d3a; color:#e2e8f0; }
.form-control:focus, .form-select:focus { background:#0f1117; border-color:#818cf8; color:#e2e8f0; }
.badge-online { background:#10b981; }
.badge-offline { background:#ef4444; }
.badge-unknown { background:#6b7280; }
.badge-error { background:#f59e0b; }
.status-dot { width:10px; height:10px; border-radius:50%; display:inline-block; margin-right:6px; }
.dot-online { background:#10b981; }
.dot-offline { background:#ef4444; }
.dot-unknown { background:#6b7280; }
.dot-error { background:#f59e0b; }
.sidebar { width:240px; min-height:100vh; position:fixed; left:0; top:0; bottom:0; background:#151822; border-right:1px solid #2a2d3a; padding-top:60px; }
.sidebar .nav-link { padding:.6rem 1.2rem; border-radius:.4rem; margin:.2rem .8rem; }
.sidebar .nav-link:hover, .sidebar .nav-link.active { background:#1e2235; }
.main-content { margin-left:240px; padding:1.5rem; }
@media (max-width:768px) { .sidebar { display:none; } .main-content { margin-left:0; } }
.stat-card .stat-value { font-size:2rem; font-weight:700; }
.toast-container { position:fixed; top:1rem; right:1rem; z-index:1060; }
</style>
{% block extra_style %}{% endblock %}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>{% block title %}SOCKS Manager{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js" rel="stylesheet">
<style>
/* ── 全局 ─────────────────────────────────────────────────────── */
:root {
--bg-primary:#0b0e14; --bg-secondary:#161922; --bg-tertiary:#1e2235;
--border:#1f2335; --text:#e2e8f0; --text-muted:#94a3b8;
--accent:#818cf8; --accent-hover:#6366f1;
--green:#10b981; --red:#ef4444; --yellow:#f59e0b; --gray:#6b7280;
--card-bg:#161922; --input-bg:#0b0e14;
}
* { box-sizing:border-box; }
body { background:var(--bg-primary); color:var(--text);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
font-size:14px; line-height:1.6; }
a { color:var(--accent); text-decoration:none; }
a:hover { color:var(--accent-hover); }
/* ── 导航 ─────────────────────────────────────────────────────── */
.navbar { background:var(--bg-secondary)!important; border-bottom:1px solid var(--border)!important;
padding:.6rem 1rem; }
.navbar-brand { color:var(--accent)!important; font-weight:700; font-size:1.1rem; }
.navbar-brand i { margin-right:6px; }
.nav-link { color:var(--text-muted)!important; font-size:14px; padding:.5rem 1rem!important; }
.nav-link:hover, .nav-link.active { color:var(--accent)!important; }
/* ── 侧边栏 ───────────────────────────────────────────────────── */
.sidebar { width:220px; min-height:100vh; position:fixed; left:0; top:56px; bottom:0;
background:var(--bg-secondary); border-right:1px solid var(--border); padding:.5rem 0; z-index:100; }
.sidebar .nav-link { padding:.55rem 1.2rem; border-radius:0 .4rem .4rem 0; margin:.15rem .6rem; font-size:14px; }
.sidebar .nav-link:hover { background:var(--bg-tertiary); color:var(--accent)!important; }
.sidebar .nav-link.active { background:var(--bg-tertiary); color:var(--accent)!important;
border-left:3px solid var(--accent); }
.sidebar .nav-link i { margin-right:10px; width:18px; text-align:center; }
.sidebar .sidebar-section { padding:.3rem 1.2rem .1rem; font-size:11px; color:var(--text-muted);
text-transform:uppercase; letter-spacing:.05em; margin-top:.8rem; }
/* ── 主内容 ───────────────────────────────────────────────────── */
.main-content { margin-left:220px; padding:1.2rem 1.5rem; min-height:calc(100vh - 56px); }
/* ── 卡片 ─────────────────────────────────────────────────────── */
.card { background:var(--card-bg); border:1px solid var(--border); border-radius:.6rem; margin-bottom:1rem; }
.card-header { background:transparent; border-bottom:1px solid var(--border); padding:.8rem 1rem; }
.card-body { padding:1rem; }
.card-footer { background:transparent; border-top:1px solid var(--border); }
/* ── 表格 ─────────────────────────────────────────────────────── */
.table { color:var(--text); margin-bottom:0; }
.table thead th { background:var(--bg-tertiary); color:var(--text-muted); font-weight:600;
font-size:12px; text-transform:uppercase; letter-spacing:.04em; border-bottom:1px solid var(--border);
padding:.65rem .8rem; }
.table tbody td { padding:.6rem .8rem; border-bottom:1px solid var(--border); font-size:14px;
vertical-align:middle; }
.table tbody tr:hover { background:var(--bg-tertiary); }
.table-dark { background:var(--card-bg); }
/* ── 表单 ─────────────────────────────────────────────────────── */
.form-label { color:var(--text-muted); font-size:13px; margin-bottom:.3rem; font-weight:500; }
.form-control, .form-select { background:var(--input-bg); border:1px solid var(--border);
color:var(--text); border-radius:.4rem; font-size:14px; padding:.5rem .7rem; }
.form-control:focus, .form-select:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(129,140,248,.12);
background:var(--input-bg); color:var(--text); }
.form-control::placeholder { color:#5a6178; }
.form-text { color:var(--text-muted); font-size:12px; }
.form-check-input { background-color:var(--input-bg); border-color:var(--border); }
.form-check-input:checked { background-color:var(--accent); border-color:var(--accent); }
/* ── 按钮 ─────────────────────────────────────────────────────── */
.btn-primary { background:var(--accent); border-color:var(--accent); }
.btn-primary:hover { background:var(--accent-hover); border-color:var(--accent-hover); }
.btn-outline-primary { color:var(--accent); border-color:var(--accent); }
.btn-outline-primary:hover { background:var(--accent); border-color:var(--accent); }
.btn-outline-secondary { color:var(--text-muted); border-color:var(--border); }
.btn-outline-secondary:hover { color:var(--text); border-color:var(--text-muted); background:var(--bg-tertiary); }
.btn-outline-danger { color:var(--red); border-color:var(--red); }
.btn-outline-danger:hover { background:var(--red); border-color:var(--red); color:#fff; }
.btn-outline-success { color:var(--green); border-color:var(--green); }
.btn-outline-success:hover { background:var(--green); border-color:var(--green); color:#fff; }
.btn-outline-warning { color:var(--yellow); border-color:var(--yellow); }
.btn-outline-warning:hover { background:var(--yellow); border-color:var(--yellow); color:#000; }
.btn-sm { font-size:13px; padding:.3rem .6rem; }
/* ── 徽章 ─────────────────────────────────────────────────────── */
.badge { font-size:11px; padding:.3em .6em; border-radius:.3rem; }
.badge-success { background:rgba(16,185,129,.15); color:var(--green); }
.badge-danger { background:rgba(239,68,68,.15); color:var(--red); }
.badge-warning { background:rgba(245,158,11,.15); color:var(--yellow); }
.badge-secondary { background:rgba(107,114,128,.15); color:var(--gray); }
.badge-info { background:rgba(129,140,248,.15); color:var(--accent); }
/* ── 状态点 ───────────────────────────────────────────────────── */
.status-dot { width:9px; height:9px; border-radius:50%; display:inline-block; margin-right:5px;
vertical-align:middle; }
.dot-online { background:var(--green); box-shadow:0 0 6px rgba(16,185,129,.6); }
.dot-offline { background:var(--red); }
.dot-warning { background:var(--yellow); }
.dot-gray { background:var(--gray); }
/* ── 统计卡片 ─────────────────────────────────────────────────── */
.stat-card { background:var(--card-bg); border:1px solid var(--border); border-radius:.6rem; padding:1rem; }
.stat-card .stat-label { color:var(--text-muted); font-size:12px; text-transform:uppercase; letter-spacing:.04em; }
.stat-card .stat-value { font-size:1.8rem; font-weight:700; margin:.2rem 0; }
.stat-card .stat-sub { color:var(--text-muted); font-size:12px; }
/* ── 表格代码 ─────────────────────────────────────────────────── */
code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem; border-radius:.25rem;
font-size:13px; font-family:"SF Mono",Monaco,Menlo,Consolas,monospace; }
/* ── 分页 ─────────────────────────────────────────────────────── */
.pagination { margin-bottom:0; }
.page-link { color:var(--accent); background:var(--card-bg); border-color:var(--border); }
.page-link:hover { background:var(--bg-tertiary); color:var(--accent); }
.page-item.active .page-link { background:var(--accent); border-color:var(--accent); color:#fff; }
.page-item.disabled .page-link { color:var(--gray); }
/* ── 模态框 ───────────────────────────────────────────────────── */
.modal-content { background:var(--card-bg); border:1px solid var(--border); color:var(--text); }
.modal-header { border-bottom:1px solid var(--border); }
.modal-footer { border-top:1px solid var(--border); }
.btn-close { filter:invert(1); }
/* ── 消息 ─────────────────────────────────────────────────────── */
.toast { background:var(--card-bg); border:1px solid var(--border); color:var(--text); }
.toast-container { position:fixed; top:1rem; right:1rem; z-index:1060; }
/* ── 响应式 ───────────────────────────────────────────────────── */
@media(max-width:768px){
.sidebar { display:none; }
.main-content { margin-left:0; padding:1rem; }
}
/* ── 滚动条 ───────────────────────────────────────────────────── */
::-webkit-scrollbar { width:6px; height:6px; }
::-webkit-scrollbar-track { background:var(--bg-primary); }
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
::-webkit-scrollbar-thumb:hover { background:var(--gray); }
</style>
{% block extra_style %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container-fluid">
<a class="navbar-brand" href="/"><i class="bi bi-hdd-rack"></i> SOCKS Manager</a>
<div class="d-flex align-items-center">
<span class="text-muted small me-3">{{ g.user if g and g.user else 'admin' }}</span>
<a href="/logout" class="btn btn-outline-secondary btn-sm">退出</a>
<!-- 导航栏 -->
<nav class="navbar navbar-expand fixed-top">
<div class="container-fluid">
<a class="navbar-brand" href="/"><i class="bi bi-hdd-rack"></i> SOCKS Manager</a>
<div class="d-flex align-items-center gap-2">
{% if nav.instances %}<span class="text-muted small">仪表盘</span>{% endif %}
<a href="/logout" class="btn btn-outline-secondary btn-sm"><i class="bi bi-box-arrow-right"></i> 退出</a>
</div>
</div>
</nav>
<!-- 侧边栏 -->
<div class="sidebar">
<div class="nav flex-column">
<div class="sidebar-section">概览</div>
<a class="nav-link {% if nav.instances %}active{% endif %}" href="/dashboard">
<i class="bi bi-speedometer2"></i> 仪表盘</a>
<a class="nav-link {% if nav.instances %}active{% endif %}" href="/instances">
<i class="bi bi-server"></i> 代理实例</a>
<div class="sidebar-section">用户</div>
<a class="nav-link {% if nav.users %}active{% endif %}" href="/users">
<i class="bi bi-people"></i> 用户管理</a>
<div class="sidebar-section">监控</div>
<a class="nav-link {% if nav.stats %}active{% endif %}" href="/stats">
<i class="bi bi-graph-up"></i> 流量统计</a>
<a class="nav-link {% if nav.stats %}active{% endif %}" href="/stats/connections">
<i class="bi bi-activity"></i> 活跃连接</a>
<a class="nav-link {% if nav.logs %}active{% endif %}" href="/logs">
<i class="bi bi-journal-text"></i> 审计日志</a>
<div class="sidebar-section">系统</div>
<a class="nav-link {% if nav.system %}active{% endif %}" href="/system">
<i class="bi bi-shield-lock"></i> 系统管理</a>
</div>
</div>
<!-- 主内容 -->
<div class="main-content">
<!-- 闪消息 -->
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="mb-3">
{% for category, msg in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ msg }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
</div>
</div>
</nav>
{% endif %}
{% endwith %}
<div class="sidebar">
<div class="nav flex-column">
<a class="nav-link {% if 'dashboard' in request.endpoint|default('') %}active{% endif %}" href="/dashboard">
<i class="bi bi-speedometer2"></i> 仪表盘</a>
<a class="nav-link {% if 'proxy' in request.endpoint|default('') %}active{% endif %}" href="/proxies">
<i class="bi bi-globe"></i> 代理列表</a>
<a class="nav-link {% if 'group' in request.endpoint|default('') %}active{% endif %}" href="/groups">
<i class="bi bi-collection"></i> 分组管理</a>
<a class="nav-link {% if 'logs' in request.endpoint|default('') %}active{% endif %}" href="/logs">
<i class="bi bi-journal-text"></i> 日志</a>
</div>
</div>
{% block content %}{% endblock %}
</div>
<div class="main-content">
<div class="toast-container" id="toastContainer"></div>
{% block content %}{% endblock %}
</div>
<!-- Toast 容器 -->
<div class="toast-container" id="toastContainer"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
function toast(msg, type='info') {
const c = document.getElementById('toastContainer');
const t = document.createElement('div');
t.className = `toast show align-items-center text-bg-${type} border-0`;
t.setAttribute('role','alert');
t.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
c.appendChild(t);
setTimeout(()=>t.remove(), 4000);
}
async function apiFetch(url, opts={}) {
const r = await fetch(url, {...opts, headers:{'Content-Type':'application/json', ...opts.headers}});
return r.json();
}
</script>
{% block extra_script %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script>
function toast(msg, type='info') {
const c = document.getElementById('toastContainer');
if (!c) return;
const colors = { success:'bg-success', danger:'bg-danger', warning:'bg-warning', info:'bg-primary' };
const t = document.createElement('div');
t.className = `toast show align-items-center ${colors[type]||'bg-primary'} text-white border-0`;
t.innerHTML = `<div class="d-flex"><div class="toast-body">${msg}</div><button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button></div>`;
c.appendChild(t);
setTimeout(()=>t.remove(), 4000);
}
function fmtBytes(b) {
if (!b) return '0 B';
const u = ['B','KB','MB','GB','TB'];
let i = 0;
while (b >= 1024 && i < u.length-1) { b/=1024; i++; }
return b.toFixed(1)+' '+u[i];
}
function confirmDelete(msg) { return confirm(msg || '确认删除?此操作不可恢复。'); }
</script>
{% block extra_script %}{% endblock %}
</body>
</html>
+53
View File
@@ -0,0 +1,53 @@
{% extends "base.html" %}
{% block title %}活跃连接 · SOCKS Manager{% endblock %}
{% block content %}
<h5 class="mb-3"><i class="bi bi-activity"></i> 活跃连接 <span class="text-muted small">({{ conns|length }} 条)</span></h5>
<div class="card">
<div class="card-body p-0">
<table class="table table-dark table-hover align-middle">
<thead>
<tr><th>ID</th><th>实例</th><th>用户</th><th>源地址</th><th>目标地址</th>
<th>协议</th><th>开始时间</th><th>入站</th><th>出站</th><th>操作</th></tr>
</thead>
<tbody>
{% for c in conns %}
<tr>
<td><code>{{ c.id[:12] }}</code></td>
<td>{{ c.instance }}</td>
<td>{{ c.username or '-' }}</td>
<td><code>{{ c.src }}</code></td>
<td><code>{{ c.dst }}</code></td>
<td><span class="badge badge-info">{{ c.protocol }}</span></td>
<td><small class="text-muted">{{ c.started }}</small></td>
<td>{{ fmtBytes(c.bytes_in) }}</td>
<td>{{ fmtBytes(c.bytes_out) }}</td>
<td>
<form method="post" style="display:inline"
onsubmit="return confirm('确认断开此连接?')">
<button class="btn btn-outline-danger btn-sm" type="submit">
<i class="bi bi-x-circle"></i> 断开</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not conns %}
<div class="text-center py-5 text-muted">
<i class="bi bi-activity text-muted" style="font-size:2rem"></i>
<p>当前无活跃连接</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
// 每 5 秒刷新
setInterval(() => location.reload(), 5000);
</script>
{% endblock %}
+133 -63
View File
@@ -2,70 +2,97 @@
{% block title %}仪表盘 · SOCKS Manager{% endblock %}
{% block content %}
<h4 class="mb-4"><i class="bi bi-speedometer2"></i> 仪表盘</h4>
<h5 class="mb-3"><i class="bi bi-speedometer2"></i> 仪表盘</h5>
<div class="row g-3 mb-4">
<div class="col-md-3"><div class="card stat-card p-3">
<div class="text-muted small">代理总数</div>
<div class="stat-value">{{ total }}</div>
</div></div>
<div class="col-md-3"><div class="card stat-card p-3">
<div class="text-muted small"><span class="status-dot dot-online"></span>在线</div>
<div class="stat-value text-success">{{ online }}</div>
</div></div>
<div class="col-md-3"><div class="card stat-card p-3">
<div class="text-muted small"><span class="status-dot dot-offline"></span>离线</div>
<div class="stat-value text-danger">{{ offline }}</div>
</div></div>
<div class="col-md-3"><div class="card stat-card p-3">
<div class="text-muted small">分组数</div>
<div class="stat-value">{{ groups }}</div>
</div></div>
</div>
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-globe"></i> 最近活跃代理</span>
<button class="btn btn-outline-primary btn-sm" onclick="checkAll()">
<i class="bi bi-arrow-clockwise"></i> 全部检测</button>
<!-- 系统状态卡片 -->
<div class="row g-3 mb-3">
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">CPU 使用率</div>
<div class="stat-value text-info">{{ "%.1f"|format(s.system.cpu if s.system else 0) }}%</div>
<div class="stat-sub">实时</div>
</div>
</div>
<div class="card-body p-0">
{% if proxies %}
<table class="table table-dark table-hover mb-0">
<thead><tr><th>名称</th><th>地址</th><th>状态</th><th>延迟</th><th>最后检测</th></tr></thead>
<tbody>
{% for p in proxies %}
<tr>
<td>{{ p.name }}</td>
<td><code>{{ p.host }}:{{ p.port }}</code></td>
<td><span class="status-dot dot-{{ p.status }}"></span>{{ p.status }}</td>
<td>{{ "%.1f"|format(p.latency_ms) if p.latency_ms else '-' }}ms</td>
<td>{{ p.last_check.strftime('%Y-%m-%d %H:%M') if p.last_check else '-' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<div class="text-center py-4 text-muted">暂无代理,<a href="/proxies/add" class="text-info">添加一个</a></div>
{% endif %}
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">内存使用</div>
<div class="stat-value text-warning">{{ s.system.mem_percent if s.system else 0 }}%</div>
<div class="stat-sub">{{ s.system.mem_used_mb if s.system else 0 }} / {{ s.system.mem_total_mb if s.system else 0 }} MB</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">活跃连接</div>
<div class="stat-value text-success">{{ s.active_connections }}</div>
<div class="stat-sub">当前</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">今日日志</div>
<div class="stat-value">{{ s.today_logs }}</div>
<div class="stat-sub"></div>
</div>
</div>
</div>
<!-- 业务概览 -->
<div class="row g-3 mb-3">
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">代理实例</div>
<div class="stat-value">{{ s.running_instances }}<span class="text-muted small"> / {{ s.total_instances }}</span></div>
<div class="stat-sub">运行中 / 总数</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">活跃用户</div>
<div class="stat-value text-primary">{{ s.active_users }}<span class="text-muted small"> / {{ s.total_users }}</span></div>
<div class="stat-sub">可用 / 总数</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">入站流量</div>
<div class="stat-value text-info">{{ "%.1f"|format((s.system.net_bytes_recv or 0) / 1024 / 1024) }} MB</div>
<div class="stat-sub">累计</div>
</div>
</div>
<div class="col-md-6 col-lg-3">
<div class="stat-card">
<div class="stat-label">出站流量</div>
<div class="stat-value text-info">{{ "%.1f"|format((s.system.net_bytes_sent or 0) / 1024 / 1024) }} MB</div>
<div class="stat-sub">累计</div>
</div>
</div>
</div>
<!-- 实时指标图表 -->
<div class="card">
<div class="card-header"><span><i class="bi bi-journal-text"></i> 最近操作日志</span></div>
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-graph-up"></i> 实时系统指标(最近 10 分钟)</span>
<span class="text-muted small">每 10 秒刷新</span>
</div>
<div class="card-body">
<canvas id="metricsChart" height="80"></canvas>
</div>
</div>
<!-- 最近审计日志 -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-journal-text"></i> 最近操作</span>
<a href="/logs" class="btn btn-sm btn-outline-secondary">查看全部</a>
</div>
<div class="card-body p-0">
<table class="table table-dark mb-0 table-sm">
<thead><tr><th>时间</th><th>级别</th><th>操作</th><th>目标</th><th>详情</th></tr></thead>
<table class="table table-dark table-sm">
<thead><tr><th>时间</th><th>事件</th><th>用户</th><th>来源IP</th><th>详情</th></tr></thead>
<tbody>
{% for l in recent %}
<tr>
<td>{{ l.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td><span class="badge bg-{{ 'success' if l.level=='INFO' else 'warning' if l.level=='WARN' else 'danger' }}">{{ l.level }}</span></td>
<td>{{ l.action }}</td>
<td>{{ l.target or '-' }}</td>
<td>{{ l.detail or '-' }}</td>
</tr>
{% endfor %}
{% set logs = namespace(count=0) %}
{% for _ in range(0) %}{% endfor %}
<tr><td colspan="5" class="text-center text-muted py-4">
<a href="/logs">查看审计日志 &rarr;</a></td></tr>
</tbody>
</table>
</div>
@@ -75,14 +102,57 @@
{% block extra_script %}
<script>
async function checkAll() {
const btn = event.currentTarget;
btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm"></span> 检测中...';
// 实时指标图表
const ctx = document.getElementById('metricsChart');
const chart = new Chart(ctx, {
type: 'line',
data: {
labels: [],
datasets: [
{ label: 'CPU %', data: [], borderColor: '#818cf8', backgroundColor: 'rgba(129,140,248,.1)',
tension: 0.4, fill: true },
{ label: '内存 %', data: [], borderColor: '#f59e0b', backgroundColor: 'rgba(245,158,11,.1)',
tension: 0.4, fill: true },
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#94a3b8' } },
},
scales: {
x: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' } },
y: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' }, min: 0, max: 100 },
},
}
});
// 每 10 秒刷新
async function refreshMetrics() {
try {
const r = await apiFetch('/health/check-all');
toast(`检测完成: ${r.checked} 个, ${r.online} 在线, ${r.offline} 离线`, r.offline?'warning':'success');
} catch(e) { toast('检测失败: '+e.message, 'danger'); }
btn.disabled = false; btn.innerHTML = '<i class="bi bi-arrow-clockwise"></i> 全部检测';
const r = await fetch('/api/stats/system');
const data = await r.json();
const history = data.history || [];
if (history.length === 0) return;
const labels = history.map(h => new Date(h.ts).toLocaleTimeString('zh-CN', {hour:'2-digit',minute:'2-digit',second:'2-digit'}));
const cpuData = history.map(h => h.cpu);
const memData = history.map(h => h.mem_percent);
chart.data.labels = labels;
chart.data.datasets[0].data = cpuData;
chart.data.datasets[1].data = memData;
chart.update('none');
// 更新 CPU 显示
const latest = history[history.length-1];
const cpuEl = document.querySelector('.stat-card .stat-value.text-info');
if (cpuEl) cpuEl.textContent = latest.cpu.toFixed(1) + '%';
const memEl = document.querySelectorAll('.stat-card .stat-value.text-warning')[0];
if (memEl) memEl.textContent = latest.mem_percent + '%';
const memSub = document.querySelectorAll('.stat-card .stat-sub')[1];
if (memSub) memSub.textContent = latest.mem_used_mb + ' / ' + latest.mem_total_mb + ' MB';
} catch(e) {}
}
setInterval(refreshMetrics, 10000);
</script>
{% endblock %}
-77
View File
@@ -1,77 +0,0 @@
{% extends "base.html" %}
{% block title %}分组管理 · SOCK Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0"><i class="bi bi-collection"></i> 分组管理</h4>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addGroupModal">
<i class="bi bi-plus-lg"></i> 添加分组</button>
</div>
<div class="row g-3">
{% for g in groups %}
<div class="col-md-4 col-lg-3">
<div class="card" style="border-left:4px solid {{ g.color }}">
<div class="card-body">
<h6 style="color:{{ g.color }}">{{ g.name }}</h6>
<p class="text-muted small mb-1">{{ g.description or '无描述' }}</p>
<div class="d-flex justify-content-between align-items-center">
<span class="badge bg-secondary">{{ g.proxies|length }} 个代理</span>
<button class="btn btn-outline-danger btn-sm" onclick="deleteGroup({{ g.id }}, '{{ g.name }}')">
<i class="bi bi-trash"></i></button>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% if not groups %}
<div class="text-center py-5 text-muted">暂无分组</div>
{% endif %}
<!-- Add Group Modal -->
<div class="modal fade" id="addGroupModal">
<div class="modal-dialog modal-sm">
<div class="modal-content" style="background:#1a1d27;border-color:#2a2d3a">
<div class="modal-header" style="border-color:#2a2d3a">
<h6 class="modal-title">添加分组</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3"><label class="form-label">名称</label>
<input id="gName" class="form-control" required></div>
<div class="mb-3"><label class="form-label">颜色</label>
<input id="gColor" type="color" class="form-control form-control-color" value="#6366f1"></div>
<div class="mb-3"><label class="form-label">描述</label>
<input id="gDesc" class="form-control"></div>
</div>
<div class="modal-footer" style="border-color:#2a2d3a">
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-sm btn-primary" onclick="addGroup()">添加</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
async function addGroup() {
const name = document.getElementById('gName').value.trim();
if (!name) return;
try {
await fetch('/groups/add', {method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'},
body:`name=${encodeURIComponent(name)}&color=${document.getElementById('gColor').value}&description=${encodeURIComponent(document.getElementById('gDesc').value)}`});
toast('分组已添加', 'success'); location.reload();
} catch(e) { toast('添加失败', 'danger'); }
}
async function deleteGroup(id, name) {
if (!confirm(`确认删除分组 "${name}"`)) return;
const r = await fetch(`/groups/${id}`, {method:'DELETE'});
const j = await r.json();
if (j.error) { toast(j.error, 'warning'); } else { toast('分组已删除', 'success'); location.reload(); }
}
</script>
{% endblock %}
+238
View File
@@ -0,0 +1,238 @@
{% extends "base.html" %}
{% block title %}代理实例 · SOCKS Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<h5 class="mb-0"><i class="bi bi-server"></i> 代理实例</h5>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addInstanceModal">
<i class="bi bi-plus-lg"></i> 新建实例</button>
</div>
<!-- 实例列表 -->
<div class="card">
<div class="card-body p-0">
<table class="table table-dark align-middle">
<thead>
<tr><th>名称</th><th>监听地址</th><th>认证</th><th>限速</th><th>并发</th>
<th>状态</th><th>活跃连接</th><th>操作</th></tr>
</thead>
<tbody>
{% for inst in instances %}
<tr>
<td>
<strong>{{ inst.name }}</strong>
{% if inst.notes %}<br><small class="text-muted">{{ inst.notes }}</small>{% endif %}
</td>
<td><code>{{ inst.host }}:{{ inst.port }}</code></td>
<td>
<span class="badge badge-{{ 'info' if inst.auth_method=='userpass' else 'secondary' }}">
{{ '账号密码' if inst.auth_method=='userpass' else '无认证' }}</span>
</td>
<td>
<span class="text-muted small">↓{{ inst.bandwidth_down or '不限' }} ↑{{ inst.bandwidth_up or '不限' }} Mbps</span>
</td>
<td>{{ inst.max_concurrent }}</td>
<td>
<span class="status-dot {{ 'dot-online' if inst.running else 'dot-offline' }}"></span>
{{ '运行中' if inst.running else '已停止' }}
</td>
<td>
<span class="badge badge-{{ 'success' if inst.active_connections>0 else 'secondary' }}">
{{ inst.active_connections }}</span>
</td>
<td>
<div class="btn-group btn-group-sm">
{% if not inst.running %}
<form method="post" action="/instances/{{ inst.id }}/start" style="display:inline">
<button class="btn btn-outline-success"><i class="bi bi-play-fill"></i></button>
</form>
{% else %}
<form method="post" action="/instances/{{ inst.id }}/stop" style="display:inline">
<button class="btn btn-outline-warning"><i class="bi bi-pause-fill"></i></button>
</form>
<form method="post" action="/instances/{{ inst.id }}/restart" style="display:inline">
<button class="btn btn-outline-primary"><i class="bi bi-arrow-clockwise"></i></button>
</form>
{% endif %}
<button class="btn btn-outline-secondary" data-bs-toggle="modal"
data-bs-target="#editInstanceModal"
data-name="{{ inst.name }}"
data-host="{{ inst.host }}"
data-port="{{ inst.port }}"
data-timeout="{{ inst.timeout }}"
data-auth="{{ inst.auth_method }}"
data-bw-down="{{ inst.bandwidth_down }}"
data-bw-up="{{ inst.bandwidth_up }}"
data-concurrent="{{ inst.max_concurrent }}"
data-notes="{{ inst.notes }}"
data-id="{{ inst.id }}">
<i class="bi bi-pencil"></i>
</button>
<form method="post" action="/instances/{{ inst.id }}/delete" style="display:inline"
onsubmit="return confirmDelete('确认删除实例 {{ inst.name }}')">
<button class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not instances %}
<div class="text-center py-5 text-muted">
<i class="bi bi-server text-muted" style="font-size:2rem;display:block;margin-bottom:.5rem"></i>
暂无实例,点击上方"新建实例"开始
</div>
{% endif %}
</div>
</div>
<!-- 添加实例模态框 -->
<div class="modal fade" id="addInstanceModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">新建代理实例</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="/instances/add">
<div class="modal-body">
<div class="mb-3"><label class="form-label">实例名称</label>
<input name="name" class="form-control" required placeholder="例: main-proxy">
</div>
<div class="row g-2 mb-3">
<div class="col-7">
<label class="form-label">监听地址</label>
<input name="listen_host" class="form-control" value="0.0.0.0" placeholder="0.0.0.0 或 ::">
</div>
<div class="col-5">
<label class="form-label">监听端口</label>
<input name="listen_port" type="number" class="form-control" value="1080" min="1" max="65535">
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label">超时 (秒)</label>
<input name="timeout" type="number" class="form-control" value="30">
</div>
<div class="col-6">
<label class="form-label">认证方式</label>
<select name="auth_method" class="form-select">
<option value="none">无认证</option>
<option value="userpass">账号密码</option>
</select>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label">下行限速 (Mbps, 0=不限)</label>
<input name="bandwidth_down" type="number" class="form-control" value="0" step="0.1">
</div>
<div class="col-6">
<label class="form-label">上行限速 (Mbps, 0=不限)</label>
<input name="bandwidth_up" type="number" class="form-control" value="0" step="0.1">
</div>
</div>
<div class="mb-3">
<label class="form-label">最大并发连接数</label>
<input name="max_concurrent" type="number" class="form-control" value="10">
</div>
<div class="mb-3">
<label class="form-label">备注</label>
<textarea name="notes" class="form-control" rows="2"></textarea>
</div>
<div class="form-check">
<input type="checkbox" name="start" class="form-check-input" id="startCheck" checked>
<label class="form-check-label" for="startCheck">创建后立即启动</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary btn-sm">创建</button>
</div>
</form>
</div>
</div>
</div>
<!-- 编辑实例模态框 -->
<div class="modal fade" id="editInstanceModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">编辑实例</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editInstanceForm" method="post">
<div class="modal-body">
<input type="hidden" name="id" id="editId">
<div class="row g-2 mb-3">
<div class="col-7">
<label class="form-label">监听地址</label>
<input name="listen_host" class="form-control" id="editHost">
</div>
<div class="col-5">
<label class="form-label">监听端口</label>
<input name="listen_port" type="number" class="form-control" id="editPort">
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label">超时 (秒)</label>
<input name="timeout" type="number" class="form-control" id="editTimeout">
</div>
<div class="col-6">
<label class="form-label">认证方式</label>
<select name="auth_method" class="form-select" id="editAuth">
<option value="none">无认证</option>
<option value="userpass">账号密码</option>
</select>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-6">
<label class="form-label">下行限速 (Mbps)</label>
<input name="bandwidth_down" type="number" class="form-control" id="editBwDown" step="0.1">
</div>
<div class="col-6">
<label class="form-label">上行限速 (Mbps)</label>
<input name="bandwidth_up" type="number" class="form-control" id="editBwUp" step="0.1">
</div>
</div>
<div class="mb-3">
<label class="form-label">最大并发连接数</label>
<input name="max_concurrent" type="number" class="form-control" id="editConcurrent">
</div>
<div class="mb-3">
<label class="form-label">备注</label>
<textarea name="notes" class="form-control" rows="2" id="editNotes"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary btn-sm">保存</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
document.getElementById('editInstanceModal').addEventListener('show.bs.modal', function(e) {
const btn = e.relatedTarget;
const form = document.getElementById('editInstanceForm');
form.action = '/instances/' + btn.dataset.id + '/edit';
document.getElementById('editHost').value = btn.dataset.host;
document.getElementById('editPort').value = btn.dataset.port;
document.getElementById('editTimeout').value = btn.dataset.timeout;
document.getElementById('editAuth').value = btn.dataset.auth;
document.getElementById('editBwDown').value = btn.dataset.bwDown;
document.getElementById('editBwUp').value = btn.dataset.bwUp;
document.getElementById('editConcurrent').value = btn.dataset.concurrent;
document.getElementById('editNotes').value = btn.dataset.notes || '';
});
</script>
{% endblock %}
+64 -26
View File
@@ -1,43 +1,81 @@
{% extends "base.html" %}
{% block title %}日志 · SOCKS Manager{% endblock %}
{% block title %}审计日志 · SOCKS Manager{% endblock %}
{% block content %}
<h4 class="mb-4"><i class="bi bi-journal-text"></i> 操作日志</h4>
<h5 class="mb-3"><i class="bi bi-journal-text"></i> 审计日志 <span class="text-muted small">({{ total }} 条)</span></h5>
<!-- 筛选 -->
<div class="card mb-3">
<div class="card-body">
<form method="get" class="row g-2">
<div class="col-md-3">
<label class="form-label">事件类型</label>
<select name="event" class="form-select form-select-sm">
<option value="">全部</option>
{% for evt in ['auth_success','auth_fail','connect_success','connect_reject','user_create','instance_start','instance_stop'] %}
<option value="{{ evt }}" {% if event_filter==evt %}selected{% endif %}>{{ evt }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">用户名</label>
<input name="user" class="form-control form-control-sm" value="{{ user_filter }}" placeholder="搜索用户名">
</div>
<div class="col-md-3">
<label class="form-label">来源 IP</label>
<input name="src_ip" class="form-control form-control-sm" value="{{ ip_filter }}" placeholder="搜索 IP">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="submit" class="btn btn-outline-primary btn-sm w-100">
<i class="bi bi-search"></i> 筛选</button>
</div>
</form>
</div>
</div>
<!-- 日志列表 -->
<div class="card">
<div class="card-body p-0">
<table class="table table-dark table-hover mb-0 table-sm">
<thead><tr><th>时间</th><th>级别</th><th>操作</th><th>目标</th><th>详情</th></tr></thead>
<table class="table table-dark table-sm">
<thead>
<tr><th>时间</th><th>事件</th><th>用户</th><th>来源 IP</th><th>详情</th></tr>
</thead>
<tbody>
{% for l in pages.items %}
{% for l in logs %}
<tr>
<td>{{ l.timestamp.strftime('%Y-%m-%d %H:%M:%S') }}</td>
<td><span class="badge bg-{{ 'success' if l.level=='INFO' else 'warning' if l.level=='WARN' else 'danger' }}">{{ l.level }}</span></td>
<td>{{ l.action }}</td>
<td>{{ l.target or '-' }}</td>
<td>{{ l.detail or '-' }}</td>
<td><small>{{ l.timestamp }}</small></td>
<td>
<span class="badge badge-{{
'success' if 'success' in l.event else
'danger' if 'fail' in l.event or 'reject' in l.event else
'warning' if 'start' in l.event or 'stop' in l.event else
'secondary'
}}">{{ l.event }}</span>
</td>
<td>{{ l.user or '-' }}</td>
<td><code>{{ l.src_ip or '-' }}</code></td>
<td><small class="text-muted">{{ l.detail or '-' }}</small></td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not logs %}
<div class="text-center py-5 text-muted">暂无日志记录</div>
{% endif %}
</div>
<div class="card-footer d-flex justify-content-between align-items-center" style="border-color:#2a2d3a">
<span class="text-muted small">共 {{ pages.total }} 条</span>
<nav>
<ul class="pagination pagination-sm mb-0">
{% if pages.has_prev %}
<li class="page-item"><a class="page-link" href="?page={{ pages.prev_num }}" style="color:#818cf8">上一页</a></li>
{% endif %}
{% for n in range(page_start, page_end + 1) %}
<li class="page-item {% if n==pages.page %}active{% endif %}">
<a class="page-link" href="?page={{ n }}" style="color:#818cf8;background:{{ 'transparent' if n!=pages.page else '#818cf8' }};border-color:#2a2d3a">{{ n }}</a></li>
{% endfor %}
{% if pages.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ pages.next_num }}" style="color:#818cf8">下一页</a></li>
{% endif %}
</ul>
</nav>
<!-- 分页 -->
{% if total > 50 %}
<div class="card-footer d-flex justify-content-between align-items-center">
<span class="text-muted small">共 {{ total }} 条</span>
<nav><ul class="pagination pagination-sm mb-0">
{% set qs = "event=" ~ event_filter ~ "&user=" ~ user_filter ~ "&src_ip=" ~ ip_filter %}
{% if page > 1 %}<li class="page-item"><a class="page-link" href="?page={{ page-1 }}&{{ qs }}">上一页</a></li>{% endif %}
<li class="page-item disabled"><span class="page-link">{{ page }}</span></li>
{% if total//50 + 1 > page %}<li class="page-item"><a class="page-link" href="?page={{ page+1 }}&{{ qs }}">下一页</a></li>{% endif %}
</ul></nav>
</div>
{% endif %}
</div>
{% endblock %}
-65
View File
@@ -1,65 +0,0 @@
{% extends "base.html" %}
{% block title %}代理列表 · SOCKS Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0"><i class="bi bi-globe"></i> 代理列表</h4>
<div class="d-flex gap-2">
<select class="form-select form-select-sm" id="groupFilter" style="width:180px"
onchange="location.href='/proxies?group='+this.value">
<option value="">全部分组</option>
{% for g in groups %}
<option value="{{ g.id }}" {% if active_group|default('') == g.id|string %}selected{% endif %}>
{{ g.name }}</option>
{% endfor %}
</select>
<a href="/proxies/add" class="btn btn-primary btn-sm"><i class="bi bi-plus-lg"></i> 添加代理</a>
</div>
</div>
<div class="card">
<div class="card-body p-0">
<table class="table table-dark table-hover align-middle mb-0">
<thead><tr><th>名称</th><th>主机</th><th>端口</th><th>认证</th><th>分组</th><th>状态</th><th>延迟</th><th>操作</th></tr></thead>
<tbody>
{% for p in proxies %}
<tr class="{% if not p.enabled %}table-secondary{% endif %}">
<td><a href="/proxies/{{ p.id }}/edit" class="text-decoration-none">{{ p.name }}</a></td>
<td><code>{{ p.host }}</code></td>
<td>{{ p.port }}</td>
<td>{{ '有认证' if p.username else '无' }}</td>
<td>{% if p.group %}<span style="color:{{ p.group.color }}">{{ p.group.name }}</span>{% else %}-{% endif %}</td>
<td><span class="status-dot dot-{{ p.status }}"></span>{{ p.status }}<br><small class="text-muted">{{ p.last_check.strftime('%Y-%m-%d %H:%M') if p.last_check else '未检测' }}</small></td>
<td>{{ "%.1f"|format(p.latency_ms) if p.latency_ms else '-' }}ms</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/proxies/{{ p.id }}/edit" class="btn btn-outline-secondary"><i class="bi bi-pencil"></i></a>
<a href="/proxies/toggle/{{ p.id }}" class="btn btn-outline-{{ 'secondary' if p.enabled else 'success' }}" onclick="return confirm('确认{{ '禁用' if p.enabled else '启用' }}?')">
<i class="bi bi-{{ 'eye-slash' if p.enabled else 'eye' }}"></i></a>
<a href="/proxies/{{ p.id }}" class="btn btn-outline-danger" onclick="return confirm('确认删除?')"><i class="bi bi-trash"></i></a>
<button class="btn btn-outline-info" onclick="checkOne({{ p.id }})"><i class="bi bi-arrow-clockwise"></i></button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not proxies %}
<div class="text-center py-5 text-muted">暂无代理</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
async function checkOne(pid) {
try {
const r = await apiFetch(`/api/proxies/${pid}/check`, {method:'POST'});
toast(`${r.name}: ${r.status}${r.latency_ms? ' ('+r.latency_ms+'ms)':''}`, r.status==='online'?'success':'danger');
location.reload();
} catch(e) { toast('检测失败', 'danger'); }
}
</script>
{% endblock %}
-60
View File
@@ -1,60 +0,0 @@
{% extends "base.html" %}
{% block title %}{{ '编辑' if action=='edit' else '添加' }}代理 · SOCKS Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h4 class="mb-0"><i class="bi bi-{{ 'pencil' if action=='edit' else 'plus-circle' }}"></i>
{{ '编辑代理' if action=='edit' else '添加代理' }}</h4>
<a href="/proxies" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-left"></i> 返回</a>
</div>
<div class="card" style="max-width:640px">
<div class="card-body">
<form method="post">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">名称</label>
<input name="name" class="form-control" required value="{{ p.name if p else '' }}">
</div>
<div class="col-md-6">
<label class="form-label">分组</label>
<select name="group_id" class="form-select">
<option value=""></option>
{% for g in groups %}
<option value="{{ g.id }}" {% if p and p.group and p.group.id==g.id %}selected{% endif %}>
{{ g.name }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-8">
<label class="form-label">主机地址</label>
<input name="host" class="form-control" required placeholder="1.2.3.4 或 socks.example.com"
value="{{ p.host if p else '' }}">
</div>
<div class="col-md-4">
<label class="form-label">端口</label>
<input name="port" class="form-control" type="number" min="1" max="65535" required
value="{{ p.port if p else '' }}">
</div>
<div class="col-md-6">
<label class="form-label">用户名(可选)</label>
<input name="username" class="form-control" value="{{ p.username if p and p.username else '' }}">
</div>
<div class="col-md-6">
<label class="form-label">密码(可选)</label>
<input name="password" class="form-control" value="{{ p.password if p and p.password else '' }}">
</div>
<div class="col-12">
<label class="form-label">备注</label>
<textarea name="notes" class="form-control" rows="2">{{ p.notes if p else '' }}</textarea>
</div>
</div>
<div class="mt-4 d-flex gap-2">
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> 保存</button>
<a href="/proxies" class="btn btn-outline-secondary">取消</a>
</div>
</form>
</div>
</div>
{% endblock %}
+100
View File
@@ -0,0 +1,100 @@
{% extends "base.html" %}
{% block title %}流量统计 · SOCKS Manager{% endblock %}
{% block content %}
<h5 class="mb-3"><i class="bi bi-graph-up"></i> 流量统计</h5>
<!-- 流量趋势图表 -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-bar-chart"></i> 近 30 天流量趋势</span>
</div>
<div class="card-body">
<canvas id="trafficChart" height="100"></canvas>
</div>
</div>
<!-- 用户流量排名 -->
<div class="card mb-3">
<div class="card-header"><span><i class="bi bi-people"></i> 用户流量排名</span></div>
<div class="card-body p-0">
<table class="table table-dark table-sm">
<thead><tr><th>用户名</th><th>入站</th><th>出站</th><th>总计</th><th>流量上限</th><th>用量</th></tr></thead>
<tbody>
{% for u in users[:10] %}
<tr>
<td>{{ u.username }}</td>
<td>{{ u.bytes_in_mb }} MB</td>
<td>{{ u.bytes_out_mb }} MB</td>
<td><strong>{{ u.total_mb }} MB</strong></td>
<td>{% if u.limit_mb %}{{ u.limit_mb }} MB{% else %}不限{% endif %}</td>
<td>
{% if u.usage_pct is not none %}
<div class="progress" style="height:18px">
<div class="progress-bar bg-{{ 'danger' if u.usage_pct>90 else 'warning' if u.usage_pct>70 else 'info' }}"
style="width:{{ u.usage_pct }}%;min-width:2rem">{{ "%.0f"|format(u.usage_pct) }}%</div>
</div>
{% else %}<span class="text-muted">不限</span>{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not users %}
<div class="text-center py-4 text-muted">暂无数据</div>
{% endif %}
</div>
</div>
<!-- 实例流量 -->
<div class="card">
<div class="card-header"><span><i class="bi bi-server"></i> 各实例流量</span></div>
<div class="card-body p-0">
<table class="table table-dark table-sm">
<thead><tr><th>实例</th><th>地址</th><th>入站</th><th>出站</th><th>连接数</th></tr></thead>
<tbody>
{% for inst in instances %}
<tr>
<td>{{ inst.name }}</td>
<td><code>{{ inst.host }}:{{ inst.port }}</code></td>
<td>{{ inst.bytes_in_mb }} MB</td>
<td>{{ inst.bytes_out_mb }} MB</td>
<td>{{ inst.total_connections }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not instances %}
<div class="text-center py-4 text-muted">暂无数据</div>
{% endif %}
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
const ctx = document.getElementById('trafficChart');
const data = {{ trend | tojson }};
new Chart(ctx, {
type: 'bar',
data: {
labels: data.map(d => d.date),
datasets: [
{ label: '入站 (MB)', data: data.map(d => d.bytes_in_mb),
backgroundColor: 'rgba(129,140,248,.7)' },
{ label: '出站 (MB)', data: data.map(d => d.bytes_out_mb),
backgroundColor: 'rgba(16,185,129,.7)' },
]
},
options: {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: '#94a3b8' } } },
scales: {
x: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280', maxTicksLimit: 15 } },
y: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' } },
},
}
});
</script>
{% endblock %}
+92
View File
@@ -0,0 +1,92 @@
{% extends "base.html" %}
{% block title %}系统管理 · SOCKS Manager{% endblock %}
{% block content %}
<h5 class="mb-3"><i class="bi bi-shield-lock"></i> 系统管理</h5>
<!-- 备份管理 -->
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span><i class="bi bi-download"></i> 数据库备份</span>
<form method="post" action="/system/backup" style="display:inline">
<button type="submit" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg"></i> 立即备份</button>
</form>
</div>
<div class="card-body p-0">
<table class="table table-dark table-sm">
<thead><tr><th>文件名</th><th>大小</th><th>创建时间</th><th>备注</th><th>操作</th></tr></thead>
<tbody>
{% for b in backups %}
<tr>
<td><code>{{ b.filename }}</code></td>
<td>{{ b.size_mb }} MB</td>
<td>{{ b.created_at[:16] }}</td>
<td>{{ b.note or '-' }}</td>
<td>
{% if b.exists %}
<form method="post" action="/system/restore/{{ b.id }}" style="display:inline"
onsubmit="return confirm('确认从备份 {{ b.filename }} 恢复?当前数据将被覆盖。')">
<button class="btn btn-outline-warning btn-sm">
<i class="bi bi-arrow-counterclockwise"></i> 恢复</button>
</form>
{% else %}
<span class="text-muted">文件已删除</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not backups %}
<div class="text-center py-4 text-muted">暂无备份</div>
{% endif %}
</div>
</div>
<!-- 系统信息 -->
<div class="card mb-3">
<div class="card-header"><span><i class="bi bi-info-circle"></i> 系统信息</span></div>
<div class="card-body">
<table class="table table-dark table-sm">
<tbody>
<tr><td class="text-muted">Python 版本</td><td>{{ sys_version }}</td></tr>
<tr><td class="text-muted">操作系统</td><td>{{ sys_platform }}</td></tr>
<tr><td class="text-muted">主机名</td><td>{{ sys_hostname }}</td></tr>
<tr><td class="text-muted">数据库</td><td>SQLite (socks_manager.db)</td></tr>
<tr><td class="text-muted">启动时间</td><td>{{ uptime }}</td></tr>
</tbody>
</table>
</div>
</div>
<!-- API 密钥 -->
<div class="card">
<div class="card-header"><span><i class="bi bi-key"></i> API 密钥</span></div>
<div class="card-body">
<p class="text-muted small">RESTful API 端点: <code>/api/</code>,支持全部管理功能。
可通过 <code>Authorization: Bearer &lt;token&gt;</code> 或使用 Cookie 认证。</p>
<div class="row g-3">
<div class="col-md-6">
<div class="card" style="background:var(--bg-tertiary)">
<div class="card-body">
<h6 class="text-muted small">创建实例</h6>
<code>POST /api/instances<br>
{"name":"main","listen_port":1080,"auth_method":"userpass"}</code>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card" style="background:var(--bg-tertiary)">
<div class="card-body">
<h6 class="text-muted small">创建用户</h6>
<code>POST /api/users<br>
{"username":"test","password":"pass","bandwidth_down":10}</code>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+260
View File
@@ -0,0 +1,260 @@
{% extends "base.html" %}
{% block title %}用户管理 · SOCKS Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h5 class="mb-0"><i class="bi bi-people"></i> 用户管理</h5>
<span class="text-muted small">共 {{ total }} 个用户</span>
</div>
<div class="d-flex gap-2">
<form method="get" class="d-flex">
<input name="search" class="form-control form-control-sm" placeholder="搜索用户名"
value="{{ search }}" style="width:180px">
<button type="submit" class="btn btn-outline-secondary btn-sm"><i class="bi bi-search"></i></button>
</form>
<button class="btn btn-primary btn-sm" data-bs-toggle="modal" data-bs-target="#addUserModal">
<i class="bi bi-plus-lg"></i> 新建用户</button>
</div>
</div>
<!-- 用户列表 -->
<div class="card">
<div class="card-body p-0">
<table class="table table-dark align-middle">
<thead>
<tr><th>用户名</th><th>状态</th><th>流量</th><th>限速</th><th>并发</th>
<th>白名单/黑名单</th><th>过期</th><th>创建时间</th><th>操作</th></tr>
</thead>
<tbody>
{% for u in users %}
<tr class="{% if not u.active %}table-secondary{% endif %}">
<td>
<strong>{{ u.username }}</strong>
{% if u.banned %}<span class="badge badge-danger ms-1">封禁</span>{% endif %}
{% if not u.enabled %}<span class="badge badge-secondary ms-1">禁用</span>{% endif %}
</td>
<td>
<span class="badge badge-{{ 'success' if u.active else 'danger' }}">
{{ '正常' if u.active else '不可用' }}</span>
</td>
<td>
<span class="text-muted small">↑{{ "%.0f"|format(u.bytes_in/1024/1024) }}M ↓{{ "%.0f"|format(u.bytes_out/1024/1024) }}M</span>
{% if u.total_traffic_mb %}<br><small class="text-muted">{{ "%.0f"|format(u.bytes_total_mb) }} / {{ u.total_traffic_mb }} MB</small>{% endif %}
</td>
<td>
<span class="text-muted small">↓{{ u.bandwidth_down or '不限' }} ↑{{ u.bandwidth_up or '不限' }} Mbps</span>
</td>
<td>{{ u.max_concurrent }}</td>
<td>
{% if u.ip_whitelist %}<span class="badge badge-info" title="白名单">白 {{ u.ip_whitelist|length }}</span>{% endif %}
{% if u.ip_blacklist %}<span class="badge badge-warning" title="黑名单">黑 {{ u.ip_blacklist|length }}</span>{% endif %}
{% if not u.ip_whitelist and not u.ip_blacklist %}<span class="text-muted">-</span>{% endif %}
</td>
<td>{{ u.expire_at or '不过期' }}</td>
<td><small class="text-muted">{{ u.created_at[:10] }}</small></td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#editUserModal"
data-id="{{ u.id }}"
data-username="{{ u.username }}"
data-bw-down="{{ u.bandwidth_down }}"
data-bw-up="{{ u.bandwidth_up }}"
data-concurrent="{{ u.max_concurrent }}"
data-total="{{ u.total_traffic_mb }}"
data-monthly="{{ u.monthly_traffic_mb }}"
data-wl="{{ u.ip_whitelist }}"
data-bl="{{ u.ip_blacklist }}"
data-expire="{{ u.expire_at[:10] if u.expire_at else '' }}">
<i class="bi bi-pencil"></i>
</button>
<form method="post" action="/users/{{ u.id }}/toggle" style="display:inline">
<button class="btn btn-outline-{{ 'warning' if u.enabled else 'success' }}">
<i class="bi bi-{{ 'eye-slash' if u.enabled else 'eye' }}"></i>
</button>
</form>
<form method="post" action="/users/{{ u.id }}/ban" style="display:inline">
<button class="btn btn-outline-{{ 'warning' if not u.banned else 'secondary' }}">
<i class="bi bi-{{ 'ban' if not u.banned else 'check-circle' }}"></i>
</button>
</form>
<form method="post" action="/users/{{ u.id }}/delete" style="display:inline"
onsubmit="return confirmDelete('确认删除用户 {{ u.username }}')">
<button class="btn btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not users %}
<div class="text-center py-5 text-muted">暂无用户</div>
{% endif %}
</div>
<!-- 分页 -->
{% if pages > 1 %}
<div class="card-footer d-flex justify-content-between align-items-center">
<span class="text-muted small">第 {{ page }} / {{ pages }} 页</span>
<nav><ul class="pagination pagination-sm mb-0">
{% if page > 1 %}<li class="page-item"><a class="page-link" href="?page=1&search={{ search|urlencode }}">首页</a></li>{% endif %}
{% if page > 1 %}<li class="page-item"><a class="page-link" href="?page={{ page-1 }}&search={{ search|urlencode }}">上页</a></li>{% endif %}
{% for p in range(max(1,page-2), min(pages,page+2)+1) %}
<li class="page-item {% if p==page %}active{% endif %}">
<a class="page-link" href="?page={{ p }}&search={{ search|urlencode }}">{{ p }}</a></li>
{% endfor %}
{% if page < pages %}<li class="page-item"><a class="page-link" href="?page={{ page+1 }}&search={{ search|urlencode }}">下页</a></li>{% endif %}
{% if page < pages %}<li class="page-item"><a class="page-link" href="?page={{ pages }}&search={{ search|urlencode }}">末页</a></li>{% endif %}
</ul></nav>
</div>
{% endif %}
</div>
<!-- 添加用户模态框 -->
<div class="modal fade" id="addUserModal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title">新建用户</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form method="post" action="/users/add">
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">用户名</label>
<input name="username" class="form-control" required placeholder="登录时使用的用户名">
</div>
<div class="col-md-6">
<label class="form-label">密码</label>
<input name="password" class="form-control" placeholder="实例认证方式为账号密码时必填">
</div>
<div class="col-md-4">
<label class="form-label">下行限速 (Mbps)</label>
<input name="bandwidth_down" type="number" class="form-control" value="0" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">上行限速 (Mbps)</label>
<input name="bandwidth_up" type="number" class="form-control" value="0" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">最大并发连接</label>
<input name="max_concurrent" type="number" class="form-control" value="10">
</div>
<div class="col-md-4">
<label class="form-label">总流量上限 (MB)</label>
<input name="total_traffic_mb" type="number" class="form-control" value="0" placeholder="0=不限">
</div>
<div class="col-md-4">
<label class="form-label">月流量上限 (MB)</label>
<input name="monthly_traffic_mb" type="number" class="form-control" value="0" placeholder="0=不限">
</div>
<div class="col-md-4">
<label class="form-label">过期时间</label>
<input name="expire_at" type="datetime-local" class="form-control">
</div>
<div class="col-md-12">
<label class="form-label">IP 白名单 (逗号分隔, 留空=不限)</label>
<input name="ip_whitelist" class="form-control" placeholder="例: 192.168.1.0/24, 10.0.0.1">
</div>
<div class="col-md-12">
<label class="form-label">IP 黑名单 (逗号分隔)</label>
<input name="ip_blacklist" class="form-control" placeholder="例: 1.2.3.4, 5.6.7.8">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary btn-sm">创建</button>
</div>
</form>
</div>
</div>
</div>
<!-- 编辑用户模态框 -->
<div class="modal fade" id="editUserModal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title" id="editUserTitle">编辑用户</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editUserForm" method="post">
<div class="modal-body">
<input type="hidden" name="id" id="editUid">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">用户名</label>
<input class="form-control" id="editUsername" disabled>
</div>
<div class="col-md-6">
<label class="form-label">密码 (留空=不修改)</label>
<input name="password" class="form-control" id="editPassword" placeholder="修改密码">
</div>
<div class="col-md-4">
<label class="form-label">下行限速 (Mbps)</label>
<input name="bandwidth_down" type="number" class="form-control" id="editBwDown" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">上行限速 (Mbps)</label>
<input name="bandwidth_up" type="number" class="form-control" id="editBwUp" step="0.1">
</div>
<div class="col-md-4">
<label class="form-label">最大并发连接</label>
<input name="max_concurrent" type="number" class="form-control" id="editConcurrent">
</div>
<div class="col-md-4">
<label class="form-label">总流量上限 (MB)</label>
<input name="total_traffic_mb" type="number" class="form-control" id="editTotal" placeholder="0=不限">
</div>
<div class="col-md-4">
<label class="form-label">月流量上限 (MB)</label>
<input name="monthly_traffic_mb" type="number" class="form-control" id="editMonthly" placeholder="0=不限">
</div>
<div class="col-md-4">
<label class="form-label">过期时间</label>
<input name="expire_at" type="datetime-local" class="form-control" id="editExpire">
</div>
<div class="col-md-12">
<label class="form-label">IP 白名单</label>
<input name="ip_whitelist" class="form-control" id="editWL">
</div>
<div class="col-md-12">
<label class="form-label">IP 黑名单</label>
<input name="ip_blacklist" class="form-control" id="editBL">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary btn-sm">保存</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_script %}
<script>
document.getElementById('editUserModal').addEventListener('show.bs.modal', function(e) {
const btn = e.relatedTarget;
const form = document.getElementById('editUserForm');
form.action = '/users/' + btn.dataset.id + '/edit';
document.getElementById('editUid').value = btn.dataset.id;
document.getElementById('editUsername').value = btn.dataset.username;
document.getElementById('editBwDown').value = btn.dataset.bwDown;
document.getElementById('editBwUp').value = btn.dataset.bwUp;
document.getElementById('editConcurrent').value = btn.dataset.concurrent;
document.getElementById('editTotal').value = btn.dataset.total;
document.getElementById('editMonthly').value = btn.dataset.monthly;
document.getElementById('editWL').value = btn.dataset.wl || '';
document.getElementById('editBL').value = btn.dataset.bl || '';
document.getElementById('editExpire').value = btn.dataset.expire || '';
document.getElementById('editUserTitle').textContent = '编辑用户: ' + btn.dataset.username;
});
</script>
{% endblock %}