diff --git a/REACT_MIGRATION.md b/REACT_MIGRATION.md
index d38678b..bddfe0a 100644
--- a/REACT_MIGRATION.md
+++ b/REACT_MIGRATION.md
@@ -8,16 +8,25 @@ Migration from lit-html (functional templates) to React 19 + TypeScript + Vite.
|-------|-------------|--------|
| 1 | Infrastructure (Vite, React shell, router, LitBridge, build pipeline, shared components) | **Complete** |
| 2 | Convert pages one-by-one from LitBridge to native React | **Complete** |
-| 3 | Chart & map components (react-chartjs-2, react-leaflet) | Not started |
+| 3 | Chart & map components (react-chartjs-2, react-leaflet) | **Complete** |
| 4 | Cleanup (remove lit-html, old spa/, build.js esbuild remnants) | Not started |
| 5 | Optional enhancements (tests, react-query, Storybook) | Not started |
+> **Phase 3 status:** All `window.Chart` / `window.L` / `window.QRCode` globals and the
+> `charts.js` helper script are gone. Charts now use **react-chartjs-2** (typed builders in
+> `spa-react/utils/charts.ts` + components in `spa-react/components/charts/Charts.tsx`),
+> maps use **react-leaflet** (`MapPage.tsx`, `NodeDetail.tsx`), and QR codes use
+> **react-qr-code** (`Channels.tsx`, `NodeDetail.tsx`). Chart.js, Leaflet (+ its CSS), and
+> react-qr-code are bundled by Vite — the vendor `
-
-
-
-
-
-
-
-
-
-
- {% if asset_app_css %}
-
- {% endif %}
{% if asset_app_js %}
{% else %}
diff --git a/tests/test_collector/test_routes.py b/tests/test_collector/test_routes.py
index 3351095..76157f8 100644
--- a/tests/test_collector/test_routes.py
+++ b/tests/test_collector/test_routes.py
@@ -1125,8 +1125,8 @@ class TestComputeAverageQuality:
"""Rolling-average tier over a history window (server-side badge source).
Mirrors the ``averageRouteTier`` JS helper in
- ``web/static/js/charts.js`` so the route card badge matches the chart
- line color when both render the same window.
+ ``web/static/js/spa-react/utils/charts.ts`` so the route card badge matches
+ the chart line color when both render the same window.
"""
@staticmethod
diff --git a/tests/test_web/test_caching.py b/tests/test_web/test_caching.py
index 93f5892..280d789 100644
--- a/tests/test_web/test_caching.py
+++ b/tests/test_web/test_caching.py
@@ -19,7 +19,7 @@ class TestCacheControlHeaders:
def test_static_js_with_version(self, client):
"""Static JS with version parameter should have long-term cache."""
- response = client.get(f"/static/js/charts.js?v={__version__}")
+ response = client.get(f"/static/js/spa/app.js?v={__version__}")
assert response.status_code == 200
assert "cache-control" in response.headers
assert (
@@ -59,7 +59,7 @@ class TestCacheControlHeaders:
def test_static_js_without_version(self, client):
"""Static JS without version should have short fallback cache."""
- response = client.get("/static/js/charts.js")
+ response = client.get("/static/js/spa/app.js")
assert response.status_code == 200
assert "cache-control" in response.headers
assert response.headers["cache-control"] == "public, max-age=3600"
@@ -152,19 +152,6 @@ class TestVersionParameterInHTML:
assert css_link is not None
assert f"?v={__version__}" in css_link["href"]
- def test_charts_js_has_version(self, client):
- """Charts.js script should include version parameter."""
- response = client.get("/")
- assert response.status_code == 200
-
- soup = BeautifulSoup(response.text, "html.parser")
- charts_script = soup.find(
- "script", {"src": lambda x: x and "/static/js/charts.js" in x}
- )
-
- assert charts_script is not None
- assert f"?v={__version__}" in charts_script["src"]
-
def test_app_js_has_version(self, client):
"""SPA app.js script should include version or content hash."""
response = client.get("/")