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
+62
View File
@@ -0,0 +1,62 @@
/**
* csrfMiddleware 401-handling regression tests.
*
* Bug: clicking Logout (or landing on /login) flooded GET /api/session with 401s
* and the page hung instead of returning to the login screen.
*
* Root cause: the middleware redirected on EVERY 401, including the session
* probe's own 401. The redirect invalidated the ['session'] query, which
* refetched GET /api/session, which 401'd, which redirected again → an infinite
* refetch loop. These tests pin the fix: the session probe and the login
* endpoint own their 401s (no redirect); any other endpoint's 401 still
* redirects (session expired mid-use).
*
* We call onResponse() directly (rather than going through apiClient.GET) so the
* test exercises the exact 401 branch without the singleton's relative baseUrl,
* which has no absolute origin to resolve against under jsdom.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { Middleware } from 'openapi-fetch'
import { csrfMiddleware, registerLoginRedirect } from './client'
type OnResponse = NonNullable<Middleware['onResponse']>
type OnResponseParams = Parameters<OnResponse>[0]
/** Build the minimal onResponse params for the given schema path + response. */
function params(schemaPath: string, response: Response): OnResponseParams {
return { schemaPath, response, request: new Request('http://test.local' + schemaPath) } as OnResponseParams
}
function response401(): Response {
return new Response(JSON.stringify({ detail: 'unauthorized' }), {
status: 401,
headers: { 'Content-Type': 'application/json' },
})
}
const onResponse = csrfMiddleware.onResponse as OnResponse
describe('csrfMiddleware 401 redirect (session-flood regression)', () => {
const redirect = vi.fn()
beforeEach(() => {
redirect.mockReset()
registerLoginRedirect(redirect)
})
it('does NOT redirect when GET /api/session returns 401 (probe owns its 401)', async () => {
await onResponse(params('/api/session', response401()))
expect(redirect).not.toHaveBeenCalled()
})
it('does NOT redirect when POST /api/auth/login returns 401 (bad credentials)', async () => {
await onResponse(params('/api/auth/login', response401()))
expect(redirect).not.toHaveBeenCalled()
})
it('redirects when a normal endpoint returns 401 (session expired mid-use)', async () => {
await onResponse(params('/api/locations', response401()))
expect(redirect).toHaveBeenCalledTimes(1)
})
})