diff --git a/dashboard.py b/dashboard.py index ebf8d5f..b129625 100644 --- a/dashboard.py +++ b/dashboard.py @@ -6,6 +6,7 @@ import os import sqlite3 from http.server import HTTPServer, BaseHTTPRequestHandler +from urllib.parse import urlparse from pathlib import Path from datetime import datetime @@ -1242,13 +1243,17 @@ def log_message(self, format, *args): pass def do_GET(self): - if self.path in ("/", "/index.html"): + # self.path includes the query string, but every URL the UI emits has + # one (e.g. "/?range=all"); compare the bare path so bookmarkable + # URLs don't fall through to 404. + path = urlparse(self.path).path + if path in ("/", "/index.html"): self.send_response(200) self.send_header("Content-Type", "text/html; charset=utf-8") self.end_headers() self.wfile.write(HTML_TEMPLATE.encode("utf-8")) - elif self.path == "/api/data": + elif path == "/api/data": data = get_dashboard_data() body = json.dumps(data).encode("utf-8") self.send_response(200) @@ -1262,7 +1267,8 @@ def do_GET(self): self.end_headers() def do_POST(self): - if self.path == "/api/rescan": + path = urlparse(self.path).path + if path == "/api/rescan": # Full rebuild: delete DB and rescan from scratch. # Pass DB_PATH / DEFAULT_PROJECTS_DIRS explicitly so tests that # patch the module globals are honored (scan's defaults are diff --git a/tests/test_dashboard.py b/tests/test_dashboard.py index 76287d1..074e783 100644 --- a/tests/test_dashboard.py +++ b/tests/test_dashboard.py @@ -166,6 +166,23 @@ def test_index_returns_html(self): self.assertEqual(resp.status, 200) self.assertIn("text/html", resp.headers["Content-Type"]) + def test_index_with_query_string_returns_html(self): + # Regression: ?range=... and ?models=... must not 404. The dashboard + # itself rewrites the URL with these params via history.replaceState, + # so anything that reloads or bookmarks the page hits this path. + for qs in ("?range=all", "?range=30d&models=claude-opus-4-7"): + with urllib.request.urlopen(f"http://127.0.0.1:{self.port}/{qs}") as resp: + self.assertEqual(resp.status, 200) + self.assertIn(b"Claude Code Usage Dashboard", resp.read()) + + def test_api_data_with_query_string(self): + # /api/data is fetched without query parameters today, but the route + # should be tolerant if any are tacked on (e.g. cache-busting). + with urllib.request.urlopen( + f"http://127.0.0.1:{self.port}/api/data?_=cachebust" + ) as resp: + self.assertEqual(resp.status, 200) + def test_api_data_returns_json(self): url = f"http://127.0.0.1:{self.port}/api/data" with urllib.request.urlopen(url) as resp: