From 0bd9bcac12a43878c05c15e10887075fa7812495 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 16 Jul 2026 23:01:39 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=9A=97=E8=89=B2?= =?UTF-8?q?=E4=B8=BB=E9=A2=98UI=E5=AF=B9=E6=AF=94=E5=BA=A6=E3=80=81?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E5=8D=A0=E7=94=A8=E6=AD=BB=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E5=8F=8A=E5=A4=9A=E9=A1=B9Web=E8=A1=A8=E5=8D=95bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端修复: - 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() 显示本地时间 --- api/v1.py | 29 ++++++--- auth.py | 2 + engine/instances.py | 50 +++++++++++++--- run.py | 5 +- templates/base.html | 118 +++++++++++++++++++++++-------------- templates/connections.html | 20 ++++++- templates/dashboard.html | 20 +++++-- templates/instances.html | 14 ++--- templates/logs.html | 6 +- templates/stats.html | 10 ++-- templates/system.html | 4 +- templates/users.html | 16 +++-- web/routes.py | 77 +++++++++++++++++++----- 13 files changed, 268 insertions(+), 103 deletions(-) diff --git a/api/v1.py b/api/v1.py index b2f202d..07a2cec 100644 --- a/api/v1.py +++ b/api/v1.py @@ -106,9 +106,12 @@ def update_instance(iid): # 重启以应用配置 if inst.running: - current_app.instance_manager.restart_instance(iid) + 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/", methods=["DELETE"]) @@ -126,25 +129,33 @@ def delete_instance(iid): @bp.route("/instances//start", methods=["POST"]) @login_required def start_instance(iid): - if current_app.instance_manager.start_instance(iid): - return jsonify({"ok": True}) - return {"error": "启动失败"}, 500 + 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//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//restart", methods=["POST"]) @login_required def restart_instance(iid): - if current_app.instance_manager.restart_instance(iid): - return jsonify({"ok": True}) - return {"error": "重启失败"}, 500 + 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"]) diff --git a/auth.py b/auth.py index b7260af..089ef19 100644 --- a/auth.py +++ b/auth.py @@ -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__) diff --git a/engine/instances.py b/engine/instances.py index cc2aefd..9c40689 100644 --- a/engine/instances.py +++ b/engine/instances.py @@ -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) - self.server = self.loop.run_until_complete( - self._create_server() - ) - self.loop.run_forever() + 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) - srv.start() + 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) - srv.start() + 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 diff --git a/run.py b/run.py index cf69f3d..ea7bbb3 100644 --- a/run.py +++ b/run.py @@ -12,7 +12,10 @@ if __name__ == "__main__": debug = os.environ.get("SM_DEBUG", "false").lower() == "true" # 启动时同步实例 - app.instance_manager.sync_instances() + try: + app.instance_manager.sync_instances() + except OSError as e: + print(f"⚠️ 部分实例启动失败(端口冲突?): {e}", flush=True) # 记录流量快照 from services.stats_service import record_traffic_snapshot diff --git a/templates/base.html b/templates/base.html index cc7112f..350d971 100644 --- a/templates/base.html +++ b/templates/base.html @@ -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; } {% block extra_style %}{% endblock %} @@ -155,7 +187,7 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;
SOCKS Manager
- {% if nav.instances %}仪表盘{% endif %} + {% if page_name %}{{ page_name }}{% endif %} 退出
@@ -165,7 +197,7 @@ code { background:var(--bg-tertiary); color:var(--accent); padding:.15rem .4rem;