Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions tests/test_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down