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:
Tejas Chopra 2026-08-03 05:38:48 -07:00
parent 007446c73a
commit 372a4e04bc
9 changed files with 179 additions and 5 deletions

14
NOTICE
View file

@ -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

View file

@ -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:

File diff suppressed because one or more lines are too long

1
headroom/dashboard/static/htmx.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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');

View file

@ -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');

View file

@ -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."""

View 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