M2: frontend walkthrough fixes + explicit dev compose stack
frontend / frontend (push) Successful in 2m0s
pytest / test (push) Successful in 1m32s

Post-M2 self-walkthrough polish, batched into one commit.

Map / heat:
- fix heat-layer white-screen crash after login (add layer to map before
  setLatLngs; an off-map leaflet.heat layer has a null _map and throws)
- normalize each heat layer to the densest pixel cell visible in the CURRENT
  viewport (maxZoom:0 so intensity factor f=1) and recompute on moveend/zoomend,
  so sparse poo data reaches red and stays normalized at any zoom level
- dark CARTO basemap tiles when the color scheme is dark

UI:
- dark-mode toggle in the top-right, beside the settings gear
- switch top-right nav (records / theme / settings / logout) to Feather icons
  with hover tooltips
- home: Grafana-style quick time-range presets + back/forward shift buttons,
  placed between the From/To pickers and Apply; fix Select/tooltip z-index
  (Leaflet stacking) and the shift-button height alignment

API client:
- stop flooding GET /api/session with 401s: the session probe and the login
  endpoint own their 401s (no global redirect), which fixes the logout hang and
  the spinning login page

Compose:
- rename docker-compose.override.yml -> docker-compose.dev.yml as an explicit,
  non-auto-layered dev stack (8001, -dev container names, prod-copy ./data DB);
  update tests/test_deployment.py (read dev.yml, tolerate the !override tag) and
  the README "Docker Compose" section

Tests:
- pixel-grid peak counter, time-range presets, heat-layer ordering regression,
  and 401-redirect regression
This commit is contained in:
2026-06-13 15:20:50 +02:00
parent bd09523e94
commit da236643f2
16 changed files with 722 additions and 66 deletions
+21 -5
View File
@@ -51,7 +51,21 @@ export function registerLoginRedirect(fn: () => void): void {
const WRITE_METHODS = new Set(['POST', 'PUT', 'PATCH', 'DELETE'])
const LOGIN_PATH = '/api/auth/login'
const csrfMiddleware: Middleware = {
/**
* Endpoints where a 401 is an EXPECTED, locally-handled outcome and must NOT
* trigger the global login redirect:
* - GET /api/session — the session probe; 401 means "not logged in", handled
* by SessionProvider's queryFn (returns null → unauthenticated state).
* - POST /api/auth/login — bad-credentials check; 401 handled by LoginPage.
*
* Redirecting on these would invalidate the session query, which refetches
* /api/session, which 401s, which redirects again → an infinite loop that
* floods GET /api/session after logout and on the login page.
*/
const SESSION_PATH = '/api/session'
const NO_REDIRECT_ON_401 = new Set<string>([SESSION_PATH, LOGIN_PATH])
export const csrfMiddleware: Middleware = {
async onRequest({ request }) {
// Always include cookies (same-origin; explicit for clarity)
// Note: credentials is set at client level; this is belt-and-suspenders doc.
@@ -69,11 +83,13 @@ const csrfMiddleware: Middleware = {
return request
},
async onResponse({ response }) {
async onResponse({ schemaPath, response }) {
if (response.status === 401) {
// Clear any cached session state by triggering a page navigation.
// The SessionProvider query will refetch and find no session.
if (_navigateToLogin) {
// The session probe and the login endpoint own their 401s (see
// NO_REDIRECT_ON_401). For any OTHER endpoint, a 401 means the session
// expired mid-use → redirect to /login. Crucially, NOT redirecting on the
// session probe breaks the refetch→401→redirect→refetch flood loop.
if (!NO_REDIRECT_ON_401.has(schemaPath) && _navigateToLogin) {
_navigateToLogin()
}
// Return the original response so callers can handle 401 if needed.