mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
fix(dashboard): serve tailwind/htmx/alpine locally instead of from CDNs
The dashboard pulled Tailwind, htmx and Alpine from cdn.tailwindcss.com and unpkg.com at page load. Edge's Tracking Prevention classifies unpkg.com as a tracker and blocks it by default on Windows, and locked-down corporate proxies block both hosts, so on those machines none of the three scripts executed: no CSS, no htmx polling, no Alpine bindings, plus a ReferenceError from the inline `tailwind.config` assignment. The dashboard rendered blank. Vendor all three under headroom/dashboard/static/ and mount them at /dashboard/static, registered before register_provider_routes' catch-all so the requests are not tunneled to the wrapped upstream provider. maturin already ships everything under headroom/, so the wheel picks them up. Adds ~498 KB to the wheel and makes the dashboard work fully offline.
This commit is contained in:
parent
007446c73a
commit
372a4e04bc
9 changed files with 179 additions and 5 deletions
14
NOTICE
14
NOTICE
|
|
@ -41,3 +41,17 @@ NumPy (optional dependency)
|
|||
Copyright (c) 2005-2024, NumPy Developers
|
||||
Licensed under the BSD 3-Clause License
|
||||
https://github.com/numpy/numpy
|
||||
|
||||
Vendored dashboard assets (headroom/dashboard/static/)
|
||||
------------------------------------------------------
|
||||
Tailwind CSS 3.4.17 (Play CDN build) — MIT License
|
||||
Copyright (c) Tailwind Labs, Inc.
|
||||
https://github.com/tailwindlabs/tailwindcss
|
||||
|
||||
htmx 1.9.10 — Zero-Clause BSD License
|
||||
Copyright (c) 2020, Big Sky Software
|
||||
https://github.com/bigskysoftware/htmx
|
||||
|
||||
Alpine.js 3.13.3 — MIT License
|
||||
Copyright (c) 2019-2025 Caleb Porzio and contributors
|
||||
https://github.com/alpinejs/alpine
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ from pathlib import Path
|
|||
|
||||
DASHBOARD_DIR = Path(__file__).parent
|
||||
TEMPLATES_DIR = DASHBOARD_DIR / "templates"
|
||||
# Vendored tailwind/htmx/alpine. Served locally because Edge's Tracking
|
||||
# Prevention and corporate proxies block unpkg.com/cdn.tailwindcss.com, which
|
||||
# left the dashboard unstyled and dataless on some Windows machines.
|
||||
STATIC_DIR = DASHBOARD_DIR / "static"
|
||||
|
||||
|
||||
def get_dashboard_html() -> str:
|
||||
|
|
|
|||
5
headroom/dashboard/static/alpine.min.js
vendored
Normal file
5
headroom/dashboard/static/alpine.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
headroom/dashboard/static/htmx.min.js
vendored
Normal file
1
headroom/dashboard/static/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
83
headroom/dashboard/static/tailwind.min.js
vendored
Normal file
83
headroom/dashboard/static/tailwind.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -4,9 +4,9 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Headroom Dashboard</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js" defer></script>
|
||||
<script src="/dashboard/static/tailwind.min.js"></script>
|
||||
<script src="/dashboard/static/htmx.min.js"></script>
|
||||
<script src="/dashboard/static/alpine.min.js" defer></script>
|
||||
<script>
|
||||
(function() {
|
||||
const saved = localStorage.getItem('headroom-theme');
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Headroom Settings</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js" defer></script>
|
||||
<script src="/dashboard/static/tailwind.min.js"></script>
|
||||
<script src="/dashboard/static/alpine.min.js" defer></script>
|
||||
<script>
|
||||
(function () {
|
||||
const saved = localStorage.getItem('headroom-theme');
|
||||
|
|
|
|||
|
|
@ -3353,6 +3353,20 @@ def create_app(config: ProxyConfig | None = None) -> FastAPI:
|
|||
content={"applied": applied, "runtime_env": runtime_env.effective_runtime_env()},
|
||||
)
|
||||
|
||||
# Vendored dashboard JS (tailwind/htmx/alpine). Mounted before
|
||||
# register_provider_routes' catch-all so it is not tunneled upstream.
|
||||
from starlette.staticfiles import StaticFiles
|
||||
|
||||
from headroom.dashboard import STATIC_DIR
|
||||
|
||||
# check_dir=False keeps a missing assets directory from aborting proxy
|
||||
# startup: the dashboard JS 404s, but proxying itself still works.
|
||||
app.mount(
|
||||
"/dashboard/static",
|
||||
StaticFiles(directory=STATIC_DIR, check_dir=False),
|
||||
name="dashboard-static",
|
||||
)
|
||||
|
||||
@app.get("/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard():
|
||||
"""Serve the Headroom dashboard UI."""
|
||||
|
|
|
|||
53
tests/test_dashboard_static_assets.py
Normal file
53
tests/test_dashboard_static_assets.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""The dashboard must not depend on third-party CDNs.
|
||||
|
||||
Edge's Tracking Prevention (and corporate proxies) block unpkg.com and
|
||||
cdn.tailwindcss.com, which left the dashboard unstyled and dataless on some
|
||||
Windows machines. Tailwind/htmx/alpine are vendored and served locally instead.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
from fastapi.testclient import TestClient # noqa: E402
|
||||
|
||||
from headroom.dashboard import get_dashboard_html, get_settings_html # noqa: E402
|
||||
from headroom.proxy.server import ProxyConfig, create_app # noqa: E402
|
||||
|
||||
ASSETS = ["tailwind.min.js", "htmx.min.js", "alpine.min.js"]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = create_app(
|
||||
ProxyConfig(
|
||||
optimize=False,
|
||||
cache_enabled=False,
|
||||
rate_limit_enabled=False,
|
||||
cost_tracking_enabled=False,
|
||||
log_requests=False,
|
||||
)
|
||||
)
|
||||
with TestClient(app) as c:
|
||||
yield c
|
||||
|
||||
|
||||
@pytest.mark.parametrize("html", [get_dashboard_html(), get_settings_html()])
|
||||
def test_templates_reference_no_cdn(html: str):
|
||||
assert "unpkg.com" not in html
|
||||
assert "cdn.tailwindcss.com" not in html
|
||||
|
||||
|
||||
@pytest.mark.parametrize("asset", ASSETS)
|
||||
def test_asset_is_served(client, asset: str):
|
||||
resp = client.get(f"/dashboard/static/{asset}")
|
||||
assert resp.status_code == 200, resp.text
|
||||
assert len(resp.content) > 10_000
|
||||
|
||||
|
||||
def test_dashboard_only_references_served_assets(client):
|
||||
html = client.get("/dashboard").text
|
||||
for asset in ASSETS:
|
||||
assert f"/dashboard/static/{asset}" in html
|
||||
Loading…
Add table
Add a link
Reference in a new issue