Skip to content

优化了一些内容 - #658

Merged
wess09 merged 2 commits into
masterfrom
dev
Jul 29, 2026
Merged

优化了一些内容#658
wess09 merged 2 commits into
masterfrom
dev

Conversation

@wess09

@wess09 wess09 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Summary by Sourcery

自动检测部署位置以选择合适的更新源,并通过自托管、带缓存破坏参数的资源来强化 Web UI 静态资源的交付。

New Features:

  • 引入地理位置查询工具,用于检测当前公网 IP 所在国家,并通过辅助函数对外暴露。
  • 当在中国大陆网络环境中运行时,自动从官方 GitHub 仓库切换到 CDN 镜像和备用仓库。

Enhancements:

  • 优化部署配置默认值,使用共享的 GitHub 仓库常量,并避免在单个进程中重复进行位置检测。
  • 通过为公共资源和文档目录显式挂载的方式提供 Web UI 静态资源,而不是暴露整个工作目录。
  • 为初始 CSS 资源添加内容哈希查询参数,并调整 cache-control 头部,确保静态资源始终被重新验证。
  • 将 PyWebIO 默认切换为使用自托管资源,而非外部 CDN,并移除对 Google Fonts 及其他外部字体服务的依赖。
  • 规范静态资源和 PWA manifest 的路径为相对路径,以便在反向代理和非根路径环境下正常工作。

Build:

  • 调整 Python 包索引配置,在保留选定镜像条目的同时,优先使用官方 PyPI 索引。

Documentation:

  • 更新 Web UI 主题和 OBS 叠加层样式,以使用本地可用字体和相对资源 URL,而不是远程字体提供方。

Tests:

  • 新增测试,覆盖基于地理位置的国家检测以及其与部署配置中仓库选择逻辑的交互。
  • 新增测试,确保在 Web UI 服务器下,静态资源挂载、缓存头、相对 URL 以及自托管主题资源的行为符合预期。
Original summary in English

Summary by Sourcery

Auto-detect deployment location to choose appropriate update sources and harden Web UI static asset delivery with self-hosted, cache-busted resources.

New Features:

  • Introduce geo lookup utility to detect the current public IP's country and expose it via a helper function.
  • Automatically switch from the official GitHub repository to a CDN mirror and fallback repository when running from mainland China networks.

Enhancements:

  • Refine deploy configuration defaults to use a shared GitHub repository constant and avoid repeated location checks per process.
  • Serve Web UI static assets via explicit mounts for public asset and documentation directories instead of exposing the working directory.
  • Add content-hash query strings to initial CSS assets and adjust cache-control headers to ensure static resources are always revalidated.
  • Default PyWebIO to self-hosted assets instead of external CDNs and remove dependencies on Google Fonts and other external font services.
  • Normalize static asset and PWA manifest paths to be relative so they work correctly behind reverse proxies and under non-root paths.

Build:

  • Adjust Python package index configuration to prefer the official PyPI index while keeping selected mirror entries.

Documentation:

  • Update Web UI theme and OBS overlay styling to use locally available fonts and relative asset URLs instead of remote font providers.

Tests:

  • Add tests covering geo-based country detection and its interaction with deploy configuration repository selection.
  • Add tests ensuring static asset mounts, cache headers, relative URLs, and self-hosted theme resources behave as expected under the Web UI server.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@wess09
wess09 merged commit 8e7fdf9 into master Jul 29, 2026
8 checks passed
@sourcery-ai

sourcery-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

添加基于地理位置的 GitHub 仓库重定向,将 WebUI 静态资源处理切换为自托管、可缓存且对反向代理友好的路径,移除外部字体/CDN 依赖,并新增测试以覆盖与位置相关的部署行为和静态资源服务。

基于地理位置的 GitHub 仓库重定向时序图

sequenceDiagram
    actor User
    participant ConfigModel
    participant geo as get_country_code
    participant logger

    User->>ConfigModel: __init__(file)
    ConfigModel->>ConfigModel: read()
    ConfigModel->>ConfigModel: config_redirect()
    ConfigModel->>ConfigModel: _redirect_github_repository()
    alt _github_location_checked or Repository != GITHUB_REPOSITORY
        ConfigModel-->>ConfigModel: return
    else first_time_and_github
        ConfigModel->>geo: get_country_code(timeout)
        geo-->>ConfigModel: country_code
        alt country_code == 'cn'
            ConfigModel->>logger: info
            ConfigModel->>ConfigModel: Repository = GIT_OVER_CDN_REPOSITORY
            ConfigModel->>ConfigModel: config['Repository'] = GIT_OVER_CDN_REPOSITORY
        else country_code is None
            ConfigModel->>logger: warning
        else other_country
            ConfigModel->>logger: info
        end
    end
Loading

File-Level Changes

Change Details Files
引入具备地理感知的 Git 仓库选择逻辑,并重构部署配置以使用共享的 GitHub 仓库常量和一次性位置检测。
  • 添加 deploy.geo.get_country_code 助手函数,请求 ip9.com.cn 并规范化 ISO 国家代码。
  • 重构 deploydeploy.Windows 配置,使其引用共享的 GITHUB_REPOSITORY 常量,而不是硬编码 GitHub URL。
  • 扩展 DeployConfig.__init__ 以跟踪是否已检查 GitHub 所在地,并在 config_redirect 中调用新的 _redirect_github_repository 助手。
  • 实现 _redirect_github_repository:对于 CN 切换到 GIT_OVER_CDN_REPOSITORY,对非 CN 或失败情况保持使用 GitHub,并记录日志结果。
deploy/Windows/config.py
deploy/config.py
deploy/geo.py
重构 WebUI 静态资源挂载与 URL,使其为相对路径、可安全缓存,并兼容反向代理和自托管的 pywebio 资源。
  • 引入 PROJECT_ROOT_versioned_static_asset,用于为初始 WebUI 样式计算带内容哈希的 CSS URL。
  • 将单一的 static_dir 挂载替换为显式的 /static/assets/static/doc 挂载,并通过 static_mounts 传入 asgi_app/start_server
  • 修改 asgi_appstart_server 的签名以接收 static_mounts,默认 cdn=False,并注册额外的 StaticFiles 挂载。
  • 调整各类 HTML/CSS/JS 引用(manifest、字体、logo、脚本、占位符),使用不带前导斜杠的相对静态路径,从而遵守反向代理前缀。
module/webui/app.py
module/webui/app_home.py
module/webui/fastapi.py
module/webui/setting.py
assets/spa/manifest.json
assets/gui/css/alas.css
从主题和叠加层资源中移除外部字体与背景依赖,并收紧静态资源缓存行为。
  • advanced-material-alas.css 中移除 Google Fonts 和外部背景图片导入,将背景图片指向在静态资源下提供的本地 bj.jpg
  • 更新 obs_overlay.html,改用系统/本地字体而非托管在 Google 的字体。
  • STATIC_ASSET_CACHE_CONTROL 修改为 no-cache,以强制对非哈希静态资源进行校验。
  • 清理 pyproject.toml 的镜像列表格式,并移除 CERNET 镜像条目。
module/webui/fastapi.py
module/webui/obs_overlay.html
assets/gui/css/advanced-material-alas.css
pyproject.toml
新增测试,确保基于地理位置的部署行为、静态资源挂载,以及外部字体/CDN 依赖的移除。
  • 新增针对 deploy.geo.get_country_code 行为的测试,以及其在不同国家代码场景下与 deploy 和 Windows DeployConfig.config_redirect 的交互。
  • 引入 WebUI 静态资源测试,验证仅公开公共目录、相对 URL 能保留反向代理前缀、pywebio 资源为自托管,以及主题/叠加层资源不再依赖外部字体/背景。
  • 在测试中使用临时目录模拟项目根目录,以验证 StaticFiles 挂载和缓存头。
tests/test_deploy_location.py
tests/test_webui_static_assets.py

Tips and commands

Interacting with Sourcery

  • 触发新评审: 在拉取请求中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的评审评论。
  • 从评审评论生成 GitHub issue: 通过回复评审评论来让 Sourcery 基于该评论创建 issue。你也可以在评审评论中回复 @sourcery-ai issue 来创建 issue。
  • 生成拉取请求标题: 在拉取请求标题中任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在拉取请求中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成拉取请求摘要: 在拉取请求正文中任意位置写上 @sourcery-ai summary,即可在该位置生成 PR 摘要。你也可以在拉取请求中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审阅指南: 在拉取请求中评论 @sourcery-ai guide,即可随时(重新)生成审阅指南。
  • 解决所有 Sourcery 评论: 在拉取请求中评论 @sourcery-ai resolve,以解决所有 Sourcery 评论。若你已经处理完所有评论并且不想再看到它们,这会非常有用。
  • 撤销所有 Sourcery 评审: 在拉取请求中评论 @sourcery-ai dismiss,以撤销所有现有的 Sourcery 评审。特别适用于你想从头开始新的评审时——别忘了评论 @sourcery-ai review 来触发新的评审!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用评审功能,例如 Sourcery 生成的拉取请求摘要、审阅指南等。
  • 更改评审语言。
  • 添加、移除或编辑自定义评审指令。
  • 调整其他评审设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds geo-based GitHub repository redirection, switches WebUI static asset handling to self-hosted, cache-safe, reverse-proxy-friendly paths, removes external font/CDN dependencies, and introduces tests to cover location-aware deploy behavior and static asset serving.

Sequence diagram for geo-based GitHub repository redirection

sequenceDiagram
    actor User
    participant ConfigModel
    participant geo as get_country_code
    participant logger

    User->>ConfigModel: __init__(file)
    ConfigModel->>ConfigModel: read()
    ConfigModel->>ConfigModel: config_redirect()
    ConfigModel->>ConfigModel: _redirect_github_repository()
    alt _github_location_checked or Repository != GITHUB_REPOSITORY
        ConfigModel-->>ConfigModel: return
    else first_time_and_github
        ConfigModel->>geo: get_country_code(timeout)
        geo-->>ConfigModel: country_code
        alt country_code == 'cn'
            ConfigModel->>logger: info
            ConfigModel->>ConfigModel: Repository = GIT_OVER_CDN_REPOSITORY
            ConfigModel->>ConfigModel: config['Repository'] = GIT_OVER_CDN_REPOSITORY
        else country_code is None
            ConfigModel->>logger: warning
        else other_country
            ConfigModel->>logger: info
        end
    end
Loading

File-Level Changes

Change Details Files
Introduce geo-aware Git repository selection and refactor deploy config to use a shared GitHub repository constant with one-time location checking.
  • Add deploy.geo.get_country_code helper that queries ip9.com.cn and normalizes ISO country codes.
  • Refactor deploy and deploy.Windows config to reference a shared GITHUB_REPOSITORY constant instead of hardcoding the GitHub URL.
  • Extend DeployConfig.init to track whether GitHub location has been checked, and invoke a new _redirect_github_repository helper from config_redirect.
  • Implement _redirect_github_repository to switch to GIT_OVER_CDN_REPOSITORY for CN, keep GitHub for non-CN or failures, and log outcomes.
deploy/Windows/config.py
deploy/config.py
deploy/geo.py
Rework WebUI static asset mounting and URLs to be relative, cache-safe, and compatible with reverse proxies and self-hosted pywebio assets.
  • Introduce PROJECT_ROOT and _versioned_static_asset to compute content-hashed CSS URLs for initial WebUI styles.
  • Replace single static_dir mount with explicit /static/assets and /static/doc mounts passed into asgi_app/start_server via static_mounts.
  • Change asgi_app and start_server signatures to accept static_mounts, default cdn to False, and register additional StaticFiles mounts.
  • Adjust various HTML/CSS/JS references (manifest, fonts, logos, scripts, placeholders) to use relative static paths without leading slashes so they honor reverse-proxy prefixes.
module/webui/app.py
module/webui/app_home.py
module/webui/fastapi.py
module/webui/setting.py
assets/spa/manifest.json
assets/gui/css/alas.css
Remove external font and background dependencies from theme and overlay assets and tighten static asset cache behavior.
  • Remove Google Fonts and external background image imports from advanced-material-alas.css and point background image to a local bj.jpg served under static assets.
  • Update obs_overlay.html to use system/local fonts instead of Google-hosted fonts.
  • Change STATIC_ASSET_CACHE_CONTROL to no-cache to force validation for non-hashed static resources.
  • Clean up pyproject.toml mirror list formatting and drop the CERNET mirror entry.
module/webui/fastapi.py
module/webui/obs_overlay.html
assets/gui/css/advanced-material-alas.css
pyproject.toml
Add tests ensuring geo-based deploy behavior, static asset mounting, and removal of external font/CDN dependencies.
  • Add tests for deploy.geo.get_country_code behavior and its interaction with deploy and Windows DeployConfig.config_redirect under different country code scenarios.
  • Introduce WebUI static asset tests that verify only public directories are exposed, relative URLs preserve reverse proxy prefixes, pywebio assets are self-hosted, and theme/overlay assets have no external font/background dependencies.
  • Use temporary directories in tests to simulate project root and validate StaticFiles mounts and cache headers.
tests/test_deploy_location.py
tests/test_webui_static_assets.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 3 个问题,并给出了一些高层次的反馈:

  • _redirect_github_repository 逻辑同时出现在 deploy/config.pydeploy/Windows/config.py 中;建议抽取一个共享的辅助函数(例如放在 deploy.geo 或一个小的公共模块里),以避免未来更新该行为时,不同平台之间产生偏差。
  • _versioned_static_asset 辅助函数在导入时直接读取资源文件并计算哈希,而且没有错误处理;如果文件缺失或不可读(例如在打包/捆绑环境中),导入 module.webui.app 时会抛异常——建议改为惰性求值,或者用 try/except 包裹 IO 并提供合理的回退路径。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_redirect_github_repository` logic is duplicated in both `deploy/config.py` and `deploy/Windows/config.py`; consider extracting a shared helper (e.g., in `deploy.geo` or a small common module) to avoid drift between platforms when this behavior is updated in future.
- The `_versioned_static_asset` helper reads asset files and computes hashes at import time without error handling; if the file is missing or unreadable (e.g., in a packaged/bundled environment), importing `module.webui.app` will raise—consider lazy evaluation or wrapping the IO in a try/except with a sensible fallback path.

## Individual Comments

### Comment 1
<location path="module/webui/app.py" line_range="84-87" />
<code_context>
+PROJECT_ROOT = Path(__file__).resolve().parents[2]
+
+
+def _versioned_static_asset(relative_path: str) -> str:
+    """返回带内容哈希的相对静态资源地址。"""
+    digest = sha256((PROJECT_ROOT / relative_path).read_bytes()).hexdigest()[:12]
+    return f"static/{relative_path}?v={digest}"
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle missing/invalid asset files in _versioned_static_asset to avoid startup-time crashes.

If this file doesn’t exist or the path is wrong, this will raise (e.g. `FileNotFoundError`) when `INITIAL_WEBUI_CSS` is resolved and can prevent the app from starting. Please catch file/IO errors here and either fall back to an unversioned path or return/log a safe default so a missing asset doesn’t break the web UI.
</issue_to_address>

### Comment 2
<location path="tests/test_webui_static_assets.py" line_range="43-52" />
<code_context>
+    def tearDown(self):
+        self._temporary_directory.cleanup()
+
+    def test_mounts_only_public_static_directories(self):
+        css_response = self.client.get("/static/assets/gui/css/test.css")
+        logo_response = self.client.get("/static/doc/logo.webp")
+
+        self.assertEqual(css_response.status_code, 200)
+        self.assertEqual(css_response.headers["content-type"], "text/css; charset=utf-8")
+        self.assertEqual(css_response.headers["cache-control"], "no-cache")
+        self.assertEqual(logo_response.status_code, 200)
+        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
+        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)
+
+    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
</code_context>
<issue_to_address>
**suggestion (testing):** Also assert cache-control headers for non-CSS assets and tightening static asset expectations

Since the middleware sets `Cache-Control: no-cache` for cacheable static assets, please also assert `logo_response.headers["cache-control"] == "no-cache"` to cover binary assets. Additionally, consider a focused test for the versioned CSS helper (e.g. calling `_versioned_static_asset` on a test file and asserting it returns a relative URL with a `?v=` query parameter of the expected length) to directly validate the cache-busting behavior.

Suggested implementation:

```python
        self.assertEqual(css_response.headers["cache-control"], "no-cache")
        self.assertEqual(logo_response.status_code, 200)
        self.assertEqual(logo_response.headers["cache-control"], "no-cache")
        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)

    def test_versioned_static_asset_adds_cache_busting_query_param(self):
        # Use a representative CSS asset path that exists in the test assets directory
        path = "assets/gui/css/test.css"

        versioned_path = _versioned_static_asset(path)

        # The helper should return a relative URL under /static that includes the original path
        self.assertTrue(versioned_path.startswith("static/"))
        self.assertIn(path, versioned_path)

        # Expect a cache-busting query parameter named "v" with a non-empty value
        self.assertIn("?v=", versioned_path)
        version = versioned_path.split("?v=", 1)[1]
        # Assert a minimum length to ensure it's not just an empty or trivial token
        self.assertGreaterEqual(len(version), 8)

    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
        css_url = urljoin(

```

1. Ensure `_versioned_static_asset` is imported or otherwise available in this test module, e.g.:
   - `from <your_webui_module> import _versioned_static_asset`
   Adjust `<your_webui_module>` to match the actual module that defines the helper.
2. If the helper returns URLs with a different prefix (e.g. `/static/` instead of `static/`), update the `startswith` assertion accordingly.
3. If the implementation uses a fixed-length version token (e.g. 12 or 16 characters), you may tighten the `len(version)` assertion to match that exact length rather than a minimum of 8.
4. Confirm that `"assets/gui/css/test.css"` corresponds to an existing file in the test assets directory; if not, adjust `path` to a file that is present so the helper behaves as expected.
</issue_to_address>

### Comment 3
<location path="tests/test_webui_static_assets.py" line_range="69-74" />
<code_context>
+        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
+        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)
+
+    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
+        css_url = urljoin(
+            "https://example.test/azur/", "static/assets/gui/css/alas.css"
+        )
+        font_url = urljoin(css_url, "../../spa/MiSans-Demibold.ttf")
+        logo_url = urljoin(css_url, "../../../doc/logo.webp")
+
+        self.assertEqual(
+            css_url, "https://example.test/azur/static/assets/gui/css/alas.css"
+        )
+        self.assertEqual(
+            font_url, "https://example.test/azur/static/assets/spa/MiSans-Demibold.ttf"
+        )
+        self.assertEqual(logo_url, "https://example.test/azur/static/doc/logo.webp")
+
+    def test_default_pywebio_assets_are_self_hosted(self):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding tests for manifest and JS asset relative URLs injected by the Web UI

You’re already covering relative paths in CSS. There’s similar logic in `app_home._load_deferred_client_assets` that switches the PWA manifest and JS bundle from `/static/...` to `static/...`. To cover reverse proxies and non-root paths end-to-end, consider a functional test that renders the index page (or triggers the PyWebIO injection) and asserts that the `link[rel="manifest"]` and `alas-utils-script` tags use `static/...` URLs rather than `/static/...`.

```suggestion
    def test_default_pywebio_assets_are_self_hosted(self):
        response = TestClient(asgi_app({"index": lambda: None})).get("/")

        self.assertEqual(response.status_code, 200)
        self.assertIn('href="pywebio_static/css/app.css?v=', response.text)
        self.assertNotIn("cdn.jsdelivr.net", response.text)

        # PWA manifest should be injected with a relative static path, not an absolute /static path
        self.assertIn('rel="manifest"', response.text)
        self.assertIn('href="static/', response.text)
        self.assertNotIn('href="/static/', response.text)

        # Deferred JS bundle should also use a relative static path
        self.assertIn('id="alas-utils-script"', response.text)
        self.assertIn('src="static/', response.text)
        self.assertNotIn('src="/static/', response.text)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Original comment in English

Hey - I've found 3 issues, and left some high level feedback:

  • The _redirect_github_repository logic is duplicated in both deploy/config.py and deploy/Windows/config.py; consider extracting a shared helper (e.g., in deploy.geo or a small common module) to avoid drift between platforms when this behavior is updated in future.
  • The _versioned_static_asset helper reads asset files and computes hashes at import time without error handling; if the file is missing or unreadable (e.g., in a packaged/bundled environment), importing module.webui.app will raise—consider lazy evaluation or wrapping the IO in a try/except with a sensible fallback path.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `_redirect_github_repository` logic is duplicated in both `deploy/config.py` and `deploy/Windows/config.py`; consider extracting a shared helper (e.g., in `deploy.geo` or a small common module) to avoid drift between platforms when this behavior is updated in future.
- The `_versioned_static_asset` helper reads asset files and computes hashes at import time without error handling; if the file is missing or unreadable (e.g., in a packaged/bundled environment), importing `module.webui.app` will raise—consider lazy evaluation or wrapping the IO in a try/except with a sensible fallback path.

## Individual Comments

### Comment 1
<location path="module/webui/app.py" line_range="84-87" />
<code_context>
+PROJECT_ROOT = Path(__file__).resolve().parents[2]
+
+
+def _versioned_static_asset(relative_path: str) -> str:
+    """返回带内容哈希的相对静态资源地址。"""
+    digest = sha256((PROJECT_ROOT / relative_path).read_bytes()).hexdigest()[:12]
+    return f"static/{relative_path}?v={digest}"
+
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Handle missing/invalid asset files in _versioned_static_asset to avoid startup-time crashes.

If this file doesn’t exist or the path is wrong, this will raise (e.g. `FileNotFoundError`) when `INITIAL_WEBUI_CSS` is resolved and can prevent the app from starting. Please catch file/IO errors here and either fall back to an unversioned path or return/log a safe default so a missing asset doesn’t break the web UI.
</issue_to_address>

### Comment 2
<location path="tests/test_webui_static_assets.py" line_range="43-52" />
<code_context>
+    def tearDown(self):
+        self._temporary_directory.cleanup()
+
+    def test_mounts_only_public_static_directories(self):
+        css_response = self.client.get("/static/assets/gui/css/test.css")
+        logo_response = self.client.get("/static/doc/logo.webp")
+
+        self.assertEqual(css_response.status_code, 200)
+        self.assertEqual(css_response.headers["content-type"], "text/css; charset=utf-8")
+        self.assertEqual(css_response.headers["cache-control"], "no-cache")
+        self.assertEqual(logo_response.status_code, 200)
+        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
+        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)
+
+    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
</code_context>
<issue_to_address>
**suggestion (testing):** Also assert cache-control headers for non-CSS assets and tightening static asset expectations

Since the middleware sets `Cache-Control: no-cache` for cacheable static assets, please also assert `logo_response.headers["cache-control"] == "no-cache"` to cover binary assets. Additionally, consider a focused test for the versioned CSS helper (e.g. calling `_versioned_static_asset` on a test file and asserting it returns a relative URL with a `?v=` query parameter of the expected length) to directly validate the cache-busting behavior.

Suggested implementation:

```python
        self.assertEqual(css_response.headers["cache-control"], "no-cache")
        self.assertEqual(logo_response.status_code, 200)
        self.assertEqual(logo_response.headers["cache-control"], "no-cache")
        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)

    def test_versioned_static_asset_adds_cache_busting_query_param(self):
        # Use a representative CSS asset path that exists in the test assets directory
        path = "assets/gui/css/test.css"

        versioned_path = _versioned_static_asset(path)

        # The helper should return a relative URL under /static that includes the original path
        self.assertTrue(versioned_path.startswith("static/"))
        self.assertIn(path, versioned_path)

        # Expect a cache-busting query parameter named "v" with a non-empty value
        self.assertIn("?v=", versioned_path)
        version = versioned_path.split("?v=", 1)[1]
        # Assert a minimum length to ensure it's not just an empty or trivial token
        self.assertGreaterEqual(len(version), 8)

    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
        css_url = urljoin(

```

1. Ensure `_versioned_static_asset` is imported or otherwise available in this test module, e.g.:
   - `from <your_webui_module> import _versioned_static_asset`
   Adjust `<your_webui_module>` to match the actual module that defines the helper.
2. If the helper returns URLs with a different prefix (e.g. `/static/` instead of `static/`), update the `startswith` assertion accordingly.
3. If the implementation uses a fixed-length version token (e.g. 12 or 16 characters), you may tighten the `len(version)` assertion to match that exact length rather than a minimum of 8.
4. Confirm that `"assets/gui/css/test.css"` corresponds to an existing file in the test assets directory; if not, adjust `path` to a file that is present so the helper behaves as expected.
</issue_to_address>

### Comment 3
<location path="tests/test_webui_static_assets.py" line_range="69-74" />
<code_context>
+        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
+        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)
+
+    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
+        css_url = urljoin(
+            "https://example.test/azur/", "static/assets/gui/css/alas.css"
+        )
+        font_url = urljoin(css_url, "../../spa/MiSans-Demibold.ttf")
+        logo_url = urljoin(css_url, "../../../doc/logo.webp")
+
+        self.assertEqual(
+            css_url, "https://example.test/azur/static/assets/gui/css/alas.css"
+        )
+        self.assertEqual(
+            font_url, "https://example.test/azur/static/assets/spa/MiSans-Demibold.ttf"
+        )
+        self.assertEqual(logo_url, "https://example.test/azur/static/doc/logo.webp")
+
+    def test_default_pywebio_assets_are_self_hosted(self):
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding tests for manifest and JS asset relative URLs injected by the Web UI

You’re already covering relative paths in CSS. There’s similar logic in `app_home._load_deferred_client_assets` that switches the PWA manifest and JS bundle from `/static/...` to `static/...`. To cover reverse proxies and non-root paths end-to-end, consider a functional test that renders the index page (or triggers the PyWebIO injection) and asserts that the `link[rel="manifest"]` and `alas-utils-script` tags use `static/...` URLs rather than `/static/...`.

```suggestion
    def test_default_pywebio_assets_are_self_hosted(self):
        response = TestClient(asgi_app({"index": lambda: None})).get("/")

        self.assertEqual(response.status_code, 200)
        self.assertIn('href="pywebio_static/css/app.css?v=', response.text)
        self.assertNotIn("cdn.jsdelivr.net", response.text)

        # PWA manifest should be injected with a relative static path, not an absolute /static path
        self.assertIn('rel="manifest"', response.text)
        self.assertIn('href="static/', response.text)
        self.assertNotIn('href="/static/', response.text)

        # Deferred JS bundle should also use a relative static path
        self.assertIn('id="alas-utils-script"', response.text)
        self.assertIn('src="static/', response.text)
        self.assertNotIn('src="/static/', response.text)
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread module/webui/app.py
Comment on lines +84 to +87
def _versioned_static_asset(relative_path: str) -> str:
"""返回带内容哈希的相对静态资源地址。"""
digest = sha256((PROJECT_ROOT / relative_path).read_bytes()).hexdigest()[:12]
return f"static/{relative_path}?v={digest}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Handle missing/invalid asset files in _versioned_static_asset to avoid startup-time crashes.

If this file doesn’t exist or the path is wrong, this will raise (e.g. FileNotFoundError) when INITIAL_WEBUI_CSS is resolved and can prevent the app from starting. Please catch file/IO errors here and either fall back to an unversioned path or return/log a safe default so a missing asset doesn’t break the web UI.

Original comment in English

issue (bug_risk): Handle missing/invalid asset files in _versioned_static_asset to avoid startup-time crashes.

If this file doesn’t exist or the path is wrong, this will raise (e.g. FileNotFoundError) when INITIAL_WEBUI_CSS is resolved and can prevent the app from starting. Please catch file/IO errors here and either fall back to an unversioned path or return/log a safe default so a missing asset doesn’t break the web UI.

Comment on lines +43 to +52
def test_mounts_only_public_static_directories(self):
css_response = self.client.get("/static/assets/gui/css/test.css")
logo_response = self.client.get("/static/doc/logo.webp")

self.assertEqual(css_response.status_code, 200)
self.assertEqual(css_response.headers["content-type"], "text/css; charset=utf-8")
self.assertEqual(css_response.headers["cache-control"], "no-cache")
self.assertEqual(logo_response.status_code, 200)
self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Also assert cache-control headers for non-CSS assets and tightening static asset expectations

Since the middleware sets Cache-Control: no-cache for cacheable static assets, please also assert logo_response.headers["cache-control"] == "no-cache" to cover binary assets. Additionally, consider a focused test for the versioned CSS helper (e.g. calling _versioned_static_asset on a test file and asserting it returns a relative URL with a ?v= query parameter of the expected length) to directly validate the cache-busting behavior.

Suggested implementation:

        self.assertEqual(css_response.headers["cache-control"], "no-cache")
        self.assertEqual(logo_response.status_code, 200)
        self.assertEqual(logo_response.headers["cache-control"], "no-cache")
        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)

    def test_versioned_static_asset_adds_cache_busting_query_param(self):
        # Use a representative CSS asset path that exists in the test assets directory
        path = "assets/gui/css/test.css"

        versioned_path = _versioned_static_asset(path)

        # The helper should return a relative URL under /static that includes the original path
        self.assertTrue(versioned_path.startswith("static/"))
        self.assertIn(path, versioned_path)

        # Expect a cache-busting query parameter named "v" with a non-empty value
        self.assertIn("?v=", versioned_path)
        version = versioned_path.split("?v=", 1)[1]
        # Assert a minimum length to ensure it's not just an empty or trivial token
        self.assertGreaterEqual(len(version), 8)

    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
        css_url = urljoin(
  1. Ensure _versioned_static_asset is imported or otherwise available in this test module, e.g.:
    • from <your_webui_module> import _versioned_static_asset
      Adjust <your_webui_module> to match the actual module that defines the helper.
  2. If the helper returns URLs with a different prefix (e.g. /static/ instead of static/), update the startswith assertion accordingly.
  3. If the implementation uses a fixed-length version token (e.g. 12 or 16 characters), you may tighten the len(version) assertion to match that exact length rather than a minimum of 8.
  4. Confirm that "assets/gui/css/test.css" corresponds to an existing file in the test assets directory; if not, adjust path to a file that is present so the helper behaves as expected.
Original comment in English

suggestion (testing): Also assert cache-control headers for non-CSS assets and tightening static asset expectations

Since the middleware sets Cache-Control: no-cache for cacheable static assets, please also assert logo_response.headers["cache-control"] == "no-cache" to cover binary assets. Additionally, consider a focused test for the versioned CSS helper (e.g. calling _versioned_static_asset on a test file and asserting it returns a relative URL with a ?v= query parameter of the expected length) to directly validate the cache-busting behavior.

Suggested implementation:

        self.assertEqual(css_response.headers["cache-control"], "no-cache")
        self.assertEqual(logo_response.status_code, 200)
        self.assertEqual(logo_response.headers["cache-control"], "no-cache")
        self.assertEqual(self.client.get("/static/config/deploy.yaml").status_code, 404)
        self.assertEqual(self.client.get("/static/.git/HEAD").status_code, 404)

    def test_versioned_static_asset_adds_cache_busting_query_param(self):
        # Use a representative CSS asset path that exists in the test assets directory
        path = "assets/gui/css/test.css"

        versioned_path = _versioned_static_asset(path)

        # The helper should return a relative URL under /static that includes the original path
        self.assertTrue(versioned_path.startswith("static/"))
        self.assertIn(path, versioned_path)

        # Expect a cache-busting query parameter named "v" with a non-empty value
        self.assertIn("?v=", versioned_path)
        version = versioned_path.split("?v=", 1)[1]
        # Assert a minimum length to ensure it's not just an empty or trivial token
        self.assertGreaterEqual(len(version), 8)

    def test_relative_css_url_preserves_reverse_proxy_prefix(self):
        css_url = urljoin(
  1. Ensure _versioned_static_asset is imported or otherwise available in this test module, e.g.:
    • from <your_webui_module> import _versioned_static_asset
      Adjust <your_webui_module> to match the actual module that defines the helper.
  2. If the helper returns URLs with a different prefix (e.g. /static/ instead of static/), update the startswith assertion accordingly.
  3. If the implementation uses a fixed-length version token (e.g. 12 or 16 characters), you may tighten the len(version) assertion to match that exact length rather than a minimum of 8.
  4. Confirm that "assets/gui/css/test.css" corresponds to an existing file in the test assets directory; if not, adjust path to a file that is present so the helper behaves as expected.

Comment on lines +69 to +74
def test_default_pywebio_assets_are_self_hosted(self):
response = TestClient(asgi_app({"index": lambda: None})).get("/")

self.assertEqual(response.status_code, 200)
self.assertIn('href="pywebio_static/css/app.css?v=', response.text)
self.assertNotIn("cdn.jsdelivr.net", response.text)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider adding tests for manifest and JS asset relative URLs injected by the Web UI

You’re already covering relative paths in CSS. There’s similar logic in app_home._load_deferred_client_assets that switches the PWA manifest and JS bundle from /static/... to static/.... To cover reverse proxies and non-root paths end-to-end, consider a functional test that renders the index page (or triggers the PyWebIO injection) and asserts that the link[rel="manifest"] and alas-utils-script tags use static/... URLs rather than /static/....

Suggested change
def test_default_pywebio_assets_are_self_hosted(self):
response = TestClient(asgi_app({"index": lambda: None})).get("/")
self.assertEqual(response.status_code, 200)
self.assertIn('href="pywebio_static/css/app.css?v=', response.text)
self.assertNotIn("cdn.jsdelivr.net", response.text)
def test_default_pywebio_assets_are_self_hosted(self):
response = TestClient(asgi_app({"index": lambda: None})).get("/")
self.assertEqual(response.status_code, 200)
self.assertIn('href="pywebio_static/css/app.css?v=', response.text)
self.assertNotIn("cdn.jsdelivr.net", response.text)
# PWA manifest should be injected with a relative static path, not an absolute /static path
self.assertIn('rel="manifest"', response.text)
self.assertIn('href="static/', response.text)
self.assertNotIn('href="/static/', response.text)
# Deferred JS bundle should also use a relative static path
self.assertIn('id="alas-utils-script"', response.text)
self.assertIn('src="static/', response.text)
self.assertNotIn('src="/static/', response.text)
Original comment in English

suggestion (testing): Consider adding tests for manifest and JS asset relative URLs injected by the Web UI

You’re already covering relative paths in CSS. There’s similar logic in app_home._load_deferred_client_assets that switches the PWA manifest and JS bundle from /static/... to static/.... To cover reverse proxies and non-root paths end-to-end, consider a functional test that renders the index page (or triggers the PyWebIO injection) and asserts that the link[rel="manifest"] and alas-utils-script tags use static/... URLs rather than /static/....

Suggested change
def test_default_pywebio_assets_are_self_hosted(self):
response = TestClient(asgi_app({"index": lambda: None})).get("/")
self.assertEqual(response.status_code, 200)
self.assertIn('href="pywebio_static/css/app.css?v=', response.text)
self.assertNotIn("cdn.jsdelivr.net", response.text)
def test_default_pywebio_assets_are_self_hosted(self):
response = TestClient(asgi_app({"index": lambda: None})).get("/")
self.assertEqual(response.status_code, 200)
self.assertIn('href="pywebio_static/css/app.css?v=', response.text)
self.assertNotIn("cdn.jsdelivr.net", response.text)
# PWA manifest should be injected with a relative static path, not an absolute /static path
self.assertIn('rel="manifest"', response.text)
self.assertIn('href="static/', response.text)
self.assertNotIn('href="/static/', response.text)
# Deferred JS bundle should also use a relative static path
self.assertIn('id="alas-utils-script"', response.text)
self.assertIn('src="static/', response.text)
self.assertNotIn('src="/static/', response.text)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant