From fa82b8621e6f456ac7c469590ca6c8e52f1bff0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=80=BB=E5=B7=A5?= Date: Wed, 16 Sep 2026 17:24:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(HEL-566):=20=E5=8E=BB=E6=8E=89=E4=B8=BB?= =?UTF-8?q?=E7=AB=99=E8=A1=8C=E6=83=85=E7=AE=A1=E7=90=86=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E5=B9=B6=E4=BF=AE=E5=A5=BD=E6=A8=A1=E5=9E=8B=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=94=B6=E8=B5=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主站行情管理并入数据中枢 - 数据中枢数据源配置页新增「主站行情任务」卡:交易时段后台刷新开关、 手动后台刷新、历史区间回补(任务提交 + 轮询主站 job 记录亮灯), 数据与调度仍归主站,控制台只经桥接代操作 - 新增桥接端点 /api/hub-admin/market/{refresh,backfill};回补改为 market.backfill 后台任务(jobs.config.json 注册,锁独立,超时 30 分钟), 桥接调用秒回,不再阻塞 - 主站桌面端删除顶栏「行情管理」按钮与整个行情管理对话框,清理随之 失效的 CSS;移动端删除 system/admin 页与入口,管理员专区只留数据中枢 - Tushare Token / iFinD 凭证编辑沿用数据源页既有凭证区,无功能缺失 模型池收起修复 - 拉出模型清单后按钮切换为「收起列表」,收起只留一行摘要;再次点击 重新拉取并展开;勾选添加完成后清单自动收起(原逻辑保留) - 交互全部沿用 HEL-558 已确认样式的既有按钮与提示组件,未新增视觉 自测 - verify_baseline 通过;pytest 492 项通过;数据中枢 240 项通过 - verify_datahub_console 新增 [11b] 行情任务桥接端到端段;UI 自测新增 行情任务卡开关往返、模型清单展开/收起/再展开/添加自动收起,日夜主题 与 1030 窄屏复验通过 - Playwright e2e 102/103:唯一失败项在基线提交上同样失败(本机字体度量 导致的头部溢出,与本卡无关) Co-authored-by: multica-agent --- README.md | 2 +- backend/http/dispatch.py | 2 + backend/http/hubadmin.py | 39 +++++ backend/jobs/service.py | 17 ++ config/architecture-inventory.json | 68 ++++---- config/jobs.config.json | 10 ++ frontend/app.js | 4 - frontend/index.html | 30 ---- frontend/m/config/nav.config.js | 3 +- frontend/m/css/features.css | 38 ----- frontend/m/js/pages.js | 148 +--------------- frontend/shared/admin.js | 97 +---------- frontend/shared/auth.css | 111 +----------- frontend/shared/context.js | 1 - frontend/shared/dashboard.js | 21 +-- frontend/shared/session.js | 1 - tests/e2e/app-shell.spec.js | 24 --- tests/e2e/mobile-app.spec.js | 37 ++-- tests/test_frontend_boundaries.py | 4 +- tests/test_frontend_contract.py | 15 +- tests/test_http_dispatch.py | 2 +- tests/test_mobile_system_pages.py | 14 +- tools/verify_datahub_console.py | 34 ++++ tools/verify_datahub_console_ui.py | 89 ++++++++++ xiaobai-datahub/admin/app.js | 189 ++++++++++++++++++++- xiaobai-datahub/datahub/httpapp.py | 12 ++ xiaobai-datahub/datahub/siteconsole.py | 48 ++++++ xiaobai-datahub/tests/test_site_console.py | 54 ++++++ 28 files changed, 563 insertions(+), 551 deletions(-) diff --git a/README.md b/README.md index f8e40a3..41c81f3 100644 --- a/README.md +++ b/README.md @@ -117,7 +117,7 @@ compose.yaml .env.example 环境变量模板(复制为 .env 后填写) ``` -管理员的配置入口在数据中枢(页面右上角「数据中枢」按钮,固定指向 `http://192.168.200.11:8766/admin/`):数据源凭证、模型池、会员与邀请码都在那里维护,数据仍存在主站同一份 SQLite 里。主站自身只保留「行情管理」面板(Tushare Token、后台刷新开关、手动刷新与补数)。所有用户读取同一份 SQLite 行情快照。`.env` 中的 Tushare 和平台 LLM 配置只用于初始化系统配置。 +管理员的配置入口在数据中枢(页面右上角「数据中枢」按钮,固定指向 `http://192.168.200.11:8766/admin/`):数据源凭证、模型池、会员与邀请码都在那里维护,主站行情任务(后台刷新开关、手动刷新与历史回补)在数据中枢「数据源配置」页的行情任务卡维护,数据仍存在主站同一份 SQLite 里,主站不再保留单独的「行情管理」入口。所有用户读取同一份 SQLite 行情快照。`.env` 中的 Tushare 和平台 LLM 配置只用于初始化系统配置。 数据中枢用主站的管理员账号进入,没有独立账号;桥接令牌 `HUB_ADMIN_TOKEN` 需在主站与中枢两侧 `.env` 填成同一个值。详见 [xiaobai-datahub/README.md](xiaobai-datahub/README.md)。 diff --git a/backend/http/dispatch.py b/backend/http/dispatch.py index 3da4fd7..4526d7f 100644 --- a/backend/http/dispatch.py +++ b/backend/http/dispatch.py @@ -22,6 +22,8 @@ HUB_SERVICE_HANDLERS = { "/api/hub-admin/settings/save": "hub_save_settings", "/api/hub-admin/settings/test": "hub_test_model", "/api/hub-admin/models/fetch": "hub_fetch_models", + "/api/hub-admin/market/refresh": "hub_market_refresh", + "/api/hub-admin/market/backfill": "hub_market_backfill", "/api/hub-admin/members": "hub_members", "/api/hub-admin/membership/save": "hub_save_membership", "/api/hub-admin/invites": "hub_invites", diff --git a/backend/http/hubadmin.py b/backend/http/hubadmin.py index 7b3d64d..28dbf2c 100644 --- a/backend/http/hubadmin.py +++ b/backend/http/hubadmin.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +from datetime import date from http import HTTPStatus from backend.features.accounts.security import token_hash, verify_password @@ -111,6 +112,44 @@ class HubAdminHttpMixin: except (ValueError, json.JSONDecodeError) as exc: self._hub_failure(exc) + def hub_market_refresh(self) -> None: + """HEL-566: manual snapshot refresh, submitted as a tracked job.""" + try: + body = self._hub_body() + trade_date = str(body.get("trade_date") or "") or date.today().isoformat() + refresh = self.application_service.request_background_sync(trade_date) + started = bool(refresh.get("started")) + self.send_json( + { + "ok": True, + "started": started, + "job_key": str(refresh.get("job_key") or ""), + "message": "后台刷新已开始" if started else "已有后台刷新任务正在运行", + } + ) + except (ValueError, json.JSONDecodeError) as exc: + self._hub_failure(exc) + + def hub_market_backfill(self) -> None: + """HEL-566: historical snapshot backfill, submitted as a tracked job.""" + try: + body = self._hub_body() + result = self.application_service.request_market_backfill( + str(body.get("start_date") or ""), + str(body.get("end_date") or ""), + ) + started = bool(result.get("started")) + self.send_json( + { + "ok": True, + "started": started, + "job_key": str(result.get("job_key") or ""), + "message": "历史回补任务已开始" if started else "已有回补任务正在运行", + } + ) + except (ValueError, json.JSONDecodeError) as exc: + self._hub_failure(exc) + def hub_members(self) -> None: service = self.application_service self.send_json( diff --git a/backend/jobs/service.py b/backend/jobs/service.py index 6be63cb..dae86f7 100644 --- a/backend/jobs/service.py +++ b/backend/jobs/service.py @@ -32,6 +32,23 @@ class JobServiceMixin: ) return {"started": started, "job_key": key if started else ""} + def request_market_backfill(self, start_date: str, end_date: str) -> dict[str, object]: + """HEL-566: submit the admin snapshot backfill as a tracked job. + + The data hub console triggers this over the service bridge; the work + itself can run for minutes, so it must not block the bridge call. + """ + normalized_start = normalize_date(start_date) + normalized_end = normalize_date(end_date) + key = f"backfill:{normalized_start}:{normalized_end}:{time.time_ns()}" + started = self.jobs.submit( + "market.backfill", + key, + lambda: self.backfill(normalized_start, normalized_end), + {"start_date": normalized_start, "end_date": normalized_end, "trigger": "administrator"}, + ) + return {"started": started, "job_key": key if started else ""} + def _background_refresh_tick(self) -> None: if not ( self.configured diff --git a/config/architecture-inventory.json b/config/architecture-inventory.json index 2867a3f..dc0d670 100644 --- a/config/architecture-inventory.json +++ b/config/architecture-inventory.json @@ -479,8 +479,8 @@ }, { "path": "frontend/index.html", - "bytes": 46900, - "lines": 638 + "bytes": 44025, + "lines": 608 }, { "path": "backend/data/providers/tushare_industries.py", @@ -547,11 +547,6 @@ "bytes": 15743, "lines": 401 }, - { - "path": "frontend/shared/dashboard.js", - "bytes": 15063, - "lines": 321 - }, { "path": "frontend/pages/pools/page.html", "bytes": 14942, @@ -567,10 +562,15 @@ "bytes": 14467, "lines": 356 }, + { + "path": "frontend/shared/dashboard.js", + "bytes": 13708, + "lines": 304 + }, { "path": "frontend/shared/session.js", - "bytes": 13633, - "lines": 296 + "bytes": 13567, + "lines": 295 }, { "path": "backend/features/market/insights_auction_data.py", @@ -642,16 +642,16 @@ "bytes": 6739, "lines": 156 }, - { - "path": "frontend/shared/context.js", - "bytes": 6547, - "lines": 220 - }, { "path": "backend/application.py", "bytes": 6500, "lines": 164 }, + { + "path": "frontend/shared/context.js", + "bytes": 6492, + "lines": 219 + }, { "path": "frontend/pages/sentiment/page.html", "bytes": 6488, @@ -682,6 +682,11 @@ "bytes": 5592, "lines": 123 }, + { + "path": "backend/http/dispatch.py", + "bytes": 5401, + "lines": 141 + }, { "path": "frontend/pages.config.js", "bytes": 5385, @@ -697,11 +702,6 @@ "bytes": 5350, "lines": 74 }, - { - "path": "backend/http/dispatch.py", - "bytes": 5281, - "lines": 139 - }, { "path": "frontend/shared/feedback.js", "bytes": 5157, @@ -717,11 +717,6 @@ "bytes": 4712, "lines": 106 }, - { - "path": "frontend/shared/admin.js", - "bytes": 4500, - "lines": 109 - }, { "path": "backend/data/providers/tushare_helpers.py", "bytes": 4406, @@ -757,11 +752,6 @@ "bytes": 3369, "lines": 81 }, - { - "path": "frontend/app.js", - "bytes": 3337, - "lines": 95 - }, { "path": "frontend/pages/themes/page.html", "bytes": 3316, @@ -772,11 +762,21 @@ "bytes": 3299, "lines": 57 }, + { + "path": "backend/jobs/service.py", + "bytes": 3186, + "lines": 76 + }, { "path": "backend/features/market/insights_context.py", "bytes": 3175, "lines": 84 }, + { + "path": "frontend/app.js", + "bytes": 3081, + "lines": 91 + }, { "path": "frontend/pages/market/bindings.js", "bytes": 2663, @@ -787,11 +787,6 @@ "bytes": 2597, "lines": 69 }, - { - "path": "backend/jobs/service.py", - "bytes": 2337, - "lines": 59 - }, { "path": "backend/features/mentor/routes.py", "bytes": 2299, @@ -887,6 +882,11 @@ "bytes": 817, "lines": 23 }, + { + "path": "frontend/shared/admin.js", + "bytes": 800, + "lines": 18 + }, { "path": "backend/features/sentiment/routes.py", "bytes": 724, diff --git a/config/jobs.config.json b/config/jobs.config.json index 9d1cbf5..1ba7dde 100644 --- a/config/jobs.config.json +++ b/config/jobs.config.json @@ -21,6 +21,16 @@ "max_attempts": 1, "output_version": "screener-library-v8" }, + { + "id": "market.backfill", + "schedule": "administrator request from the data hub console", + "input_date_policy": "requested start and end trade dates", + "dependencies": ["market provider", "trade calendar", "database"], + "lock_key": "market-backfill", + "timeout_seconds": 1800, + "max_attempts": 1, + "output_version": "snapshot-backfill-v1" + }, { "id": "market.ifind-event-enrichment", "schedule": "on demand after market close", diff --git a/frontend/app.js b/frontend/app.js index 90fbb7f..4f18651 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -13,10 +13,6 @@ async function initialize() { document.querySelector("#journalDate").max = todayString(); document.querySelector("#tradeLogDate").value = elements.tradeDate.value; document.querySelector("#tradeLogDate").max = todayString(); - document.querySelector("#backfillStart").value = todayString(); - document.querySelector("#backfillEnd").value = todayString(); - document.querySelector("#backfillStart").max = todayString(); - document.querySelector("#backfillEnd").max = todayString(); document.querySelector("#qiObservationDate").value = elements.tradeDate.value; document.querySelector("#qiObservationDate").max = todayString(); document.querySelector("#accountBirthDate").max = todayString(); diff --git a/frontend/index.html b/frontend/index.html index e46599a..2d1621b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -157,7 +157,6 @@ -