Add MySQL web controls
This commit is contained in:
@@ -57,6 +57,8 @@ docker compose down -v
|
|||||||
- 启动项目内置的每日自动同步调度器
|
- 启动项目内置的每日自动同步调度器
|
||||||
- 启动 Web 页面与 API 服务
|
- 启动 Web 页面与 API 服务
|
||||||
|
|
||||||
|
首次启动默认值仍来自环境变量;之后可在 Web UI 中修改自动装载开关,运行期配置会持久化到 `/data/state/mysql_settings.json`。
|
||||||
|
|
||||||
## MySQL 默认连接
|
## MySQL 默认连接
|
||||||
|
|
||||||
- Host: `127.0.0.1`
|
- Host: `127.0.0.1`
|
||||||
@@ -129,10 +131,25 @@ docker compose down -v
|
|||||||
- 品牌与厂商关系管理
|
- 品牌与厂商关系管理
|
||||||
- 品牌同义词管理
|
- 品牌同义词管理
|
||||||
- 数据来源优先级管理
|
- 数据来源优先级管理
|
||||||
|
- 外部 MySQL 手动初始化
|
||||||
- 原始数据同步
|
- 原始数据同步
|
||||||
- 每日自动同步时间点设置
|
- 每日自动同步时间点设置
|
||||||
- 索引数据查看与重新加载
|
- 索引数据查看与重新加载
|
||||||
|
|
||||||
|
### 外部 MySQL 手动初始化
|
||||||
|
|
||||||
|
- 页面入口:`数据管理 -> 原始数据同步 -> 初始化外部 MySQL`
|
||||||
|
- 适用于 `MYSQL_AUTO_LOAD=0` 的远程 MySQL
|
||||||
|
- 点击后会执行 schema 与 seed 导入,自动创建数据库,并重建 `mobilemodels` 相关表与视图
|
||||||
|
- 执行前请确认 `MYSQL_HOST`、`MYSQL_PORT`、`MYSQL_ROOT_USER`、`MYSQL_ROOT_PASSWORD` 指向正确且具备建库建表权限
|
||||||
|
|
||||||
|
### MySQL 自动装载开关
|
||||||
|
|
||||||
|
- 页面入口:`数据管理 -> 原始数据同步 -> MySQL 自动装载`
|
||||||
|
- 保存后会更新运行期配置 `/data/state/mysql_settings.json`
|
||||||
|
- 会影响后续“开始同步原始数据”是否自动刷新 MySQL
|
||||||
|
- 也会影响容器后续启动时是否自动执行 schema 与 seed 导入
|
||||||
|
|
||||||
### 每日自动同步
|
### 每日自动同步
|
||||||
|
|
||||||
- 调度器运行在项目容器内部,不依赖 GitHub Actions
|
- 调度器运行在项目容器内部,不依赖 GitHub Actions
|
||||||
|
|||||||
@@ -8,10 +8,32 @@ sh tools/init_runtime_data.sh
|
|||||||
python3 tools/device_mapper.py build
|
python3 tools/device_mapper.py build
|
||||||
python3 tools/export_mysql_seed.py
|
python3 tools/export_mysql_seed.py
|
||||||
|
|
||||||
if [ "${MYSQL_AUTO_LOAD:-0}" = "1" ]; then
|
MYSQL_AUTO_LOAD_EFFECTIVE="$(python3 - <<'PY'
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
data_root = Path(os.environ.get("MOBILEMODELS_DATA_ROOT", "/data"))
|
||||||
|
config_path = data_root / "state/mysql_settings.json"
|
||||||
|
raw_default = os.environ.get("MYSQL_AUTO_LOAD", "0").strip().lower()
|
||||||
|
value = raw_default in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if config_path.exists():
|
||||||
|
payload = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
|
raw = payload.get("auto_load", value) if isinstance(payload, dict) else value
|
||||||
|
value = raw if isinstance(raw, bool) else str(raw).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print("1" if value else "0")
|
||||||
|
PY
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [ "$MYSQL_AUTO_LOAD_EFFECTIVE" = "1" ]; then
|
||||||
python3 tools/load_mysql_seed.py
|
python3 tools/load_mysql_seed.py
|
||||||
else
|
else
|
||||||
echo "Skipping MySQL load because MYSQL_AUTO_LOAD=${MYSQL_AUTO_LOAD:-0}"
|
echo "Skipping MySQL load because MYSQL_AUTO_LOAD=$MYSQL_AUTO_LOAD_EFFECTIVE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exec python3 tools/web_server.py --host 0.0.0.0 --port 8123
|
exec python3 tools/web_server.py --host 0.0.0.0 --port 8123
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ MYSQL_LOADER = PROJECT_ROOT / "tools/load_mysql_seed.py"
|
|||||||
DATA_ROOT = Path(os.environ.get("MOBILEMODELS_DATA_ROOT", "/data"))
|
DATA_ROOT = Path(os.environ.get("MOBILEMODELS_DATA_ROOT", "/data"))
|
||||||
SYNC_METADATA_PATH = DATA_ROOT / "state/sync_status.json"
|
SYNC_METADATA_PATH = DATA_ROOT / "state/sync_status.json"
|
||||||
SCHEDULE_CONFIG_PATH = DATA_ROOT / "state/sync_schedule.json"
|
SCHEDULE_CONFIG_PATH = DATA_ROOT / "state/sync_schedule.json"
|
||||||
|
MYSQL_CONFIG_PATH = DATA_ROOT / "state/mysql_settings.json"
|
||||||
SYNC_LOCK = threading.Lock()
|
SYNC_LOCK = threading.Lock()
|
||||||
SCHEDULE_LOCK = threading.Lock()
|
SCHEDULE_LOCK = threading.Lock()
|
||||||
|
MYSQL_CONFIG_LOCK = threading.Lock()
|
||||||
NORMALIZE_RE = re.compile(r"[^0-9a-z\u4e00-\u9fff]+")
|
NORMALIZE_RE = re.compile(r"[^0-9a-z\u4e00-\u9fff]+")
|
||||||
SCHEDULE_TIME_RE = re.compile(r"^(?:[01]?\d|2[0-3]):[0-5]\d$")
|
SCHEDULE_TIME_RE = re.compile(r"^(?:[01]?\d|2[0-3]):[0-5]\d$")
|
||||||
SCHEDULER_POLL_SECONDS = 20
|
SCHEDULER_POLL_SECONDS = 20
|
||||||
@@ -47,7 +49,18 @@ def apply_timezone_from_env() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def mysql_auto_load_enabled() -> bool:
|
def mysql_auto_load_enabled() -> bool:
|
||||||
return truthy_env("MYSQL_AUTO_LOAD", "0")
|
return bool(read_mysql_config().get("auto_load"))
|
||||||
|
|
||||||
|
|
||||||
|
def mysql_probe_credentials() -> tuple[str, str]:
|
||||||
|
reader_user = os.environ.get("MYSQL_READER_USER", "").strip()
|
||||||
|
reader_password = os.environ.get("MYSQL_READER_PASSWORD", "")
|
||||||
|
if reader_user:
|
||||||
|
return reader_user, reader_password
|
||||||
|
return (
|
||||||
|
os.environ.get("MYSQL_ROOT_USER", "root").strip() or "root",
|
||||||
|
os.environ.get("MYSQL_ROOT_PASSWORD", ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def local_now() -> datetime:
|
def local_now() -> datetime:
|
||||||
@@ -116,6 +129,58 @@ def default_schedule_config() -> dict[str, object]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def default_mysql_config() -> dict[str, object]:
|
||||||
|
return {
|
||||||
|
"auto_load": truthy_env("MYSQL_AUTO_LOAD", "0"),
|
||||||
|
"updated_at": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_mysql_config(raw: dict[str, object] | None) -> dict[str, object]:
|
||||||
|
config = default_mysql_config()
|
||||||
|
if isinstance(raw, dict):
|
||||||
|
if "auto_load" in raw:
|
||||||
|
value = raw.get("auto_load")
|
||||||
|
config["auto_load"] = value if isinstance(value, bool) else str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
if raw.get("updated_at") is not None:
|
||||||
|
config["updated_at"] = raw.get("updated_at")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
def read_mysql_config() -> dict[str, object]:
|
||||||
|
with MYSQL_CONFIG_LOCK:
|
||||||
|
if not MYSQL_CONFIG_PATH.exists():
|
||||||
|
return normalize_mysql_config(None)
|
||||||
|
try:
|
||||||
|
payload = json.loads(MYSQL_CONFIG_PATH.read_text(encoding="utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return normalize_mysql_config(None)
|
||||||
|
return normalize_mysql_config(payload if isinstance(payload, dict) else None)
|
||||||
|
|
||||||
|
|
||||||
|
def write_mysql_config(payload: dict[str, object]) -> dict[str, object]:
|
||||||
|
normalized = normalize_mysql_config(payload)
|
||||||
|
with MYSQL_CONFIG_LOCK:
|
||||||
|
MYSQL_CONFIG_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
MYSQL_CONFIG_PATH.write_text(
|
||||||
|
json.dumps(normalized, ensure_ascii=False, indent=2),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def update_mysql_config(payload: dict[str, object]) -> dict[str, object]:
|
||||||
|
current = read_mysql_config()
|
||||||
|
auto_load_raw = payload.get("auto_load", current.get("auto_load", False))
|
||||||
|
auto_load = auto_load_raw if isinstance(auto_load_raw, bool) else str(auto_load_raw).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
updated = {
|
||||||
|
**current,
|
||||||
|
"auto_load": auto_load,
|
||||||
|
"updated_at": local_now().isoformat(timespec="seconds"),
|
||||||
|
}
|
||||||
|
return write_mysql_config(updated)
|
||||||
|
|
||||||
|
|
||||||
def normalize_schedule_config(raw: dict[str, object] | None) -> dict[str, object]:
|
def normalize_schedule_config(raw: dict[str, object] | None) -> dict[str, object]:
|
||||||
config = default_schedule_config()
|
config = default_schedule_config()
|
||||||
if isinstance(raw, dict):
|
if isinstance(raw, dict):
|
||||||
@@ -372,26 +437,38 @@ def get_status_payload() -> dict[str, object]:
|
|||||||
mysql_database = os.environ.get("MYSQL_DATABASE", "mobilemodels")
|
mysql_database = os.environ.get("MYSQL_DATABASE", "mobilemodels")
|
||||||
mysql_reader_user = os.environ.get("MYSQL_READER_USER", "")
|
mysql_reader_user = os.environ.get("MYSQL_READER_USER", "")
|
||||||
mysql_reader_password = os.environ.get("MYSQL_READER_PASSWORD", "")
|
mysql_reader_password = os.environ.get("MYSQL_READER_PASSWORD", "")
|
||||||
mysql_auto_load = mysql_auto_load_enabled()
|
mysql_config = read_mysql_config()
|
||||||
|
mysql_auto_load = bool(mysql_config.get("auto_load"))
|
||||||
mysql_ready = False
|
mysql_ready = False
|
||||||
mysql_status = ""
|
mysql_status = ""
|
||||||
sync_metadata = read_sync_metadata()
|
sync_metadata = read_sync_metadata()
|
||||||
schedule_config = read_schedule_config()
|
schedule_config = read_schedule_config()
|
||||||
github_proxy_prefix = str(schedule_config.get("github_proxy_prefix") or "")
|
github_proxy_prefix = str(schedule_config.get("github_proxy_prefix") or "")
|
||||||
effective_repo_url = get_effective_repo_url(github_proxy_prefix)
|
effective_repo_url = get_effective_repo_url(github_proxy_prefix)
|
||||||
if mysql_auto_load:
|
probe_user, probe_password = mysql_probe_credentials()
|
||||||
mysql_proc = run_command(["python3", str(MYSQL_LOADER), "--check-only", "--wait-timeout", "5"])
|
mysql_proc = run_command(
|
||||||
if mysql_proc.returncode == 0:
|
[
|
||||||
mysql_ready = True
|
"python3",
|
||||||
mysql_status = mysql_proc.stdout.strip() or "MySQL ready"
|
str(MYSQL_LOADER),
|
||||||
else:
|
"--check-only",
|
||||||
mysql_status = sanitize_mysql_message(
|
"--wait-timeout",
|
||||||
mysql_proc.stderr.strip() or mysql_proc.stdout.strip() or "MySQL unavailable",
|
"5",
|
||||||
host=mysql_host,
|
f"--user={probe_user}",
|
||||||
port=mysql_port,
|
f"--password={probe_password}",
|
||||||
)
|
]
|
||||||
|
)
|
||||||
|
if mysql_proc.returncode == 0:
|
||||||
|
mysql_ready = True
|
||||||
|
mysql_status = mysql_proc.stdout.strip() or "MySQL ready"
|
||||||
|
if not mysql_auto_load:
|
||||||
|
mysql_status = f"{mysql_status}; auto load disabled"
|
||||||
else:
|
else:
|
||||||
mysql_status = "MySQL auto load disabled"
|
failure_message = sanitize_mysql_message(
|
||||||
|
mysql_proc.stderr.strip() or mysql_proc.stdout.strip() or "MySQL unavailable",
|
||||||
|
host=mysql_host,
|
||||||
|
port=mysql_port,
|
||||||
|
)
|
||||||
|
mysql_status = failure_message if mysql_auto_load else f"{failure_message}; auto load disabled"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"supports_upstream_sync": True,
|
"supports_upstream_sync": True,
|
||||||
@@ -400,6 +477,8 @@ def get_status_payload() -> dict[str, object]:
|
|||||||
"workspace_root": str(WORKSPACE_ROOT),
|
"workspace_root": str(WORKSPACE_ROOT),
|
||||||
"data_root": str(DATA_ROOT),
|
"data_root": str(DATA_ROOT),
|
||||||
"mysql_auto_load": mysql_auto_load,
|
"mysql_auto_load": mysql_auto_load,
|
||||||
|
"mysql_config_file": str(MYSQL_CONFIG_PATH.relative_to(DATA_ROOT)),
|
||||||
|
"mysql_config_updated_at": mysql_config.get("updated_at"),
|
||||||
"upstream_repo_url": DEFAULT_REPO_URL,
|
"upstream_repo_url": DEFAULT_REPO_URL,
|
||||||
"effective_upstream_repo_url": effective_repo_url,
|
"effective_upstream_repo_url": effective_repo_url,
|
||||||
"upstream_branch": DEFAULT_BRANCH,
|
"upstream_branch": DEFAULT_BRANCH,
|
||||||
@@ -498,6 +577,30 @@ def run_upstream_sync(trigger_source: str = "manual") -> dict[str, object]:
|
|||||||
SYNC_LOCK.release()
|
SYNC_LOCK.release()
|
||||||
|
|
||||||
|
|
||||||
|
def run_mysql_init(trigger_source: str = "manual") -> dict[str, object]:
|
||||||
|
if not SYNC_LOCK.acquire(blocking=False):
|
||||||
|
raise RuntimeError("已有同步或 MySQL 初始化任务在执行,请稍后再试。")
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = run_command(["python3", str(MYSQL_LOADER)])
|
||||||
|
output = "\n".join(
|
||||||
|
part for part in [proc.stdout.strip(), proc.stderr.strip()] if part
|
||||||
|
).strip()
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(output or f"mysql load failed with exit code {proc.returncode}")
|
||||||
|
|
||||||
|
payload = get_status_payload()
|
||||||
|
payload.update(
|
||||||
|
{
|
||||||
|
"trigger_source": trigger_source,
|
||||||
|
"output": output or "MySQL 初始化完成。",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return payload
|
||||||
|
finally:
|
||||||
|
SYNC_LOCK.release()
|
||||||
|
|
||||||
|
|
||||||
def run_scheduled_sync_if_due() -> None:
|
def run_scheduled_sync_if_due() -> None:
|
||||||
schedule_config = read_schedule_config()
|
schedule_config = read_schedule_config()
|
||||||
if not schedule_config.get("enabled"):
|
if not schedule_config.get("enabled"):
|
||||||
@@ -590,6 +693,16 @@ class MobileModelsHandler(SimpleHTTPRequestHandler):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
self._send_json({"error": str(err)}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
self._send_json({"error": str(err)}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
return
|
return
|
||||||
|
if self.path == "/api/init-mysql":
|
||||||
|
try:
|
||||||
|
payload = run_mysql_init()
|
||||||
|
self._send_json(payload)
|
||||||
|
except RuntimeError as err:
|
||||||
|
status = HTTPStatus.CONFLICT if "已有同步或 MySQL 初始化任务" in str(err) else HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
|
self._send_json({"error": str(err)}, status=status)
|
||||||
|
except Exception as err:
|
||||||
|
self._send_json({"error": str(err)}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
return
|
||||||
if self.path == "/api/query-sql":
|
if self.path == "/api/query-sql":
|
||||||
try:
|
try:
|
||||||
content_length = int(self.headers.get("Content-Length", "0") or "0")
|
content_length = int(self.headers.get("Content-Length", "0") or "0")
|
||||||
@@ -631,6 +744,27 @@ class MobileModelsHandler(SimpleHTTPRequestHandler):
|
|||||||
except Exception as err:
|
except Exception as err:
|
||||||
self._send_json({"error": str(err)}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
self._send_json({"error": str(err)}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
return
|
return
|
||||||
|
if self.path == "/api/mysql-settings":
|
||||||
|
try:
|
||||||
|
content_length = int(self.headers.get("Content-Length", "0") or "0")
|
||||||
|
raw_body = self.rfile.read(content_length) if content_length > 0 else b"{}"
|
||||||
|
req = json.loads(raw_body.decode("utf-8") or "{}")
|
||||||
|
if not isinstance(req, dict):
|
||||||
|
raise RuntimeError("请求体必须是 JSON 对象。")
|
||||||
|
mysql_config = update_mysql_config(req)
|
||||||
|
self._send_json(
|
||||||
|
{
|
||||||
|
"message": "MySQL 自动装载设置已保存。",
|
||||||
|
"mysql_auto_load": mysql_config.get("auto_load"),
|
||||||
|
"mysql_config_file": str(MYSQL_CONFIG_PATH.relative_to(DATA_ROOT)),
|
||||||
|
"mysql_config_updated_at": mysql_config.get("updated_at"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except RuntimeError as err:
|
||||||
|
self._send_json({"error": str(err)}, status=HTTPStatus.BAD_REQUEST)
|
||||||
|
except Exception as err:
|
||||||
|
self._send_json({"error": str(err)}, status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
return
|
||||||
|
|
||||||
self._send_json({"error": "Not found"}, status=HTTPStatus.NOT_FOUND)
|
self._send_json({"error": "Not found"}, status=HTTPStatus.NOT_FOUND)
|
||||||
|
|
||||||
@@ -645,6 +779,7 @@ def parse_args() -> argparse.Namespace:
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
apply_timezone_from_env()
|
apply_timezone_from_env()
|
||||||
write_schedule_config(read_schedule_config())
|
write_schedule_config(read_schedule_config())
|
||||||
|
write_mysql_config(read_mysql_config())
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
scheduler = threading.Thread(target=scheduler_loop, name="sync-scheduler", daemon=True)
|
scheduler = threading.Thread(target=scheduler_loop, name="sync-scheduler", daemon=True)
|
||||||
scheduler.start()
|
scheduler.start()
|
||||||
|
|||||||
@@ -426,6 +426,27 @@
|
|||||||
<section id="syncTabPanel" class="manage-panel hidden">
|
<section id="syncTabPanel" class="manage-panel hidden">
|
||||||
<h3 class="title">原始数据同步</h3>
|
<h3 class="title">原始数据同步</h3>
|
||||||
<p class="sub">从上游 `KHwang9883/MobileModels` 拉取原始 markdown 数据,并重建 `dist/device_index.json`。如已开启 MySQL 自动装载,也会同步刷新 MySQL。请先启动完整服务。</p>
|
<p class="sub">从上游 `KHwang9883/MobileModels` 拉取原始 markdown 数据,并重建 `dist/device_index.json`。如已开启 MySQL 自动装载,也会同步刷新 MySQL。请先启动完整服务。</p>
|
||||||
|
<div class="sync-schedule-card">
|
||||||
|
<h4 class="title">MySQL 自动装载</h4>
|
||||||
|
<p class="sub">控制同步任务和容器后续启动时是否自动导入 schema 与 seed。保持关闭更安全;开启后,启动容器和“开始同步原始数据”都可能刷新 MySQL 数据。</p>
|
||||||
|
<div class="sync-schedule-grid">
|
||||||
|
<label class="check-row">
|
||||||
|
<input id="mysqlAutoLoadEnabled" type="checkbox" />
|
||||||
|
<span>启用 MySQL 自动装载</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="btns">
|
||||||
|
<button id="saveMysqlSettingsBtn" type="button">保存 MySQL 设置</button>
|
||||||
|
</div>
|
||||||
|
<div id="mysqlSettingsStatus" class="sub">正在读取 MySQL 自动装载设置。</div>
|
||||||
|
</div>
|
||||||
|
<div class="sync-schedule-card">
|
||||||
|
<h4 class="title">外部 MySQL 初始化</h4>
|
||||||
|
<p class="sub">面向关闭自动装载的外部 MySQL。点击后会执行 schema 与 seed 导入,自动创建数据库,并重建 `mobilemodels` 相关表与视图。请确认连接参数与账号权限无误后再执行。</p>
|
||||||
|
<div class="btns">
|
||||||
|
<button id="initMysqlBtn" type="button">初始化外部 MySQL</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="sync-schedule-card">
|
<div class="sync-schedule-card">
|
||||||
<h4 class="title">每日自动同步</h4>
|
<h4 class="title">每日自动同步</h4>
|
||||||
<p class="sub">在项目容器内按固定时间自动拉取上游原始数据,并重建索引与 MySQL Seed。时间按容器时区执行,设置会持久化到运行期数据目录。</p>
|
<p class="sub">在项目容器内按固定时间自动拉取上游原始数据,并重建索引与 MySQL Seed。时间按容器时区执行,设置会持久化到运行期数据目录。</p>
|
||||||
@@ -520,7 +541,11 @@
|
|||||||
const syncStatusEl = document.getElementById("syncStatus");
|
const syncStatusEl = document.getElementById("syncStatus");
|
||||||
const syncLogEl = document.getElementById("syncLog");
|
const syncLogEl = document.getElementById("syncLog");
|
||||||
const syncUpstreamBtnEl = document.getElementById("syncUpstreamBtn");
|
const syncUpstreamBtnEl = document.getElementById("syncUpstreamBtn");
|
||||||
|
const initMysqlBtnEl = document.getElementById("initMysqlBtn");
|
||||||
const refreshSyncStatusBtnEl = document.getElementById("refreshSyncStatusBtn");
|
const refreshSyncStatusBtnEl = document.getElementById("refreshSyncStatusBtn");
|
||||||
|
const mysqlAutoLoadEnabledEl = document.getElementById("mysqlAutoLoadEnabled");
|
||||||
|
const saveMysqlSettingsBtnEl = document.getElementById("saveMysqlSettingsBtn");
|
||||||
|
const mysqlSettingsStatusEl = document.getElementById("mysqlSettingsStatus");
|
||||||
const scheduleEnabledEl = document.getElementById("scheduleEnabled");
|
const scheduleEnabledEl = document.getElementById("scheduleEnabled");
|
||||||
const scheduleTimeInputEl = document.getElementById("scheduleTimeInput");
|
const scheduleTimeInputEl = document.getElementById("scheduleTimeInput");
|
||||||
const githubProxyPrefixInputEl = document.getElementById("githubProxyPrefixInput");
|
const githubProxyPrefixInputEl = document.getElementById("githubProxyPrefixInput");
|
||||||
@@ -532,6 +557,8 @@
|
|||||||
|
|
||||||
let syncSupported = false;
|
let syncSupported = false;
|
||||||
let syncRunning = false;
|
let syncRunning = false;
|
||||||
|
let mysqlInitRunning = false;
|
||||||
|
let mysqlSettingsSaving = false;
|
||||||
let scheduleSaving = false;
|
let scheduleSaving = false;
|
||||||
|
|
||||||
function normalizeText(text) {
|
function normalizeText(text) {
|
||||||
@@ -557,9 +584,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateSyncButtons() {
|
function updateSyncButtons() {
|
||||||
syncUpstreamBtnEl.disabled = syncRunning || !syncSupported;
|
const busy = syncRunning || mysqlInitRunning;
|
||||||
refreshSyncStatusBtnEl.disabled = syncRunning;
|
syncUpstreamBtnEl.disabled = busy || !syncSupported;
|
||||||
saveSyncScheduleBtnEl.disabled = syncRunning || scheduleSaving;
|
initMysqlBtnEl.disabled = busy || !syncSupported;
|
||||||
|
refreshSyncStatusBtnEl.disabled = busy;
|
||||||
|
saveMysqlSettingsBtnEl.disabled = busy || mysqlSettingsSaving || !syncSupported;
|
||||||
|
saveSyncScheduleBtnEl.disabled = busy || scheduleSaving;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderIndexStatus(message, details) {
|
function renderIndexStatus(message, details) {
|
||||||
@@ -597,7 +627,7 @@
|
|||||||
if (data.mysql_status) lines.push(`MySQL 详情: ${data.mysql_status}`);
|
if (data.mysql_status) lines.push(`MySQL 详情: ${data.mysql_status}`);
|
||||||
if (data.output) {
|
if (data.output) {
|
||||||
lines.push("");
|
lines.push("");
|
||||||
lines.push("同步输出:");
|
lines.push("任务输出:");
|
||||||
lines.push(data.output);
|
lines.push(data.output);
|
||||||
}
|
}
|
||||||
syncLogEl.textContent = lines.join("\n").trim() || "暂无同步记录";
|
syncLogEl.textContent = lines.join("\n").trim() || "暂无同步记录";
|
||||||
@@ -627,9 +657,24 @@
|
|||||||
scheduleStatusEl.textContent = lines.join(";");
|
scheduleStatusEl.textContent = lines.join(";");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMysqlSettingsStatus(data, options = {}) {
|
||||||
|
const preserveMessage = !!options.preserveMessage;
|
||||||
|
const enabled = !!(data && data.mysql_auto_load);
|
||||||
|
mysqlAutoLoadEnabledEl.checked = enabled;
|
||||||
|
if (preserveMessage) return;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`自动装载: ${enabled ? "已启用" : "未启用"}`,
|
||||||
|
];
|
||||||
|
if (data && data.mysql_config_updated_at) lines.push(`最近更新: ${data.mysql_config_updated_at}`);
|
||||||
|
if (data && data.mysql_config_file) lines.push(`配置文件: ${data.mysql_config_file}`);
|
||||||
|
mysqlSettingsStatusEl.textContent = lines.join(";");
|
||||||
|
}
|
||||||
|
|
||||||
async function loadSyncStatus(options = {}) {
|
async function loadSyncStatus(options = {}) {
|
||||||
const preserveLog = !!options.preserveLog;
|
const preserveLog = !!options.preserveLog;
|
||||||
const preserveScheduleMessage = !!options.preserveScheduleMessage;
|
const preserveScheduleMessage = !!options.preserveScheduleMessage;
|
||||||
|
const preserveMysqlSettingsMessage = !!options.preserveMysqlSettingsMessage;
|
||||||
syncStatusEl.textContent = "正在检测同步能力。";
|
syncStatusEl.textContent = "正在检测同步能力。";
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson("/api/status", { cache: "no-store" });
|
const data = await fetchJson("/api/status", { cache: "no-store" });
|
||||||
@@ -637,6 +682,7 @@
|
|||||||
syncStatusEl.textContent = syncSupported
|
syncStatusEl.textContent = syncSupported
|
||||||
? "已连接 Docker Compose 服务,可以直接从页面同步原始数据、索引和 MySQL。"
|
? "已连接 Docker Compose 服务,可以直接从页面同步原始数据、索引和 MySQL。"
|
||||||
: "当前服务不支持原始数据同步。";
|
: "当前服务不支持原始数据同步。";
|
||||||
|
renderMysqlSettingsStatus(data, { preserveMessage: preserveMysqlSettingsMessage });
|
||||||
renderScheduleStatus(data, { preserveMessage: preserveScheduleMessage });
|
renderScheduleStatus(data, { preserveMessage: preserveScheduleMessage });
|
||||||
if (!preserveLog) {
|
if (!preserveLog) {
|
||||||
renderSyncLog(data, "服务状态");
|
renderSyncLog(data, "服务状态");
|
||||||
@@ -644,6 +690,9 @@
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
syncSupported = false;
|
syncSupported = false;
|
||||||
syncStatusEl.textContent = `当前页面未连接支持同步的 Docker Compose 服务:${err.message}`;
|
syncStatusEl.textContent = `当前页面未连接支持同步的 Docker Compose 服务:${err.message}`;
|
||||||
|
if (!preserveMysqlSettingsMessage) {
|
||||||
|
mysqlSettingsStatusEl.textContent = `MySQL 设置读取失败: ${err.message}`;
|
||||||
|
}
|
||||||
if (!preserveScheduleMessage) {
|
if (!preserveScheduleMessage) {
|
||||||
scheduleStatusEl.textContent = `自动同步设置读取失败: ${err.message}`;
|
scheduleStatusEl.textContent = `自动同步设置读取失败: ${err.message}`;
|
||||||
}
|
}
|
||||||
@@ -656,7 +705,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function runUpstreamSync() {
|
async function runUpstreamSync() {
|
||||||
if (syncRunning) return;
|
if (syncRunning || mysqlInitRunning) return;
|
||||||
syncRunning = true;
|
syncRunning = true;
|
||||||
updateSyncButtons();
|
updateSyncButtons();
|
||||||
syncStatusEl.textContent = "正在同步原始数据、重建索引并刷新 MySQL,请稍候。";
|
syncStatusEl.textContent = "正在同步原始数据、重建索引并刷新 MySQL,请稍候。";
|
||||||
@@ -682,6 +731,59 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function runMysqlInit() {
|
||||||
|
if (syncRunning || mysqlInitRunning) return;
|
||||||
|
const confirmed = window.confirm(
|
||||||
|
"初始化外部 MySQL 会创建数据库,并重建 mobilemodels 相关表、视图和 seed 数据。是否继续?"
|
||||||
|
);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
mysqlInitRunning = true;
|
||||||
|
updateSyncButtons();
|
||||||
|
syncStatusEl.textContent = "正在初始化外部 MySQL,请稍候。";
|
||||||
|
syncLogEl.textContent = "MySQL 初始化进行中...";
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchJson("/api/init-mysql", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: "{}",
|
||||||
|
});
|
||||||
|
syncSupported = true;
|
||||||
|
syncStatusEl.textContent = "外部 MySQL 初始化完成。";
|
||||||
|
renderSyncLog(data, "MySQL 初始化完成");
|
||||||
|
} catch (err) {
|
||||||
|
syncStatusEl.textContent = `MySQL 初始化失败: ${err.message}`;
|
||||||
|
syncLogEl.textContent = `MySQL 初始化失败\n${err.message}`;
|
||||||
|
} finally {
|
||||||
|
mysqlInitRunning = false;
|
||||||
|
await loadSyncStatus({ preserveLog: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveMysqlSettings() {
|
||||||
|
mysqlSettingsSaving = true;
|
||||||
|
updateSyncButtons();
|
||||||
|
mysqlSettingsStatusEl.textContent = "正在保存 MySQL 自动装载设置...";
|
||||||
|
try {
|
||||||
|
const payload = await fetchJson("/api/mysql-settings", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
auto_load: !!mysqlAutoLoadEnabledEl.checked,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
mysqlSettingsStatusEl.textContent = payload.message || "MySQL 自动装载设置已保存。";
|
||||||
|
await loadSyncStatus({ preserveLog: true, preserveMysqlSettingsMessage: true });
|
||||||
|
renderMysqlSettingsStatus(payload);
|
||||||
|
} catch (err) {
|
||||||
|
mysqlSettingsStatusEl.textContent = `保存失败: ${err.message}`;
|
||||||
|
} finally {
|
||||||
|
mysqlSettingsSaving = false;
|
||||||
|
updateSyncButtons();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveSyncSchedule() {
|
async function saveSyncSchedule() {
|
||||||
scheduleSaving = true;
|
scheduleSaving = true;
|
||||||
updateSyncButtons();
|
updateSyncButtons();
|
||||||
@@ -1348,7 +1450,9 @@
|
|||||||
brandCountBtnEl.addEventListener("click", openBrandListModal);
|
brandCountBtnEl.addEventListener("click", openBrandListModal);
|
||||||
manufacturerCountBtnEl.addEventListener("click", openManufacturerListModal);
|
manufacturerCountBtnEl.addEventListener("click", openManufacturerListModal);
|
||||||
syncUpstreamBtnEl.addEventListener("click", runUpstreamSync);
|
syncUpstreamBtnEl.addEventListener("click", runUpstreamSync);
|
||||||
|
initMysqlBtnEl.addEventListener("click", runMysqlInit);
|
||||||
refreshSyncStatusBtnEl.addEventListener("click", loadSyncStatus);
|
refreshSyncStatusBtnEl.addEventListener("click", loadSyncStatus);
|
||||||
|
saveMysqlSettingsBtnEl.addEventListener("click", saveMysqlSettings);
|
||||||
saveSyncScheduleBtnEl.addEventListener("click", saveSyncSchedule);
|
saveSyncScheduleBtnEl.addEventListener("click", saveSyncSchedule);
|
||||||
reloadIndexBtnEl.addEventListener("click", loadIndexFromPath);
|
reloadIndexBtnEl.addEventListener("click", loadIndexFromPath);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user