Commit graph

22 commits

Author SHA1 Message Date
Viktor Barzin
8d22c97320
Add comprehensive test suite: 219 new tests across backend and frontend
Backend (103 tests):
- Unit tests for listing_service, export_service, district_service
- Regression tests for API response contracts and query parameter validation
- Integration tests for API workflows, Redis listing cache, listing processor pipeline, and repository advanced queries
- E2E tests for streaming with filters, batching, caching, and task management

Frontend (116 tests):
- Service tests for apiClient, streamingService, taskService, listingService, healthService
- Hook tests for useTaskProgress (WebSocket + polling)
- Component tests for PropertyCard, FilterPanel, Header, ListView, TaskProgressDrawer, TaskIndicator, StreamingProgressBar, HealthIndicator
- E2E tests for filter-stream-display flow

Infrastructure:
- Add pytest-xdist and test markers (regression, integration, e2e)
- Add conftest fixtures: fake_redis, rent_listing_factory, seeded_repository
- Add vitest + testing-library + MSW for frontend testing
2026-02-10 21:59:45 +00:00
Viktor Barzin
dbcd30679a
Add POI task phases to TaskPhase type union
Add 'starting' and 'computing' to TaskPhase so TypeScript accepts
the POI phase comparisons in TaskProgressDrawer.
2026-02-09 23:06:47 +00:00
Viktor Barzin
2d86213db5
Refactor task progress to unified useTaskProgress hook
Replace WebSocket-only useTaskWebSocket with useTaskProgress that
provides a unified task state interface. TaskIndicator no longer
manages its own polling or auth — it receives task state from the
parent via props. Rename wsTasks prop to tasks throughout.
2026-02-09 23:02:24 +00:00
Viktor Barzin
3616e678ac
Reduce task polling frequency and raise rate limits to prevent 429s
With 8+ active tasks, polling every 5s generates ~96 task_status
requests/min, exceeding the 60/60s rate limit. Two fixes:

- Adaptive polling: 30s when WebSocket is connected (safety net),
  5s only when WebSocket is down (primary source)
- Raise task_status rate limit to 200/60s and tasks_for_user to
  60/60s to handle burst scenarios (page reloads, WS reconnects)
2026-02-09 22:59:39 +00:00
Viktor Barzin
791b5a9d55
Fix real-time task progress by closing WS on pubsub exit and keeping polling active
Three interconnected bugs prevented progress updates from reaching the frontend:

1. _forward_pubsub could exit silently while _handle_client_messages kept
   the WebSocket alive (responding to pings), so the client never detected
   the broken forwarding path. Replace asyncio.gather with asyncio.wait
   (FIRST_COMPLETED) so both coroutines are cancelled together.

2. Polling was stopped on WS connect with no fallback if forwarding broke.
   Now polling runs always alongside WebSocket as a safety net.

3. Redis publish failures in task_progress_publisher were logged at DEBUG
   and the broken client was reused forever. Log at WARNING and reset the
   client so the next call reconnects.
2026-02-09 22:48:57 +00:00
Viktor Barzin
8d52bdf99d
Fix stale task polling by keeping it always active alongside WebSocket
Polling was disabled when wsConnected was true, but if the WS connected
while workers hadn't been redeployed (no pub/sub messages flowing), the
UI received no updates at all. Polling now always runs at 5s as the
baseline. WebSocket provides faster real-time updates on top when
available — the two coexist, last writer wins.
2026-02-09 21:37:07 +00:00
Viktor Barzin
8559c4b461
Add real-time WebSocket task progress with multi-job drawer
Replace 5s HTTP polling with WebSocket-based real-time updates for task
progress. Celery workers publish progress to Redis pub/sub channels;
a FastAPI WebSocket endpoint subscribes and forwards to the browser.
Polling is kept as a 30s fallback when WebSocket is unavailable.

The task progress drawer now supports multiple concurrent jobs with a
tab bar for switching between scrape and POI distance tasks.

Backend:
- Add services/task_progress_publisher.py (Redis pub/sub bridge)
- Add api/ws_routes.py (WebSocket endpoint with JWT auth)
- Publish progress from listing_tasks and poi_tasks
- Publish REVOKED via pub/sub on cancel/clear to fix stuck UI

Frontend:
- Add useTaskWebSocket hook with reconnection and keepalive
- Add TaskState and WS message types
- TaskIndicator: WS-driven updates with polling fallback
- TaskProgressDrawer: multi-job tabs, POI phase timeline
- Guard against WS overwriting local cancel state
2026-02-09 21:31:45 +00:00
Viktor Barzin
73d19e29d5
Fix duplicate listings via staged Redis cache and frontend stream cancellation
Three-pronged fix for duplicate listings appearing in the UI:

1. Backend: Replace direct rpush cache writes with staged population
   (write to temp key, then atomic RENAME to live key). Skip cache
   writes entirely for POI-enriched requests. Clean staging keys on
   invalidation.

2. Frontend: Add AbortController to cancel in-flight streaming requests
   when loadListings is called again, preventing data mixing.

3. Frontend: Deduplicate features by URL during stream accumulation as
   a safety net against any remaining server-side duplicates.
2026-02-09 21:17:30 +00:00
Viktor Barzin
5b8aa98446
Reformat docker-compose command arrays and update tsconfig buildinfo 2026-02-09 20:52:44 +00:00
Viktor Barzin
0fb4504646
Fix stale browser cache after redeployment with proper nginx cache headers
index.html is served with Cache-Control: no-cache so the browser always
fetches the latest version with updated asset hashes. Hashed assets under
/assets/ are cached indefinitely since their filenames change on rebuild.

This prevents browsers from serving old cached JS bundles (including the
broken obfuscated build) after a new deployment.
2026-02-08 22:51:11 +00:00
Viktor Barzin
7319f77f1d
Remove JS obfuscator that broke Mapbox GL map rendering
vite-plugin-obfuscator processes ALL output chunks including vendor
libraries, corrupting Mapbox GL's WebGL shader string literals via
base64 encoding and string splitting. This caused the map to render
as a blank screen in production.

Vite's built-in esbuild minification already mangles identifiers and
removes whitespace, providing sufficient code protection.

Adds regression tests to prevent re-introducing obfuscation plugins.
2026-02-08 22:47:01 +00:00
Viktor Barzin
162d9a886d
Harden frontend assets: disable source maps, add JS obfuscation, env var config
- Disable source maps in production builds (vite.config.ts: sourcemap: false)
- Add vite-plugin-obfuscator for JS obfuscation (hex identifiers, base64 string encoding)
- Move OIDC config behind VITE_* env vars with dev fallbacks (auth/config.ts)
- Add server_tokens off to nginx.conf to stop advertising nginx version
- Add type declaration for vite-plugin-obfuscator
2026-02-08 20:06:33 +00:00
Viktor Barzin
727dd537ef
Fix XSS in map popups by replacing setHTML with setDOMContent
- POI popup: use DOM API with textContent (auto-escapes) instead of template literal in setHTML
- Listing popup: replace renderToString + setHTML with createRoot + setDOMContent for proper React lifecycle
2026-02-08 19:42:44 +00:00
Viktor Barzin
54bdcac14a
Run alembic migrations on startup, fix User model, add POI travel sorting and streaming options 2026-02-08 18:50:13 +00:00
Viktor Barzin
743e018668
Redesign filter panel with range sliders, separated visualization card, and backend filter support
Simplify the filter UI to show only essential filters (type toggle, price/bedroom
range sliders, min size) by default, with advanced filters collapsed. Extract
visualization controls (color-by metric, POI travel mode) into a separate
VisualizationCard component. Wire up previously ignored backend filters: max_sqm,
min/max_price_per_sqm, and district_names now work end-to-end.
2026-02-08 18:50:06 +00:00
Viktor Barzin
1f4a3f858c
Fix heatmap crash on small datasets by clamping percentile indices
Math.round(values.length * 0.95) produces an out-of-bounds index when
the dataset has fewer than ~20 features (e.g. after tight travel time
filtering). values[outOfBounds] returns undefined, cascading to NaN
color stops which crash Mapbox's expression evaluator. Clamp both
min and max indices to values.length - 1.
2026-02-08 17:46:33 +00:00
Viktor Barzin
07d4fa5f84
Add per-POI travel time filtering and fix heatmap color stops
Replace the single global max travel time filter with per-POI filters.
Each POI gets its own travel mode selector and max minutes input in the
filter panel. Listings must satisfy ALL active filters (AND logic).

Fix Mapbox "Input is not a number" error by ensuring color stops are
always strictly monotonic (guard min === max) and always set (even when
no valid metric values exist). Also filter Infinity values from the
color scale computation. Widen the filter panel from w-64 to w-80.
2026-02-08 16:02:46 +00:00
Viktor Barzin
81d31eaecf
Auto-reload listings on task completion and show all POIs in detail view
Thread onTaskCompleted callback from TaskIndicator through Header to App.tsx
so listings auto-refresh when a background task (e.g. POI distance calculation)
completes. Add AllPOIDistances component to PropertyCard that shows all user
POIs with travel times or — placeholder for missing modes.
2026-02-08 15:11:21 +00:00
Viktor Barzin
2fdafdcb64
Auto-trigger WALK + BICYCLE distance calculation on POI creation
After creating a POI, automatically trigger WALK and BICYCLE distance
calculations (cheap OSRM batch API). TRANSIT is excluded since it uses
the expensive OTP backend — users trigger it manually via the calculator
button. Failure is non-fatal: the POI is still created and calculation
can be retried manually.
2026-02-08 14:51:34 +00:00
Viktor Barzin
8509a0326f
Add frontend POI management and travel time display
POIManager component in FilterPanel for creating/deleting POIs and
triggering distance calculations. PropertyCard shows travel time badges
(walk/cycle/transit) per POI. Map renders POI locations as red markers.
API client extended with POST body support for POI endpoints.
2026-02-08 13:16:32 +00:00
Viktor Barzin
a7fa9b81cf
Fix OIDC logout not redirecting back to app
Pass id_token_hint explicitly to signoutRedirect() so Authentik
honors the post_logout_redirect_uri and sends users back to the app.
2026-02-08 00:29:02 +00:00
Viktor Barzin
eafbc1ac52
Flatten repo structure: move crawler/ to root, remove vqa/ and immoweb/
The crawler subdirectory was the only active project. Moving it to the
repo root simplifies paths and removes the unnecessary nesting. The
vqa/ and immoweb/ directories were legacy/unused and have been removed.

Updated .drone.yml, .gitignore, .claude/ docs, and skills to reflect
the new flat structure.
2026-02-07 23:01:20 +00:00