diff --git a/tests/test_manager_logic.py b/tests/test_manager_logic.py index 976de4e..8f0a30e 100644 --- a/tests/test_manager_logic.py +++ b/tests/test_manager_logic.py @@ -394,6 +394,33 @@ class ManagerLogicTests(unittest.TestCase): self.assertIn('class="country-option-input"', manager.INDEX_HTML) self.assertIn('${testBtn}', manager.INDEX_HTML) + def test_web_dashboard_has_browser_freeze_safeguards(self) -> None: + self.assertNotIn("backdrop-filter", manager.LOGIN_HTML) + self.assertNotIn("backdrop-filter", manager.INDEX_HTML) + self.assertNotIn("background-attachment: fixed", manager.INDEX_HTML) + self.assertIn("@media (prefers-reduced-motion: reduce)", manager.LOGIN_HTML) + self.assertIn("@media (prefers-reduced-motion: reduce)", manager.INDEX_HTML) + self.assertIn("const pageSize = 50;", manager.INDEX_HTML) + self.assertIn("const MAX_RENDERED_LOG_LINES = 300;", manager.INDEX_HTML) + self.assertIn("nodesRequestPromise", manager.INDEX_HTML) + self.assertIn("backgroundPollInFlight", manager.INDEX_HTML) + self.assertIn('typeof document.hidden !== "boolean" || !document.hidden', manager.INDEX_HTML) + self.assertEqual(500, manager.WEB_LOG_MAX_ENTRIES) + + def test_web_log_reader_only_returns_recent_valid_entries(self) -> None: + log_file = manager.DATA_DIR / "logs" / "current.json" + log_file.parent.mkdir(parents=True) + with log_file.open("w", encoding="utf-8") as f: + for index in range(520): + f.write(json.dumps({"index": index}) + "\n") + f.write("not-json\n") + + entries = manager.read_recent_log_entries(log_file) + + self.assertEqual(500, len(entries)) + self.assertEqual(20, entries[0]["index"]) + self.assertEqual(519, entries[-1]["index"]) + def test_web_update_controls_only_expose_stable_main_channel(self) -> None: self.assertEqual("2.1.0", manager.APP_VERSION) self.assertEqual("V2.1 正式版", manager.APP_VERSION_LABEL) diff --git a/vpngate_manager.py b/vpngate_manager.py index 125459c..c09e8da 100644 --- a/vpngate_manager.py +++ b/vpngate_manager.py @@ -16,6 +16,7 @@ import threading import time import urllib.parse import urllib.request +from collections import deque from http import HTTPStatus from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path @@ -150,6 +151,7 @@ BLACKLIST_FILE = DATA_DIR / "blacklist.json" API_CACHE_FILE = DATA_DIR / "api_snapshot.csv" API_CACHE_META_FILE = DATA_DIR / "api_snapshot.meta.json" BUNDLED_SNAPSHOT_FILE = ROOT_DIR / "mirror" / "vpngate.csv" +WEB_LOG_MAX_ENTRIES = 500 lock = threading.RLock() maintenance_lock = threading.Lock() @@ -385,6 +387,23 @@ def cleanup_old_logs(logs_dir: Path) -> None: except Exception as e: print(f"[清理错误] 清理旧日志失败: {e}", flush=True) +def read_recent_log_entries(log_file: Path, limit: int = WEB_LOG_MAX_ENTRIES) -> list[dict[str, Any]]: + if limit <= 0 or not log_file.exists(): + return [] + entries: deque[dict[str, Any]] = deque(maxlen=limit) + with open(log_file, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except (TypeError, ValueError): + continue + if isinstance(entry, dict): + entries.append(entry) + return list(entries) + def log_to_json(level: str, module: str, message: str) -> None: try: logs_dir = DATA_DIR / "logs" @@ -2484,7 +2503,7 @@ LOGIN_HTML = r""" @@ -2742,7 +2767,7 @@ INDEX_HTML = r""" :root { --bg-dark: #0b0f19; - --bg-surface: rgba(22, 30, 49, 0.6); + --bg-surface: rgba(22, 30, 49, 0.94); --bg-surface-hover: rgba(30, 41, 67, 0.85); --border-color: rgba(255, 255, 255, 0.08); --border-color-hover: rgba(99, 102, 241, 0.35); @@ -2769,7 +2794,6 @@ INDEX_HTML = r""" radial-gradient(at 0% 0%, rgba(99, 102, 241, 0.15) 0px, transparent 50%), radial-gradient(at 100% 0%, rgba(16, 185, 129, 0.08) 0px, transparent 50%), radial-gradient(at 50% 100%, rgba(79, 70, 229, 0.05) 0px, transparent 50%); - background-attachment: fixed; color: var(--text-primary); min-height: 100vh; -webkit-font-smoothing: antialiased; @@ -2777,9 +2801,7 @@ INDEX_HTML = r""" header { padding: 16px 32px; - background: rgba(11, 15, 25, 0.7); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); + background: rgba(11, 15, 25, 0.97); border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; @@ -2909,8 +2931,6 @@ INDEX_HTML = r""" .active-card { background: linear-gradient(135deg, rgba(99, 102, 241, 0.12) 0%, rgba(79, 70, 229, 0.04) 100%); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(99, 102, 241, 0.25); border-radius: 16px; padding: 24px; @@ -2975,8 +2995,6 @@ INDEX_HTML = r""" .stat { background: var(--bg-surface); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border-color); border-radius: 12px; padding: 20px; @@ -3208,8 +3226,6 @@ INDEX_HTML = r""" position: relative; z-index: 50; background: var(--bg-surface); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border-color); border-radius: 12px; padding: 16px; @@ -3429,8 +3445,6 @@ INDEX_HTML = r""" .table-wrapper { background: var(--bg-surface); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; @@ -3702,14 +3716,12 @@ INDEX_HTML = r""" right: 0; margin-top: 6px; min-width: 140px; - background: rgba(22, 30, 49, 0.95); + background: rgba(22, 30, 49, 0.99); border: 1px solid var(--border-color); border-radius: 8px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 1000; overflow: hidden; - backdrop-filter: blur(10px); - -webkit-backdrop-filter: blur(10px); } .dropdown-content a, .dropdown-content button { @@ -3797,14 +3809,12 @@ INDEX_HTML = r""" width: 100%; height: 100%; overflow: auto; - background-color: rgba(9, 13, 22, 0.7); - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); + background-color: rgba(9, 13, 22, 0.92); align-items: center; justify-content: center; } .modal-content { - background: rgba(22, 30, 49, 0.9); + background: rgba(22, 30, 49, 0.99); border: 1px solid var(--border-color); border-radius: 20px; width: 90%; @@ -3907,6 +3917,15 @@ INDEX_HTML = r""" color: var(--text-secondary); line-height: 1.3; } + + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } @@ -4027,7 +4046,7 @@ INDEX_HTML = r""" 收藏菜单 -