fix: 修复暗色主题UI对比度、端口占用死循环及多项Web表单bug
后端修复: - engine/instances.py: Socks5Server.start() 端口占用时死循环, 增加超时和异常透传 - engine/instances.py: import time 移至顶层 - engine/instances.py: sync_instances/start_instance 容忍单实例启动失败 - run.py: 启动时 sync_instances 失败不退出进程 - web/routes.py: instances start/restart/edit 路由捕获 OSError, flash 友好提示 - api/v1.py: instances start/restart/update API 返回 JSON 错误而非 500 崩溃 - web/routes.py: toggle_user/ban_user 支持 hidden 字段语义, 缺字段时切换状态 - web/routes.py: inject_nav 增加独立 nav.dashboard 和 page_name, 修复双 active bug - auth.py: login 路由添加 logging import, 修 NameError 前端重构: - templates/base.html: 全局对比度提升(--text #f1f5f9, --accent #a5b4fc), 标题色显式声明, alert/modal/progress/table 完整暗色样式 - templates/base.html: main-content padding-top 修复 navbar 遮挡内容 - templates/base.html: 侧边栏 active 高亮 (白字 + elevated bg + 左边框) - templates/base.html: 顶栏动态显示当前页面名 - templates/*.html: 统一 page-header 类, 所有图标按钮添加 title tooltip - templates/users.html: toggle/ban 表单添加 hidden enabled/ban 字段 - templates/connections.html: fmtBytes 改为 Jinja macro, 时间格式化 - templates/dashboard.html: 时间格式化为 YYYY-MM-DD HH:MM:SS, chart canvas 高度调整 - templates/logs.html: 时间格式化为可读格式 - templates/stats.html: 提升 chart 颜色对比度 - templates/system.html: 使用 time.localtime() 显示本地时间
This commit is contained in:
@@ -106,9 +106,12 @@ def update_instance(iid):
|
||||
|
||||
# 重启以应用配置
|
||||
if inst.running:
|
||||
try:
|
||||
current_app.instance_manager.restart_instance(iid)
|
||||
except OSError as e:
|
||||
return {"error": f"重启失败(端口可能被占用): {e}"}, 500
|
||||
|
||||
return jsonify({"ok": True})
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@bp.route("/instances/<int:iid>", methods=["DELETE"])
|
||||
@@ -126,25 +129,33 @@ def delete_instance(iid):
|
||||
@bp.route("/instances/<int:iid>/start", methods=["POST"])
|
||||
@login_required
|
||||
def start_instance(iid):
|
||||
if current_app.instance_manager.start_instance(iid):
|
||||
return jsonify({"ok": True})
|
||||
try:
|
||||
ok = current_app.instance_manager.start_instance(iid)
|
||||
if not ok:
|
||||
return {"error": "启动失败"}, 500
|
||||
return {"ok": True}
|
||||
except OSError as e:
|
||||
return {"error": f"启动失败: {e}"}, 500
|
||||
|
||||
|
||||
@bp.route("/instances/<int:iid>/stop", methods=["POST"])
|
||||
@login_required
|
||||
def stop_instance(iid):
|
||||
if current_app.instance_manager.stop_instance(iid):
|
||||
return jsonify({"ok": True})
|
||||
return {"ok": True}
|
||||
return {"error": "停止失败"}, 500
|
||||
|
||||
|
||||
@bp.route("/instances/<int:iid>/restart", methods=["POST"])
|
||||
@login_required
|
||||
def restart_instance(iid):
|
||||
if current_app.instance_manager.restart_instance(iid):
|
||||
return jsonify({"ok": True})
|
||||
try:
|
||||
ok = current_app.instance_manager.restart_instance(iid)
|
||||
if not ok:
|
||||
return {"error": "重启失败"}, 500
|
||||
return {"ok": True}
|
||||
except OSError as e:
|
||||
return {"error": f"重启失败: {e}"}, 500
|
||||
|
||||
|
||||
@bp.route("/instances/sync", methods=["POST"])
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""登录认证。"""
|
||||
import os
|
||||
import hashlib
|
||||
import logging
|
||||
import secrets
|
||||
from functools import wraps
|
||||
from flask import Blueprint, request, session, redirect, url_for, current_app
|
||||
|
||||
log = logging.getLogger("socks.auth")
|
||||
bp = Blueprint("auth", __name__)
|
||||
|
||||
|
||||
|
||||
+36
-2
@@ -2,6 +2,7 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from engine.server import ConnectionHandler
|
||||
@@ -51,20 +52,41 @@ class Socks5Server:
|
||||
|
||||
self._active_connections = 0
|
||||
self.running = True
|
||||
self.server = None
|
||||
self._start_error = None # 用于在主线程透传子线程异常
|
||||
|
||||
def run_loop():
|
||||
asyncio.set_event_loop(self.loop)
|
||||
try:
|
||||
self.server = self.loop.run_until_complete(
|
||||
self._create_server()
|
||||
)
|
||||
self.loop.run_forever()
|
||||
except Exception as e:
|
||||
# 端口占用等异常:保存异常并停止循环,避免主线程死等
|
||||
self._start_error = e
|
||||
log.error("[%s] SOCKS5 服务器启动失败: %s",
|
||||
self.config.name, e)
|
||||
try:
|
||||
self.loop.stop()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
self.thread = threading.Thread(target=run_loop, daemon=True,
|
||||
name=f"socks5-{self.config.name}")
|
||||
self.thread.start()
|
||||
# 等 server 创建完成
|
||||
while self.server is None and self.running:
|
||||
# 等 server 创建完成 (或失败) — 设了上限避免永久死等
|
||||
deadline = time.time() + 5.0
|
||||
while self.server is None and self.running and self._start_error is None:
|
||||
time.sleep(0.05)
|
||||
if time.time() > deadline:
|
||||
log.error("[%s] SOCKS5 服务器启动超时", self.config.name)
|
||||
self.running = False
|
||||
break
|
||||
# 如果子线程失败, 把异常抛回调用方
|
||||
if self._start_error is not None:
|
||||
self.running = False
|
||||
raise self._start_error
|
||||
if self.server:
|
||||
log.info("[%s] SOCKS5 服务器已启动: %s:%d",
|
||||
self.config.name, self.config.listen_host, self.config.listen_port)
|
||||
@@ -157,7 +179,11 @@ class InstanceManager:
|
||||
for name in current_names - running_names:
|
||||
cfg = self._config_cache[name]
|
||||
srv = Socks5Server(cfg, self.user_service, self.flask_app)
|
||||
try:
|
||||
srv.start()
|
||||
except OSError as e:
|
||||
log.error("[%s] 跳过启动(端口冲突?): %s", name, e)
|
||||
continue
|
||||
with self._lock:
|
||||
self._instances[name] = srv
|
||||
|
||||
@@ -198,7 +224,15 @@ class InstanceManager:
|
||||
)
|
||||
self._config_cache[inst.name] = cfg
|
||||
srv = Socks5Server(cfg, self.user_service, self.flask_app)
|
||||
try:
|
||||
srv.start()
|
||||
except OSError as e:
|
||||
# 端口占用等启动失败: 回滚状态, 清理缓存, 返回 False
|
||||
log.error("[%s] 启动实例失败: %s", inst.name, e)
|
||||
self._config_cache.pop(inst.name, None)
|
||||
inst.running = False
|
||||
db.session.commit()
|
||||
raise
|
||||
with self._lock:
|
||||
self._instances[inst.name] = srv
|
||||
inst.running = True
|
||||
|
||||
@@ -12,7 +12,10 @@ if __name__ == "__main__":
|
||||
debug = os.environ.get("SM_DEBUG", "false").lower() == "true"
|
||||
|
||||
# 启动时同步实例
|
||||
try:
|
||||
app.instance_manager.sync_instances()
|
||||
except OSError as e:
|
||||
print(f"⚠️ 部分实例启动失败(端口冲突?): {e}", flush=True)
|
||||
|
||||
# 记录流量快照
|
||||
from services.stats_service import record_traffic_snapshot
|
||||
|
||||
+75
-43
@@ -11,21 +11,31 @@
|
||||
/* ── 全局 ─────────────────────────────────────────────────────── */
|
||||
:root {
|
||||
--bg-primary:#0b0e14; --bg-secondary:#161922; --bg-tertiary:#1e2235;
|
||||
--border:#374151; --text:#e2e8f0; --text-muted:#9ca3af;
|
||||
--accent:#818cf8; --accent-hover:#6366f1;
|
||||
--green:#10b981; --red:#ef4444; --yellow:#f59e0b; --gray:#6b7280;
|
||||
--bg-elevated:#1f2433;
|
||||
--border:#3a4254; --border-strong:#4b5563;
|
||||
--text:#f1f5f9; /* 提高主文字亮度, 之前 #e2e8f0 在小字号下偏暗 */
|
||||
--text-muted:#a3acbf; /* 之前 #9ca3af, 稍微提亮 */
|
||||
--accent:#a5b4fc; /* 提亮主色, 之前 #818cf8 对比度太低 */
|
||||
--accent-hover:#818cf8;
|
||||
--accent-dim:rgba(165,180,252,.15);
|
||||
--green:#34d399; --red:#f87171; --yellow:#fbbf24; --gray:#94a3b8;
|
||||
--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:16px; line-height:1.7; }
|
||||
font-size:15px; line-height:1.6; }
|
||||
a { color:var(--accent); text-decoration:none; }
|
||||
a:hover { color:var(--accent-hover); }
|
||||
|
||||
/* 显式覆盖 Bootstrap 默认的标题色 (默认是 inherit, 但某些情况下浏览器渲染为黑) */
|
||||
h1, h2, h3, h4, h5, h6 { color:var(--text); margin-top:0; }
|
||||
.h1, .h2, .h3, .h4, .h5, .h6 { color:var(--text); }
|
||||
.text-secondary, .text-body { color:var(--text) !important; }
|
||||
|
||||
/* ── 导航 ─────────────────────────────────────────────────────── */
|
||||
.navbar { background:var(--bg-secondary)!important; border-bottom:1px solid var(--border)!important;
|
||||
padding:.6rem 1rem; }
|
||||
padding:.6rem 1rem; height:56px; }
|
||||
.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; }
|
||||
@@ -36,70 +46,78 @@ a:hover { color:var(--accent-hover); }
|
||||
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.active { background:var(--bg-elevated); color:#fff!important;
|
||||
border-left:3px solid var(--accent); font-weight:600; }
|
||||
.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); }
|
||||
.main-content { margin-left:220px; padding:1.2rem 1.5rem; min-height:calc(100vh - 56px); padding-top:calc(56px + 1.2rem); }
|
||||
.page-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; }
|
||||
.page-header h5 { margin:0; color:var(--text); font-size:1.1rem; font-weight:600; }
|
||||
|
||||
/* ── 卡片 ─────────────────────────────────────────────────────── */
|
||||
.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-header { background:transparent; border-bottom:1px solid var(--border); padding:.8rem 1rem;
|
||||
color:var(--text); font-weight:600; }
|
||||
.card-header i { color:var(--accent); margin-right:.4rem; }
|
||||
.card-body { padding:1rem; color:var(--text); }
|
||||
.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;
|
||||
.table thead th { background:var(--bg-tertiary); color:#cbd5e1; 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; }
|
||||
padding:.7rem .8rem; }
|
||||
.table tbody td { padding:.65rem .8rem; border-bottom:1px solid var(--border); font-size:14px;
|
||||
vertical-align:middle; color:var(--text); }
|
||||
.table tbody tr:hover { background:var(--bg-tertiary); }
|
||||
.table-dark { background:var(--card-bg); }
|
||||
.table-dark th, .table-dark td { color:var(--text); border-color:var(--border); }
|
||||
.table code { background:var(--bg-tertiary); color:var(--accent); }
|
||||
|
||||
/* ── 表单 ─────────────────────────────────────────────────────── */
|
||||
.form-control, .form-select { background:#1a1d2b; border:1px solid #4b5563;
|
||||
.form-control, .form-select { background:#1a1d2b; border:1px solid var(--border-strong);
|
||||
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);
|
||||
.form-control:focus, .form-select:focus { border-color:var(--accent); box-shadow:0 0 0 3px rgba(165,180,252,.15);
|
||||
background:#1a1d2b; color:var(--text); }
|
||||
.form-control::placeholder { color:#5a6178; }
|
||||
.form-control::placeholder { color:#6b7592; }
|
||||
.form-text { color:var(--text-muted); font-size:12px; }
|
||||
.form-check-input { background-color:var(--input-bg); border-color:var(--border); }
|
||||
.form-label { color:#d1d9e6 !important; font-size:13px; margin-bottom:.3rem; font-weight:500; }
|
||||
.form-check-input { background-color:var(--input-bg); border-color:var(--border-strong); }
|
||||
.form-check-input:checked { background-color:var(--accent); border-color:var(--accent); }
|
||||
|
||||
/* ── 按钮 ─────────────────────────────────────────────────────── */
|
||||
.btn-primary { background:var(--accent); border-color:var(--accent); color:#fff; }
|
||||
.btn-primary:hover { background:var(--accent-hover); border-color:var(--accent-hover); color:#fff; }
|
||||
.btn-primary { background:var(--accent); border-color:var(--accent); color:#0b0e14; font-weight:600; }
|
||||
.btn-primary:hover { background:var(--accent-hover); border-color:var(--accent-hover); color:#0b0e14; }
|
||||
.btn-primary:active { background:var(--accent-hover); }
|
||||
.btn-primary:focus { box-shadow:0 0 0 3px rgba(129,140,248,.3); }
|
||||
.btn-primary:focus { box-shadow:0 0 0 3px rgba(165,180,252,.3); }
|
||||
.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-primary:hover { background:var(--accent); border-color:var(--accent); color:#0b0e14; }
|
||||
.btn-outline-secondary { color:var(--text-muted); border-color:var(--border-strong); }
|
||||
.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-success:hover { background:var(--green); border-color:var(--green); color:#0b0e14; }
|
||||
.btn-outline-warning { color:var(--yellow); border-color:var(--yellow); }
|
||||
.btn-outline-warning:hover { background:var(--yellow); border-color:var(--yellow); color:#000; }
|
||||
.btn-outline-warning:hover { background:var(--yellow); border-color:var(--yellow); color:#0b0e14; }
|
||||
.btn-sm { font-size:13px; padding:.3rem .6rem; }
|
||||
.btn-group-sm > .btn { padding:.25rem .55rem; }
|
||||
|
||||
/* ── 徽章 ─────────────────────────────────────────────────────── */
|
||||
.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); }
|
||||
.badge { font-size:11px; padding:.3em .6em; border-radius:.3rem; font-weight:600; }
|
||||
.badge-success { background:rgba(52,211,153,.18); color:#34d399; }
|
||||
.badge-danger { background:rgba(248,113,113,.18); color:#f87171; }
|
||||
.badge-warning { background:rgba(251,191,36,.18); color:#fbbf24; }
|
||||
.badge-secondary { background:rgba(148,163,184,.18); color:#cbd5e1; }
|
||||
.badge-info { background:var(--accent-dim); 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-online { background:var(--green); box-shadow:0 0 6px rgba(52,211,153,.6); }
|
||||
.dot-offline { background:var(--red); }
|
||||
.dot-warning { background:var(--yellow); }
|
||||
.dot-gray { background:var(--gray); }
|
||||
@@ -107,7 +125,7 @@ a:hover { color:var(--accent-hover); }
|
||||
/* ── 统计卡片 ─────────────────────────────────────────────────── */
|
||||
.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-value { font-size:1.8rem; font-weight:700; margin:.2rem 0; color:var(--text); }
|
||||
.stat-card .stat-sub { color:var(--text-muted); font-size:12px; }
|
||||
|
||||
/* ── 表格代码 ─────────────────────────────────────────────────── */
|
||||
@@ -117,35 +135,49 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;
|
||||
/* ── 分页 ─────────────────────────────────────────────────────── */
|
||||
.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-link:hover { background:var(--bg-tertiary); color:var(--accent-hover); }
|
||||
.page-item.active .page-link { background:var(--accent); border-color:var(--accent); color:#0b0e14; }
|
||||
.page-item.disabled .page-link { color:var(--gray); }
|
||||
|
||||
/* ── 进度条 ──────────────────────────────────────────────────── */
|
||||
.progress { background:var(--bg-tertiary); }
|
||||
.progress-bar { font-weight:600; }
|
||||
|
||||
/* ── 模态框 ───────────────────────────────────────────────────── */
|
||||
.modal-content { background:var(--card-bg); border:1px solid var(--border); color:var(--text); }
|
||||
.modal-content { background:var(--card-bg); border:1px solid var(--border-strong); color:var(--text); }
|
||||
.modal-header { border-bottom:1px solid var(--border); }
|
||||
.modal-header .modal-title { color:var(--text); font-weight:600; }
|
||||
.modal-footer { border-top:1px solid var(--border); }
|
||||
.btn-close { filter:invert(1); }
|
||||
.btn-close { filter:invert(1) grayscale(100%) brightness(200%); }
|
||||
|
||||
/* ── Alert ─────────────────────────────────────────────────────── */
|
||||
.alert { border:1px solid var(--border); color:var(--text); background:var(--card-bg); }
|
||||
.alert-success { background:rgba(52,211,153,.12); border-color:rgba(52,211,153,.4); color:#34d399; }
|
||||
.alert-danger { background:rgba(248,113,113,.12); border-color:rgba(248,113,113,.4); color:#f87171; }
|
||||
.alert-warning { background:rgba(251,191,36,.12); border-color:rgba(251,191,36,.4); color:#fbbf24; }
|
||||
.alert-info { background:var(--accent-dim); border-color:rgba(165,180,252,.4); color:var(--accent); }
|
||||
|
||||
/* ── 消息 ─────────────────────────────────────────────────────── */
|
||||
.toast { background:var(--card-bg); border:1px solid var(--border); color:var(--text); }
|
||||
.toast-container { position:fixed; top:1rem; right:1rem; z-index:1060; }
|
||||
|
||||
/* ── Chart.js canvas 容器 ─────────────────────────────────────── */
|
||||
canvas { max-width:100%; }
|
||||
|
||||
/* ── 响应式 ───────────────────────────────────────────────────── */
|
||||
@media(max-width:768px){
|
||||
.sidebar { display:none; }
|
||||
.main-content { margin-left:0; padding:1rem; }
|
||||
.main-content { margin-left:0; padding:1rem; padding-top:calc(56px + 1rem); }
|
||||
}
|
||||
|
||||
/* ── 滚动条 ───────────────────────────────────────────────────── */
|
||||
::-webkit-scrollbar { width:6px; height:6px; }
|
||||
::-webkit-scrollbar { width:8px; height:8px; }
|
||||
::-webkit-scrollbar-track { background:var(--bg-primary); }
|
||||
::-webkit-scrollbar-thumb { background:var(--border); border-radius:3px; }
|
||||
::-webkit-scrollbar-thumb { background:var(--border-strong); border-radius:4px; }
|
||||
::-webkit-scrollbar-thumb:hover { background:var(--gray); }
|
||||
|
||||
/* ── 覆盖 Bootstrap text-muted 提高深色背景对比度 ─────────────── */
|
||||
.text-muted { color:var(--text-muted) !important; }
|
||||
.form-label { color:#cbd5e0 !important; font-size:13px; margin-bottom:.3rem; font-weight:500; }
|
||||
</style>
|
||||
{% block extra_style %}{% endblock %}
|
||||
</head>
|
||||
@@ -155,7 +187,7 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;
|
||||
<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 %}
|
||||
{% if page_name %}<span class="text-muted small">{{ page_name }}</span>{% endif %}
|
||||
<a href="/logout" class="btn btn-outline-secondary btn-sm"><i class="bi bi-box-arrow-right"></i> 退出</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -165,7 +197,7 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;
|
||||
<div class="sidebar">
|
||||
<div class="nav flex-column">
|
||||
<div class="sidebar-section">概览</div>
|
||||
<a class="nav-link {% if nav.instances %}active{% endif %}" href="/dashboard">
|
||||
<a class="nav-link {% if nav.dashboard %}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>
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}活跃连接 · SOCKS Manager{% endblock %}
|
||||
|
||||
{% macro fmtBytes(b) -%}
|
||||
{%- if not b %}0 B
|
||||
{%- else -%}
|
||||
{%- set u = ['B','KB','MB','GB','TB'] -%}
|
||||
{%- set ns = namespace(v=b, i=0) -%}
|
||||
{%- for _ in range(4) -%}
|
||||
{%- if ns.v >= 1024 and ns.i < 4 -%}
|
||||
{%- set ns.v = ns.v / 1024 -%}
|
||||
{%- set ns.i = ns.i + 1 -%}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{{ '%.1f'|format(ns.v) }} {{ u[ns.i] }}
|
||||
{%- endif -%}
|
||||
{%- endmacro %}
|
||||
{% block content %}
|
||||
<h5 class="mb-3"><i class="bi bi-activity"></i> 活跃连接 <span class="text-muted small">({{ conns|length }} 条)</span></h5>
|
||||
<div class="page-header">
|
||||
<h5><i class="bi bi-activity"></i> 活跃连接 <span class="text-muted small">({{ conns|length }} 条)</span></h5>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body p-0">
|
||||
@@ -20,7 +36,7 @@
|
||||
<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><small class="text-muted">{{ c.started[:19].replace('T',' ') if c.started else '-' }}</small></td>
|
||||
<td>{{ fmtBytes(c.bytes_in) }}</td>
|
||||
<td>{{ fmtBytes(c.bytes_out) }}</td>
|
||||
<td>
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
{% block title %}仪表盘 · SOCKS Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="mb-3"><i class="bi bi-speedometer2"></i> 仪表盘</h5>
|
||||
<div class="page-header">
|
||||
<h5><i class="bi bi-speedometer2"></i> 仪表盘</h5>
|
||||
</div>
|
||||
|
||||
<!-- 系统状态卡片 -->
|
||||
<div class="row g-3 mb-3">
|
||||
@@ -75,7 +77,7 @@
|
||||
<span class="text-muted small">每 10 秒刷新</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="metricsChart" height="80"></canvas>
|
||||
<canvas id="metricsChart" style="height:240px"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -118,14 +120,20 @@ const chart = new Chart(ctx, {
|
||||
maintainAspectRatio: false,
|
||||
animation: false,
|
||||
plugins: {
|
||||
legend: { labels: { color: '#94a3b8' } },
|
||||
legend: { labels: { color: '#cbd5e1' } },
|
||||
},
|
||||
scales: {
|
||||
x: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' } },
|
||||
y: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' }, min: 0, max: 100 },
|
||||
x: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf', maxTicksLimit: 10 } },
|
||||
y: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf' }, min: 0, max: 100 },
|
||||
},
|
||||
}
|
||||
});
|
||||
// 工具函数
|
||||
function fmtTs(s) {
|
||||
if (!s) return '-';
|
||||
// 把 ISO 格式 "2026-07-16T14:33:38.297700" 转成 "2026-07-16 14:33:38"
|
||||
return s.replace('T', ' ').slice(0, 19);
|
||||
}
|
||||
|
||||
// 加载最近日志
|
||||
async function loadRecentLogs() {
|
||||
@@ -144,7 +152,7 @@ async function loadRecentLogs() {
|
||||
else if (l.event.includes('fail') || l.event.includes('reject')) badgeClass = 'danger';
|
||||
else if (l.event.includes('start') || l.event.includes('stop')) badgeClass = 'warning';
|
||||
return `<tr>
|
||||
<td><small>${l.timestamp.slice(0,19).replace('T',' ')}</small></td>
|
||||
<td><small>${fmtTs(l.timestamp)}</small></td>
|
||||
<td><span class="badge badge-${badgeClass}">${l.event}</span></td>
|
||||
<td>${l.user || '-'}</td>
|
||||
<td><code>${l.src_ip || '-'}</code></td>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
{% 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>
|
||||
<div class="page-header">
|
||||
<h5><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>
|
||||
@@ -44,17 +44,17 @@
|
||||
<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>
|
||||
<button class="btn btn-outline-success" title="启动"><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>
|
||||
<button class="btn btn-outline-warning" title="停止"><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>
|
||||
<button class="btn btn-outline-primary" title="重启"><i class="bi bi-arrow-clockwise"></i></button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<button class="btn btn-outline-secondary" data-bs-toggle="modal"
|
||||
<button class="btn btn-outline-secondary" title="编辑" data-bs-toggle="modal"
|
||||
data-bs-target="#editInstanceModal"
|
||||
data-name="{{ inst.name }}"
|
||||
data-host="{{ inst.host }}"
|
||||
@@ -70,7 +70,7 @@
|
||||
</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>
|
||||
<button class="btn btn-outline-danger" title="删除"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
+4
-2
@@ -2,7 +2,9 @@
|
||||
{% block title %}审计日志 · SOCKS Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="mb-3"><i class="bi bi-journal-text"></i> 审计日志 <span class="text-muted small">({{ total }} 条)</span></h5>
|
||||
<div class="page-header">
|
||||
<h5><i class="bi bi-journal-text"></i> 审计日志 <span class="text-muted small">({{ total }} 条)</span></h5>
|
||||
</div>
|
||||
|
||||
<!-- 筛选 -->
|
||||
<div class="card mb-3">
|
||||
@@ -43,7 +45,7 @@
|
||||
<tbody>
|
||||
{% for l in logs %}
|
||||
<tr>
|
||||
<td><small>{{ l.timestamp }}</small></td>
|
||||
<td><small>{{ l.timestamp[:19].replace('T',' ') if l.timestamp else '-' }}</small></td>
|
||||
<td>
|
||||
<span class="badge badge-{{
|
||||
'success' if 'success' in l.event else
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
{% block title %}流量统计 · SOCKS Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="mb-3"><i class="bi bi-graph-up"></i> 流量统计</h5>
|
||||
<div class="page-header">
|
||||
<h5><i class="bi bi-graph-up"></i> 流量统计</h5>
|
||||
</div>
|
||||
|
||||
<!-- 流量趋势图表 -->
|
||||
<div class="card mb-3">
|
||||
@@ -89,10 +91,10 @@ new Chart(ctx, {
|
||||
},
|
||||
options: {
|
||||
responsive: true, maintainAspectRatio: false,
|
||||
plugins: { legend: { labels: { color: '#94a3b8' } } },
|
||||
plugins: { legend: { labels: { color: '#cbd5e1' } } },
|
||||
scales: {
|
||||
x: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280', maxTicksLimit: 15 } },
|
||||
y: { grid: { color: '#1f2335' }, ticks: { color: '#6b7280' } },
|
||||
x: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf', maxTicksLimit: 10 } },
|
||||
y: { grid: { color: '#262b3d' }, ticks: { color: '#a3acbf' } },
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
{% block title %}系统管理 · SOCKS Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h5 class="mb-3"><i class="bi bi-shield-lock"></i> 系统管理</h5>
|
||||
<div class="page-header">
|
||||
<h5><i class="bi bi-shield-lock"></i> 系统管理</h5>
|
||||
</div>
|
||||
|
||||
<!-- 备份管理 -->
|
||||
<div class="card mb-3">
|
||||
|
||||
+10
-6
@@ -2,7 +2,7 @@
|
||||
{% block title %}用户管理 · SOCKS Manager{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h5 class="mb-0"><i class="bi bi-people"></i> 用户管理</h5>
|
||||
<span class="text-muted small">共 {{ total }} 个用户</span>
|
||||
@@ -11,7 +11,7 @@
|
||||
<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>
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm" title="搜索"><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>
|
||||
@@ -55,7 +55,7 @@
|
||||
<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"
|
||||
<button class="btn btn-outline-secondary" title="编辑" data-bs-toggle="modal" data-bs-target="#editUserModal"
|
||||
data-id="{{ u.id }}"
|
||||
data-username="{{ u.username }}"
|
||||
data-bw-down="{{ u.bandwidth_down }}"
|
||||
@@ -69,18 +69,22 @@
|
||||
<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' }}">
|
||||
<input type="hidden" name="enabled" value="{{ 'false' if u.enabled else 'true' }}">
|
||||
<button class="btn btn-outline-{{ 'warning' if u.enabled else 'success' }}"
|
||||
title="{{ '禁用' if u.enabled else '启用' }}">
|
||||
<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' }}">
|
||||
<input type="hidden" name="ban" value="{{ 'false' if u.banned else 'true' }}">
|
||||
<button class="btn btn-outline-{{ 'warning' if not u.banned else 'secondary' }}"
|
||||
title="{{ '封禁' if not u.banned else '解封' }}">
|
||||
<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>
|
||||
<button class="btn btn-outline-danger" title="删除"><i class="bi bi-trash"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
+58
-9
@@ -12,15 +12,33 @@ bp = Blueprint("web", __name__)
|
||||
@bp.app_context_processor
|
||||
def inject_nav():
|
||||
from flask import session as flask_session
|
||||
p = request.path
|
||||
if p == "/" or p.startswith("/dashboard"):
|
||||
page_name = "仪表盘"
|
||||
elif p.startswith("/instances"):
|
||||
page_name = "代理实例"
|
||||
elif p.startswith("/users"):
|
||||
page_name = "用户管理"
|
||||
elif p == "/stats":
|
||||
page_name = "流量统计"
|
||||
elif p.startswith("/stats/connections"):
|
||||
page_name = "活跃连接"
|
||||
elif p.startswith("/logs"):
|
||||
page_name = "审计日志"
|
||||
elif p.startswith("/system"):
|
||||
page_name = "系统管理"
|
||||
else:
|
||||
page_name = ""
|
||||
return {
|
||||
"nav": {
|
||||
"instances": request.path.startswith("/instances") or
|
||||
request.path == "/" or request.path == "/dashboard",
|
||||
"users": request.path.startswith("/users"),
|
||||
"stats": request.path.startswith("/stats"),
|
||||
"logs": request.path.startswith("/logs"),
|
||||
"system": request.path.startswith("/system"),
|
||||
"dashboard": p == "/" or p.startswith("/dashboard"),
|
||||
"instances": p.startswith("/instances"),
|
||||
"users": p.startswith("/users"),
|
||||
"stats": p.startswith("/stats"),
|
||||
"logs": p.startswith("/logs"),
|
||||
"system": p.startswith("/system"),
|
||||
},
|
||||
"page_name": page_name,
|
||||
"logged_in": flask_session.get("logged_in", False),
|
||||
}
|
||||
|
||||
@@ -45,8 +63,11 @@ def instances():
|
||||
@bp.route("/instances/<int:iid>/start", methods=["POST"])
|
||||
@login_required
|
||||
def start_instance(iid):
|
||||
try:
|
||||
current_app.instance_manager.start_instance(iid)
|
||||
flash("实例已启动", "success")
|
||||
except OSError as e:
|
||||
flash(f"启动失败(端口可能被占用): {e}", "danger")
|
||||
return redirect(url_for("web.instances"))
|
||||
|
||||
|
||||
@@ -61,8 +82,11 @@ def stop_instance(iid):
|
||||
@bp.route("/instances/<int:iid>/restart", methods=["POST"])
|
||||
@login_required
|
||||
def restart_instance(iid):
|
||||
try:
|
||||
current_app.instance_manager.restart_instance(iid)
|
||||
flash("实例已重启", "info")
|
||||
except OSError as e:
|
||||
flash(f"重启失败(端口可能被占用): {e}", "danger")
|
||||
return redirect(url_for("web.instances"))
|
||||
|
||||
|
||||
@@ -116,7 +140,10 @@ def edit_instance(iid):
|
||||
db.session.commit()
|
||||
|
||||
if inst.running:
|
||||
try:
|
||||
current_app.instance_manager.restart_instance(iid)
|
||||
except OSError as e:
|
||||
flash(f"重启失败(端口可能被占用): {e}", "warning")
|
||||
|
||||
flash("配置已更新", "success")
|
||||
return redirect(url_for("web.instances"))
|
||||
@@ -205,7 +232,19 @@ def delete_user(uid):
|
||||
@bp.route("/users/<int:uid>/toggle", methods=["POST"])
|
||||
@login_required
|
||||
def toggle_user(uid):
|
||||
enabled = "enabled" in request.form
|
||||
# 支持两种调用方式:
|
||||
# 1) 隐藏字段 enabled=on/off (web form)
|
||||
# 2) 不带字段时切换当前状态 (传统行为, 防止表单无字段导致总是禁用)
|
||||
if "enabled" in request.form:
|
||||
enabled = request.form.get("enabled") in ("on", "true", "1", "yes")
|
||||
else:
|
||||
from models import User
|
||||
from database import db
|
||||
u = db.session.query(User).get(uid)
|
||||
if not u:
|
||||
flash("用户不存在", "danger")
|
||||
return redirect(url_for("web.users"))
|
||||
enabled = not u.enabled
|
||||
result = current_app.user_service.toggle_user(uid, enabled)
|
||||
flash("已" + ("启用" if enabled else "禁用") + "该用户", "success")
|
||||
return redirect(url_for("web.users"))
|
||||
@@ -214,7 +253,17 @@ def toggle_user(uid):
|
||||
@bp.route("/users/<int:uid>/ban", methods=["POST"])
|
||||
@login_required
|
||||
def ban_user(uid):
|
||||
banned = "ban" in request.form
|
||||
# 同 toggle_user 的两种调用方式
|
||||
if "ban" in request.form:
|
||||
banned = request.form.get("ban") in ("on", "true", "1", "yes")
|
||||
else:
|
||||
from models import User
|
||||
from database import db
|
||||
u = db.session.query(User).get(uid)
|
||||
if not u:
|
||||
flash("用户不存在", "danger")
|
||||
return redirect(url_for("web.users"))
|
||||
banned = not u.banned
|
||||
result = current_app.user_service.ban_user(uid, banned)
|
||||
flash("已" + ("封禁" if banned else "解封") + "该用户", "success")
|
||||
return redirect(url_for("web.users"))
|
||||
@@ -265,7 +314,7 @@ def system():
|
||||
sys_version=platform.python_version(),
|
||||
sys_platform=platform.platform(),
|
||||
sys_hostname=platform.node(),
|
||||
uptime=time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime()))
|
||||
uptime=time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
|
||||
|
||||
|
||||
@bp.route("/system/backup", methods=["POST"])
|
||||
|
||||
Reference in New Issue
Block a user