Commit graph

78 commits

Author SHA1 Message Date
Viktor Barzin
25458fd2a2 wrongmove: bake VITE_MAPBOX_TOKEN into the frontend build
Adds a build-arg path so the Mapbox public token is injected at
`vite build` time instead of being hardcoded in the bundle:

- `frontend/Dockerfile` declares `ARG VITE_MAPBOX_TOKEN` in the
  builder stage and re-exports it via `ENV` so Vite picks it up.
- `.woodpecker/frontend.yml` maps the global `wrongmove-mapbox-token`
  Woodpecker secret into a step-level `VITE_MAPBOX_TOKEN` env var,
  then forwards it via `build_args_from_env`.

Token is a domain-restricted `pk.*` public token (Mapbox), so bundle
exposure is the intended threat model. Vault-stored at
`secret/ci/global/wrongmove-mapbox-token`; synced to Woodpecker by
the existing vault-woodpecker-sync CronJob every 6h.

Replaces the post-Fix-4 "Map unavailable — set VITE_MAPBOX_TOKEN"
banner with a working basemap.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 21:42:40 +00:00
Viktor Barzin
a42944a756 wrongmove: round-3 fix sweep — scrape pipeline, BUY tab, filter URL state, render hygiene, map polish
Coordinated fix across 31 bugs found in a parallel QA pass. Findings docs at /tmp/wrongmove-bugs/qa-round-3/qa{1,2,3,4}-*.md.

## Backend / scrape (Fix-1) — 8 bugs

- B1 [P0] Scrape totally broken on prod: pod UID 100 vs NFS dir 1000:1000 mode 775 → PermissionError on every never-seen listing. Switched Dockerfile to explicit `useradd --uid 1000 --gid 1000`; added securityContext + chown initContainer to k8s/{api,celery-beat}-deployment.yaml. Celery worker manifest lives outside this repo — Dockerfile UID change is the load-bearing fix.
- B4 [P1] Celery broker reaped every ~30s by Redis HAProxy idle timeout. Added `broker_transport_options` / `result_backend_transport_options` with `socket_keepalive=True, health_check_interval=25` in celery_app.py + same kwargs on every redis.from_url/Redis call across services/, utils/redis_lock.py, redis_repository.py.
- B5 [P1] dump_listings_task never published terminal FAILURE to the task_progress pub/sub channel — UI polled forever. Wrap body in try/except that publishes FAILURE before re-raising.
- B6 [P1] _process_worker had no per-listing exception handler — one bad listing killed the whole scrape via asyncio.gather. Wrap loop body in try/except Exception (re-raises CancelledError).
- B20 [P2] dump_listings_task gained time_limit=3600, soft_time_limit=3500, acks_late=True.
- B21 [P2] RedisRepository moved off shared db0 (was alongside paperless-ngx) to db3 via REDIS_USER_DB env var; keys prefixed `wrongmove:user:`.
- B32 [P3] redis_lock now uses uuid4() owner token + Lua compare-and-delete.
- B33 [P3] Slack notify in refresh_listings → asyncio.create_task (fire-and-forget).

## Frontend filter system (Fix-2) — 7 bugs

- B2 [P0] BUY tab click triggered "Maximum update depth exceeded" → ErrorBoundary. Replaced the three mutually-triggering useEffects in FilterBar with a single one-way controlled-value flow (URL → parent state → form), guarded by previousListingTypeRef so price-defaults fires once per real transition.
- B3 [P0] Filter values never reached the URL. Wired useFilterParams.setFilterValues into FilterBar/FilterPanel onSubmit + handleRemoveChip + new handleResetAllFilters; fed parsed filterValues into both forms' defaultValues; added URL→form sync via form.reset on browser back/forward.
- B8 [P1] Chip removal now resets form state via new FilterBar onFormReady callback — More badge no longer sticks.
- B12 [P2] Desktop swipe-review FAB added next to header (mobile FAB unchanged).
- B17 [P2] "Reset all" affordance on chip strip.
- B22 [P2] formatPrice precision: 1500 → £1.5k, 2500 → £2.5k (no longer collides with £2k/£3k defaults).
- B30 [P3] last_seen_days input gained min={0}.

## Frontend render hygiene + data integrity (Fix-3) — 8 bugs

- B7 [P1] streamingService bails on first non-NDJSON chunk (HTML response = backend down) and throws StreamParseError so the existing AlertError dialog surfaces a single user-visible error instead of 18× console.error spam.
- B9 [P1] formatDuration widened to (null|undefined|number): returns "—" for non-finite or negative, caps implausibly large values.
- B10 [P1] PropertyCard / PropertyCardCompact / SwipeCard JSX leaves render "—" for null total_price/qm/qmprice (was "£0/0 m²/£0/m²" — looked like free listings).
- B13 [P2] hexgrid worker reduceAverage uses Number.isFinite filter instead of !isNaN (which incorrectly accepted null → 0, biasing per-hex averages low).
- B14 [P2] ListingDetail Overview wraps agency in "Listed by" labelled block so it can't collapse to a bare agency name.
- B15 [P2] Compact POIDistanceBadges iterates all three travel modes with "—" for missing, matching the detail-sheet Travel table.
- B24 [P3] Drawer.Description (sr-only) added to ListingDetailSheet + MobileBottomSheet to silence Radix a11y warning.
- B25 [P3] lastSeenDays clamped to ≥0 so future timestamps don't render as "-7d ago".

## Frontend map / carousel / tasks polish (Fix-4) — 8 bugs

- B11 [P2] HexgridHeatmapClient destroy race: Map.tsx adds .catch() + ref guard so post-destroy promise rejections are silent no-ops. Verified by browser smoke (24 rapid Map↔List toggles → 0 pageErrors).
- B16 [P2] PhotoCarousel + inner CardCarousel gained keyboard nav (Arrow keys).
- B18 [P2] Default map center moved from Czech Republic to London (zoom 10).
- B19+B29 [P2/P3] Mapbox token: no longer hard-coded fallback; reads env-only and shows a clear "Map unavailable — set VITE_MAPBOX_TOKEN" banner when missing.
- B23 [P3] PhotoCarousel suppresses "1/1" counter for single-photo listings; added onError fallback for broken URLs.
- B26 [P3] PhotoCarousel only enables loop when photos.length > 1.
- B27 [P3] TaskIndicator cancel/clear-all buttons gained aria-label + data-testid.
- B28 [P3] useTaskProgress strips terminal-local task IDs from the polling union — no more forever-poll on completed tasks.

## Tests

74 new vitest tests + 18 new pytest tests. Local: tsc clean, 201 vitest tests pass, 633 pytest tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 22:27:29 +00:00
Viktor Barzin
0b5308200e wrongmove: guard property cards against null backend fields (fix BUY crash)
BUY listings can come back from the API with null `total_price`, `qm`,
`qmprice`, `rooms`, and `last_seen` even though the TypeScript types
declare them non-nullable. The cards called `.toLocaleString()` and
`.split('T')` on those values, throwing TypeError and tripping the
ErrorBoundary — which is the "BUY part of the page crashes" symptom.

Coerce numeric fields via a `safeNum` helper (0 fallback), gate the
"Nd ago" line on a finite last_seen, and apply the same guards in
PropertyCard, PropertyCardCompact, SwipeCard, and the price-history
section of ListingDetail. Added regression tests asserting both card
variants render with all-null backend fields without throwing.
2026-05-10 21:17:41 +00:00
Viktor Barzin
fad834c20b ui: harden a11y, plurals, NaN guards, map viewport persistence
Surgical UX/a11y fixes layered on top of the team's UI redesign,
keeping the teal palette and FilterBar architecture intact.

- TaskIndicator: NaN-safe progressPercentage (Number.isFinite +
  clamp 0..100). No more "NaN%" when a task posts undefined progress.
- StreamingProgressBar: same NaN guard on the inline width calc.
- StatsBar: pluralize listings ("1 listing" / "30 listings"), drop
  the duplicated "Avg:" label (now "Avg price" / "<n>/m²" / "Size"),
  tabular-nums on every numeric.
- FilterPanel furnishing pills: aria-pressed + data-state for
  screen readers and tests.
- ListView sort buttons: aria-pressed + aria-sort
  (ascending/descending/none); listing count pluralizes
  ("1 property" / "N properties") with tabular-nums.
- Map: only fitBounds the FIRST time data loads (hasFittedOnceRef).
  The previous lastDataLengthRef-based gate refit when results
  went N → 0 → M, blowing away the user's pan/zoom.

Verified: tsc --noEmit clean, 125/125 vitest specs pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 18:52:49 +00:00
Viktor Barzin
3016cbb047
fix: use real Rightmove listings for dev mock data
Replace picsum.photos placeholders with actual Rightmove listing data
fetched from the API - real property photos, prices, addresses, and
coordinates. Skip auto-load API call in dev bypass mode.
2026-02-28 16:47:52 +00:00
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
b720013a08
chore: add react-router-dom dependency for URL-based navigation 2026-02-28 15:58:39 +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
15dbcfa332
Parallelize frontend tests in Drone pipeline with 4 shards
Split the monolithic "build and test" kaniko step into a DAG:
tests run in 4 parallel shards (vitest --shard) alongside the
Docker image build, gated by a shared npm ci step. The kaniko
build now targets the named 'production' stage to skip the
in-Dockerfile test stage.
2026-02-21 21:02:22 +00:00
Viktor Barzin
a36c0dfdf7
Enable BuildKit in Drone API pipeline 2026-02-21 20:51:30 +00:00
Viktor Barzin
aea4b3c008
Expand frontend .dockerignore to exclude build artifacts 2026-02-21 19:51:04 +00:00
Viktor Barzin
231552c5e0
Remove unused frontend deps, move @types to devDependencies 2026-02-21 19:50:54 +00:00
Viktor Barzin
c762d5a0a6
Optimize frontend Dockerfile: npm cache mount, non-root nginx, healthcheck 2026-02-21 19:49:06 +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
b1be4d4170
perf: optimize CI pipeline — eliminate double dependency installs, use local registry cache
- Frontend Dockerfile: split into deps/test/builder/nginx stages so npm ci
  runs once (cached when package-lock.json unchanged), tests run in build
- Backend Dockerfile: add test stage that runs pytest inside the build,
  eliminating separate test image build
- .drone.yml: remove separate test steps (now inside Dockerfile builds),
  point cache_from/cache_repo at local registry (10.0.20.10:5000) instead
  of Docker Hub for faster layer cache pulls
2026-02-21 15:10:55 +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
e7eb8523cb
feat: add react-spring and use-gesture dependencies 2026-02-21 13:50:38 +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
8f068a581e
Fix frontend CI pipeline OOM kills and test timeouts
- Set memory limit to 2048MiB for the "Run frontend tests" step
  (node:24-alpine was OOM killed at the default 1Gi)
- Set memory limit to 2048MiB for the "Build frontend image" kaniko
  step (also OOM killed at 1Gi)
- Increase vitest testTimeout to 30s (CI runner is ~75x slower than
  local dev; tests were timing out at the default 5s)
2026-02-17 21:46:29 +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