feat: revamp web hub — global connection status, array config editor, log streaming

- Global connection badge in app header (persistent dashboard SSE via ConnectionProvider)
- LogProvider keeps log data and SSE stream alive across page switches (no reload spinner)
- Config drawer: password reveal toggle, array-type row editor for list fields
- Plugin management: optimistic updates, race-free loading, reset all group overrides
- Logs: load-earlier pagination; backend tail-read optimization (before/end offsets)
- web_auth: Web password changes persist (sync_admin seeding only when .env has explicit key)
- Dashboard: collect in thread pool + 1s result cache
- ErrorBoundary + local Suspense: plugin chunk loading stays inside content area
- UI polish: table row hover, mobile header/breadcrumb fixes, wider config drawer
- config_standard: array type inference for list fields; array env skip
- random_jm_code / steam_info: array schema migration; README web frontend note

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-03 15:39:10 +08:00
co-authored by Claude Code
parent 9371a28e35
commit c53b210413
45 changed files with 1312 additions and 576 deletions
+75 -2
View File
@@ -9,6 +9,7 @@
from __future__ import annotations
import hashlib
import json
import os
import secrets
import sqlite3
@@ -104,11 +105,78 @@ def init_auth_db() -> None:
conn.commit()
def _env_has_password_key() -> bool:
""".env 中是否显式配置了 hexi_web_password(区别于代码默认值 admin)。"""
env_path = Path(__file__).resolve().parent.parent / ".env"
if not env_path.exists():
return False
for line in env_path.read_text(encoding="utf-8").splitlines():
line = line.strip()
if line.startswith("hexi_web_password=") or line.startswith(
"hexi_web_password ="
):
return True
return False
def _write_env_password(password: str) -> None:
"""把 Web 端修改后的密码回写 .env,保持 .env 永远是权威源(重启后不还原)。"""
env_path = Path(__file__).resolve().parent.parent / ".env"
try:
lines = (
env_path.read_text(encoding="utf-8").splitlines()
if env_path.exists()
else []
)
replaced = False
out: list[str] = []
for line in lines:
stripped = line.strip()
if stripped.startswith("hexi_web_password=") or stripped.startswith(
"hexi_web_password ="
):
if not replaced:
# JSON 字符串写法可安全转义 # 等特殊字符,dotenv 解析后与裸值一致
out.append("hexi_web_password=" + json.dumps(password))
replaced = True
else:
out.append(line)
if not replaced:
out.append("hexi_web_password=" + json.dumps(password))
env_path.write_text(
"\n".join(out).rstrip("\n") + "\n", encoding="utf-8"
)
except OSError:
pass
def _seed_admin_if_missing(username: str, password: str) -> None:
with _conn() as conn:
row = conn.execute(
"SELECT id FROM web_users WHERE username=?", (username,)
).fetchone()
if row is None:
conn.execute(
"INSERT INTO web_users(username,password_hash,is_active,created_at) VALUES(?,?,1,?)",
(username, _hash_password(password), _now()),
)
conn.commit()
def sync_admin() -> None:
"""启动时调用:以 .env 为准创建/同步超级管理员(含改密码场景)。"""
"""启动时调用:以 .env 为准创建/同步超级管理员。
仅当 .env 显式配置了 hexi_web_password 时才会在每次启动同步密码
(此时 .env 是权威源,重启始终以 .env 覆盖);若 .env 未显式配置,
则只在用户不存在时播种默认账号,保证 Web 端「修改密码」能持久生效、
不被每次启动静默还原。
"""
init_auth_db()
username = _read_env("hexi_web_username", "admin")
password = _read_env("hexi_web_password", "admin")
if not _env_has_password_key():
_seed_admin_if_missing(username, password)
return
with _conn() as conn:
row = conn.execute(
"SELECT id FROM web_users WHERE username=?", (username,)
@@ -155,7 +223,11 @@ def revoke_user_tokens(user_id: int, except_token: Optional[str] = None) -> None
def change_password(
user_id: int, old_password: str, new_password: str, keep_token: Optional[str] = None
) -> bool:
"""校验旧密码后改新密码,并吊销旧令牌(保留当前令牌可选)。成功返回 True。"""
"""校验旧密码后改新密码,并吊销旧令牌(保留当前令牌可选)。成功返回 True。
成功后会回写 .env(hexi_web_password),保证重启后仍是新密码:
否则 sync_admin 会按 .env 默认值把密码还原。
"""
if not new_password:
return False
init_auth_db()
@@ -170,6 +242,7 @@ def change_password(
(_hash_password(new_password), user_id),
)
conn.commit()
_write_env_password(new_password)
revoke_user_tokens(user_id, except_token=keep_token)
return True