Commit graph

57 commits

Author SHA1 Message Date
Viktor Barzin
69ce458308
feat: add dev auth bypass for UI testing without backend
Guarded by VITE_DEV_BYPASS_AUTH env var + import.meta.env.DEV check.
Vite tree-shakes the DEV branch in production builds.
The .env.development.local file is gitignored (**.env pattern).
Includes mock listing data to preview property cards without API.
2026-02-28 16:37:33 +00:00
Viktor Barzin
dea930dbc4
fix: remove unused imports flagged by TypeScript 2026-02-28 16:23:36 +00:00
Viktor Barzin
ab02fb120c
feat: redesign listing detail with tabbed sections and larger drawer 2026-02-28 16:21:20 +00:00
Viktor Barzin
812bfece4a
style: redesign PropertyCard with better visual hierarchy 2026-02-28 16:21:17 +00:00
Viktor Barzin
be2f0ef282
feat: add error boundary to prevent white-screen crashes
Wraps the entire app in an ErrorBoundary that shows a friendly
error message with reload button instead of crashing to white.
2026-02-28 16:18:10 +00:00
Viktor Barzin
8f112f30e3
feat: integrate FilterBar into layout, remove sidebar
Replace the fixed w-80 sidebar with a horizontal FilterBar below the
header, giving the map full viewport width. Key changes:

- App.tsx: Remove sidebar layout, add FilterBar + FilterChips + inline
  StreamingProgressBar between header and main content area
- Header.tsx: Add Rent/Buy listing type toggle (compact Tabs) after logo
- StatsBar.tsx: Add "Color by" metric selector (moved from
  VisualizationCard) as a compact Select alongside view mode toggles
- Mobile: Replace Sheet-based filter panel with full-screen Dialog
2026-02-28 16:16:03 +00:00
Viktor Barzin
4053c0c759
feat: create FilterBar and FilterChips components
Add a horizontal FilterBar component with popover-based dropdowns for
Price, Beds, Size, and a "More Filters" panel with advanced options
(price/m2, furnishing, district, date, POI travel filters). Action
buttons (Show Listings / Scrape New) are aligned to the right.

Add FilterChips component that renders active (non-default) filter
values as removable pills below the filter bar.
2026-02-28 16:12:09 +00:00
Viktor Barzin
de47e2cca8
feat: add React Router with URL-based filter state and deep linking
- Wrap App in BrowserRouter in main.tsx
- Create useFilterParams hook that syncs filter state with URL search params
  and derives viewMode from the URL pathname
- Replace window.location.pathname callback check with React Router Routes
- Split App into AppContent (main UI) and App (route definitions)
2026-02-28 16:07:14 +00:00
Viktor Barzin
676fad520c
style: update color palette from neutral to teal-accented property theme
- Primary/accent changed from achromatic black to teal (oklch 0.55 0.14 175)
- Background/foreground given subtle cool slate tint
- Added --deal-good (emerald) and --deal-above (amber) custom properties
- Ring color updated to teal for focus states
- Dark mode updated to match teal theme
2026-02-28 16:03:56 +00:00
Viktor Barzin
1037ff164d
refactor: extract shared utility functions to eliminate duplication 2026-02-28 16:02:06 +00:00
Viktor Barzin
35f1987ac1
Add navigation & usage metrics for end-user experience visibility
Instrument DB query timing (11 operations across 3 repositories),
streaming lifecycle (TTFB, duration, feature count), cache operation
latency, listing detail step breakdown, and frontend page load /
time-to-first-listing / stream download / detail load metrics.

Adds 16 new OTel instruments, extends the perf ingestion endpoint
with 4 new frontend metrics, and adds ~20 Grafana dashboard panels
across 4 new rows (DB Query Performance, Streaming Performance,
Listing Detail Breakdown, Cache Performance, Frontend Navigation).
2026-02-23 20:28:42 +00:00
Viktor Barzin
1ae00b7cbf
Add multi-layer caching: 24h Redis TTL, stale-while-revalidate, frontend LRU cache
- Increase Redis cache TTL from 30 minutes to 24 hours
- Add stale-while-revalidate: serve stale cache (>4h) immediately while
  repopulating in background with SETNX lock to prevent concurrent rebuilds
- Add in-memory frontend LRU cache (5 entries) so repeat filter visits
  are instant without network requests
- Invalidate frontend cache on listing refresh and task completion
- Add unit tests for get_cache_age, is_cache_stale, acquire_repopulation_lock
2026-02-23 20:09:36 +00:00
Viktor Barzin
04bda8c127
Fix FilterPanel test: button text changed to 'Show Matching Listings'
The FilterPanel component's submit button was renamed from 'Apply Filters'
to 'Show Matching Listings' but the test was not updated to match.
2026-02-23 19:40:06 +00:00
Viktor Barzin
339e2cf2ab
Improve frontend UI/UX: accessibility, discoverability, and cleanup
- Add prefers-reduced-motion support (global CSS + SwipeCard spring config)
- Add dismissible map click hint overlay with localStorage persistence
- Replace generic image alt text with descriptive property info across 4 components
- Rename filter buttons to clarify intent (Show Matching Listings / Scrape New from Rightmove)
- Fix mobile FAB overlap with bottom sheet via dynamic snap-aware positioning
- Add swipe review onboarding overlay with gesture explanations and button labels
- Delete unused components: AppSidebar, ActiveQuery, SavedView
2026-02-22 18:47:09 +00:00
Viktor Barzin
d90fa38776
Add frontend performance metrics pipeline to Prometheus
Collect browser-side worker round-trips, computation times, main-thread
operations, and feature counts, batch them client-side, and expose as
Prometheus histograms via a new POST /api/perf endpoint.
2026-02-22 17:30:29 +00:00
Viktor Barzin
81ff9d9e41
Move hexgrid heatmap computation to Web Worker
R-tree building, hex grid generation, and percentile sorting now run
off the main thread, eliminating 20s+ UI freezes on large datasets.
The old bundled HexgridHeatmap.js is replaced by a typed worker +
main-thread client with dual R-trees (worker for grid gen, main
thread for synchronous click queries).
2026-02-22 15:04:37 +00:00
Viktor Barzin
8ef6868881
Eliminate frontend POI waterfall for faster initial load
Listing stream fires immediately on auth without waiting for POI
fetch. POI distances are not needed for initial rendering and are
only computed when user selects POI metric or sets travel filters.
This saves ~200-500ms on initial load and keeps the stream on the
cached Redis path.
2026-02-22 13:31:10 +00:00
Viktor Barzin
3885fd52fe
Add bulk POI distances endpoint for decoupled loading
New GET /api/poi/distances/bulk returns all POI distances keyed by
listing ID, allowing the frontend to fetch distances separately
from the listing stream and keep the stream on the cached path.
2026-02-22 13:29:35 +00:00
Viktor Barzin
9dc011754b
Add cancel button to streaming progress bar
The X button aborts the in-flight fetch via AbortController,
which was already wired up but had no UI trigger. Works for
both desktop and mobile views.
2026-02-22 02:04:56 +00:00
Viktor Barzin
301579c255
Separate photo swipe from card swipe gestures
Move drag handler from outer card to details section only.
Swiping on photos now navigates the carousel, swiping on
the details area below triggers like/dislike/skip actions.
2026-02-22 02:02:10 +00:00
Viktor Barzin
e2c22f025f
Expand swipe card to 50/50 photo/details split with all info
- Card now fills available height with photo carousel in top half
  and property details in bottom half
- Details section shows: price, beds/sqm/price-per-sqm, agency,
  available date, all POI distances, and price history summary
- Fix DialogTitle accessibility warning in ListingDetailSheet and
  MobileBottomSheet (add sr-only Drawer.Title)
2026-02-22 00:49:32 +00:00
Viktor Barzin
611449d328
Add photo carousel to swipe cards
SwipeCard now shows all available photos (up to 5) using
embla-carousel instead of just the thumbnail. Includes prev/next
arrow buttons and dot indicators. The photo area uses touch-action:
pan-x so carousel swipes don't trigger card swipes.
2026-02-21 23:57:26 +00:00
Viktor Barzin
eacdf24621
Add tap-to-detail on swipe cards and fix color overlay alignment
- Add onTap callback to SwipeCard using useDrag's tap detection
- Wire through SwipeReviewMode to open ListingDetailSheet on tap
- Fix color overlay misalignment: add relative to card container so
  the absolute overlay positions within the rounded card, not the
  full-width outer wrapper
2026-02-21 21:13:32 +00:00
Viktor Barzin
9c954c0e43
Add prev/next arrow buttons to photo carousel 2026-02-21 21:07:17 +00:00
Viktor Barzin
4deed9911c
Add photo carousel to listing cards and fix tap-to-detail
Backend: include first 5 photo URLs from additional_info in GeoJSON
streaming response, with fallback to photo_thumbnail.

Frontend: replace single thumbnail with swipeable embla-carousel on
compact cards. Remove window.open on card tap so clicking opens the
detail bottom sheet instead of navigating to Rightmove.
2026-02-21 19:19:32 +00:00
Viktor Barzin
a153f64af4
Format available_from dates as human-readable on listing cards
Display dates like "22 Jun 2025" instead of raw ISO timestamps.
2026-02-21 18:02:14 +00:00
Viktor Barzin
a2745c1478
Add tappable cards, detail bottom sheet, swipe gestures, and favorites view
- Decision types, services (decisionService, listingDetailService), and index exports
- useDecisions hook with optimistic updates and Map-based state
- useListingDetail hook with session-level caching
- PhotoCarousel component using embla-carousel-react
- ListingDetail component with full property info, like/dislike buttons
- ListingDetailSheet using vaul Drawer (slide-up bottom sheet)
- SwipeablePropertyCard with @use-gesture/react and @react-spring/web
- SwipeReviewMode for mobile full-screen swipe review
- FavoritesView with virtualized liked listings and remove button
- App.tsx integration: decision state, client-side disliked filtering, detail sheet, swipe handlers
- ListView conditionally renders SwipeablePropertyCard when handlers provided
- StatsBar adds 'saved' view mode with heart icon
- Header adds liked count indicator
- New deps: vaul, embla-carousel-react, @use-gesture/react, @react-spring/web
2026-02-21 15:49:15 +00:00
Viktor Barzin
49280d9679
fix: QA fixes for property decisions feature
- Replace deprecated datetime.utcnow() with datetime.now(UTC) in model
  and repository
- Add listing_type validation to decision_service (RENT/BUY only)
- Fix decision filtering tests failing due to rate limiting by patching
  _match_endpoint
- Add SwipeCard component test suite (11 tests covering rendering,
  interactions, and POI distances)
- Add test for invalid listing_type validation
2026-02-21 14:04:34 +00:00
Viktor Barzin
43084ef19a
feat: add SavedView, integrate decisions into App with review mode and client-side filtering 2026-02-21 13:55:05 +00:00
Viktor Barzin
d350b806ba
feat: add SwipeCard and SwipeReviewMode components with gesture support 2026-02-21 13:53:06 +00:00
Viktor Barzin
2fdeebbae1
feat: add useDecisions hook with optimistic updates 2026-02-21 13:51:50 +00:00
Viktor Barzin
813f048e46
feat: add frontend decision service and types 2026-02-21 13:51:12 +00:00
Viktor Barzin
13637ec881
fix: update tests for mobile responsive changes
- Add matchMedia polyfill to test setup (jsdom doesn't implement it)
- Remove Header tests for filter toggle (replaced by mobile FAB)
2026-02-21 11:45:50 +00:00
Viktor Barzin
a744b33578
feat: make frontend fully responsive with mobile-first layout
Add mobile-responsive design with full feature parity:
- Bottom sheet (vaul) with 3 snap points for map+list coexistence
- Swipeable property cards with horizontal scroll-snap
- Hamburger menu with health, tasks, user info
- Full-screen map with repositioned legend (top-left on mobile)
- Filter FAB opening Sheet drawer
- TaskProgressDrawer from bottom on mobile
- All changes gated behind useIsMobile() hook (768px breakpoint)
- Desktop layout completely untouched

New components: MobileBottomSheet, SwipeableCardRow,
PropertyCardCompact, MobileMenu

Also fixes: idempotent longitude migration, React hooks order
2026-02-21 11:34:53 +00:00
Viktor Barzin
2d6726dcd7
Show last listing update time next to connection status in header
Add last_updated timestamp to /api/status endpoint by querying
MAX(last_seen) across both listing tables. Display it in the
HealthIndicator as relative time (e.g. "2h ago") with full
date/time in the tooltip on hover.
2026-02-17 19:54:15 +00:00
Viktor Barzin
6489012e8f
Auto-pan map to keep popup fully visible in viewport
When a property popup opens near the edge of the map, the map now
automatically pans so the entire popup is visible with 20px padding.
2026-02-15 15:27:51 +00:00
Viktor Barzin
a1829957c1
Auto-redirect to login on 401 API responses
When a session token expires, API calls return 401 but nothing caught
it — errors were shown as generic dialogs or swallowed. Now both
apiClient and streamingService detect 401 responses and clear auth
state, which causes App.tsx to render the login modal automatically.
2026-02-13 21:16:53 +00:00
Viktor Barzin
41b7d221e4
Fix 7 bugs: security, memory leak, stale state, error handling
- WebSocket: verify task ownership before allowing subscribe (security)
- POI routes: replace assert with HTTPException for production safety
- cancel_task: return HTTP 404 instead of 200 for missing tasks
- routing_config: add descriptive ValueError for invalid env vars
- POIManager: show error feedback instead of silently swallowing failures
- VisualizationCard: reset POI/travel mode state on metric switch
- Map: clean up heatmap layers/sources on unmount to prevent memory leak
- Update test to expect 404 from cancel_task ownership check
2026-02-13 19:36:43 +00:00
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
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