Commit graph

527 commits

Author SHA1 Message Date
Viktor Barzin
01a940b9b6 wrongmove: fix DetachedInstanceError in daily market aggregator task
`compute_aggregate_snapshot` returns ORM rows that were created inside
a `with Session(engine)` block — by the time the Celery task tries to
serialise their attributes into the result dict the session has closed,
triggering SQLAlchemy's DetachedInstanceError. Combined with acks_late
this caused the task to be redelivered repeatedly (4× in the first
manual trigger).

Fix: drop the per-row dict-serialisation in the task return — keep just
`aggregates_written: int`. The per-band stats are already logged by the
aggregator's own info-level lines, so no observability is lost.

Caught when manually firing the task on prod to seed today's snapshot
before the 04:00 UTC daily fire. Aggregator itself ran fine (the rows
were written before the session closed); only the post-return access
was broken.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:53:26 +00:00
Viktor Barzin
9fdf7fd356 wrongmove: stabilise setup_periodic_tasks tests after market-aggregator add
CI test-unit failed on pipeline #49 because the three TestSetupPeriodicTasks
cases asserted exact call counts on `sender.add_periodic_task` and the
new unconditional `daily-market-aggregator` registration bumped each by
one. Fix:

- `tasks/listing_tasks.py`: lifted the market-aggregator registration
  out of the SchedulesConfig try-block — it's now independent of the
  user's SCRAPE_SCHEDULES (a malformed scrape config no longer takes
  the aggregator down with it).
- `tests/unit/test_listing_tasks.py`: updated the four cases to account
  for the +1 unconditional aggregator call. `test_handles_config_error_
  gracefully` now asserts the aggregator still registers when
  SchedulesConfig.from_env raises (regression coverage for the
  independence guarantee).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:07:50 +00:00
Viktor Barzin
49e3514780 wrongmove: daily price-trend monitoring (per-listing badge + macro strip)
Two surfaces wired up so the user can "get a vibe of the market":

**Per-listing** — each PropertyCard now shows a small pill next to the
price when the listing's total_price moved >=1% over a 14-day lookback
(e.g. "↓ £200 (-4%) in 14d"). Drops render green, rises render red.
Computed from `price_history_json` by the daily aggregator and
denormalised onto the listing row so the streaming endpoint just
passes it through.

**Macro** — new always-visible inline strip above the chip strip
showing today's median total price, median £/m², and listing count
for the current filter's bedroom band, each with a 30-day % delta:
"Rent · 1-2 bed · 30d: Median £2,500 ↓ -4% · £/m² £50 ↓ -2% · Listings 4,200 ↑ +5%".

Both data sources are populated daily at 04:00 UTC by a new Celery
beat task that fires 1h after the 03:00 RENT scrape and feeds two
sinks: a per-listing update pass and an upsert to a new
`dailylistingaggregate` table keyed on
(snapshot_date, listing_type, min_bedrooms, max_bedrooms).

## Backend
- `models/listing.py`: Listing parent gains `price_14d_ago` + `price_
  change_pct_14d` nullable floats (inherited by RentListing/BuyListing).
  New `DailyListingAggregate` table model with unique constraint on
  (date, type, min_bed, max_bed).
- Alembic `a8b9c0d1e2f3`: adds the two columns to both listing tables
  and creates the aggregate table + date index.
- `services/market_aggregator.py` (new): `compute_trend_for_listing`,
  `update_per_listing_trend` (batched, idempotent), `_stats` (median
  + mean filtered to positive finite values), `compute_aggregate_
  snapshot` (dialect-aware MySQL / SQLite upsert), `fetch_trend_
  series` (range query for the API).
- `tasks/market_tasks.py` (new): `compute_daily_market_aggregates_task`
  Celery task wrapping both stages.
- `tasks/listing_tasks.py:setup_periodic_tasks`: registers the daily
  task at 04:00 UTC alongside the existing scrape schedules.
- `celery_app.py`: includes the new tasks module.
- `api/app.py`: new `GET /api/market_trend?listing_type=&min_bedrooms=&
  max_bedrooms=&days=` endpoint returning the daily series.
- `ui_exporter.py`: GeoJSON feature properties now carry
  `price_14d_ago` and `price_change_pct_14d` so the frontend can
  render the badge without an extra round-trip.

## Frontend
- `types/index.ts`: new `MarketTrendPoint`; `PropertyProperties` gains
  the two optional trend fields.
- `components/PropertyCard.tsx`: derived `trendBadge` (>=1% threshold,
  null-safe) rendered as a small pill on both card variants.
- `hooks/useMarketTrend.ts` (new): fetches the trend series, derives
  current-vs-oldest deltas per metric (% change rounded to 1dp).
- `components/MarketTrendStrip.tsx` (new): compact inline strip with
  three metric cells. Hidden when the aggregator hasn't produced any
  rows yet (graceful start during the first week post-launch).
- `App.tsx`: renders the strip above the chip strip whenever the
  active queryParameters are known.

## Tests
- pytest: 10 new (trend math edge cases including null history,
  malformed JSON, only-recent entries, drops, rises, zero current
  price; _stats empty / nonpositive filtering; upsert idempotency on
  an in-memory SQLite seed). 34 decision + aggregator tests pass.
- vitest: 8 new (useMarketTrend fetch URL, two-point delta,
  single-point null delta, empty series; PropertyCard trend badge
  arrow direction + sign for drops/rises, noise threshold, null
  guard). 229 tests pass total, tsc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 12:02:25 +00:00
Viktor Barzin
c2e08fe46e wrongmove: add "seen" soft-hide decision with price-aware resurfacing
Opening a listing's detail sheet for >3s now passively marks it `seen`
and snapshots its current `total_price`. Seen listings are hidden from
the main list by default but automatically resurface when the price
changes (any direction). Distinct from `disliked` — explicit
like/dislike always overrides the passive seen state.

New "Hidden (N)" toggle on the FilterBar appears whenever at least one
listing is currently being hidden by the seen filter. Toggling it
reveals those rows in-place (without unmarking them) so the user can
review or explicitly clear via the existing Like/Dislike/Clear flow.

## Backend
- Alembic f7a8b9c0d1e2: `ALTER TABLE listingdecision ADD COLUMN
  price_at_decision FLOAT NULL`.
- `models/decision.py`: ListingDecision gains nullable
  `price_at_decision: float | None`.
- `services/decision_service.py`: adds `seen` to VALID_DECISIONS;
  set_decision accepts an optional `price_at_decision`; it's only
  forwarded to the repo for decision='seen' (other types null-out the
  column to keep semantics clean).
- `repositories/decision_repository.py`: upsert_decision now carries
  price_at_decision through the MySQL + SQLite upsert paths.
- `api/decision_routes.py`: SetDecisionRequest + DecisionResponse
  expose the new field.

## Frontend
- `types/index.ts`: DecisionType = 'liked' | 'disliked' | 'seen';
  ListingDecision gains `price_at_decision?: number | null`.
- `services/decisionService.ts`: setDecision sends the price only for
  decision='seen' (and only when it's a finite number).
- `hooks/useDecisions.ts`: rewritten to store `Map<key, DecisionEntry>`
  (decision + price snapshot). New `markSeen(id, price, type)` short-
  circuits on existing liked/disliked. New `getDecisionEntry`,
  `seenCount`.
- `App.tsx`: 3s `setTimeout` dwell timer fires markSeen when the
  detail sheet stays open. Filter logic in `processedListingData`
  hides `seen` rows whose `total_price === price_at_decision`, with
  `showHidden` bypass. Computes `hiddenCount` to drive the toggle.
- `components/FilterBar.tsx`: new conditional "Hidden (N)" / "Showing
  hidden (N)" toggle button (Eye / EyeOff lucide icons), surfaces only
  when hiddenCount > 0.

## Tests
- pytest: 2 new (test_set_seen_carries_price, test_liked_drops_price_
  even_if_supplied) + 1 updated to assert the new 5-arg repo
  signature. 24 passed.
- vitest: 6 new for useDecisions (markSeen liked/disliked skip, price
  snapshot, re-mark, null price, seenCount) + 5 new for decisionService
  payload shape. 221 total passed, tsc clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 11:07:44 +00:00
Viktor Barzin
9a5ad7878c wrongmove: preserve heatmap color on multi-property popup + arrow nav on card carousel
When the user clicks a heatmap hex that contains multiple properties,
each property card in the resulting popup now renders a left-edge
color stripe matching the heatmap gradient for that property's
individual value of the active metric (Price/m², Total Price, Size,
Bedrooms, …). The "color code" carries from the map into the popup
instead of dying at the hex boundary.

Plumbing:
- `colorSchemes.ts` gains `interpolateMetricColor(value, min, max, stops)`
  that walks the color-stop ramp and returns `rgb(R, G, B)`.
- `Map.tsx` stashes the latest `{min, max}` from `computeColorScale` in
  a ref so `getListingDialog` can compute per-property colors without
  re-running the worker.
- `PropertyCard` accepts an optional `metricColor` prop and applies it
  as a 4px `border-left`. Compact variant unchanged (no stripe).

Also resolves the Round-3 Fix-4 follow-up: `CardCarousel` (inside
PropertyCard.tsx) now has clickable prev/next chevron buttons in
addition to drag + keyboard navigation. Buttons fade in on hover
(group-hover) and are always focus-visible for keyboard users; clicks
stop propagation so the parent card click handler doesn't fire.

Tests: 9 new (4 covering interpolateMetricColor edge cases —
null/NaN/clamp — and 4 covering metricColor stripe + carousel
buttons present/absent). Full suite 210/210.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 23:10:20 +00:00
Viktor Barzin
9bb5320e2b wrongmove: write VITE_MAPBOX_TOKEN to .env.production in CI (replaces broken build_args)
The previous attempt passed the Mapbox token via `--build-arg`, but
the docker-buildx plugin's KEY=VALUE list-parser mangled the value
(the rendered command was `--build-arg *=VITE_MAPBOX_TOKEN=********`,
key got lost). Inspecting `viktorbarzin/immoweb:45` confirmed
`pk.eyJ...` was nowhere in the bundle.

Switching to the idiomatic Vite path: a new `prepare-frontend-env`
commands step writes `frontend/.env.production` from the
`wrongmove-mapbox-token` Woodpecker secret. `COPY . .` in the
Dockerfile pulls the file into the build context, and Vite
auto-loads `.env.production` during `npx vite build`.

Net diff:
- `.woodpecker/frontend.yml`: new prepare step, build step now
  depends on it, dropped the build_args line.
- `frontend/Dockerfile`: dropped the ARG/ENV lines (no longer needed,
  also silences `SecretsUsedInArgOrEnv` linter warning).
- `frontend/.gitignore`: ignore `.env.production` / `.env.local` so
  the CI-written file never gets accidentally committed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 22:10:25 +00:00
Viktor Barzin
d03a9a0fe2 wrongmove: inline VITE_MAPBOX_TOKEN as a build_arg (drop secret indirection)
The previous attempt used a step-level `environment:` block with
`from_secret:`, which the Woodpecker linter rejected on plugin steps
("Should not configure both `environment` and `settings`"). Net effect
was build-and-push-frontend reverted to a commands step and the
docker daemon never started.

The Mapbox `pk.*` token ends up baked into the public bundle anyway —
its security model is domain restrictions in the Mapbox dashboard, not
build-time secrecy. Inlining the value in `build_args` is the simplest
working path and avoids the secret-indirection footgun. The token also
still lives in Vault at `secret/ci/global/wrongmove-mapbox-token` for
the day we adopt a private style URL or replace this with a different
provider.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-15 21:54:40 +00:00
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
73823bd381 ci: bump frontend test-shard memory to 2Gi (fix exit-137 OOM)
The 4 vitest shards were getting SIGKILL (exit code 137) because the
container memory limit was tighter than NODE_OPTIONS=--max-old-space-size=1024,
and 1024 wasn't enough headroom for the test workers either.

Set explicit kubernetes resources (request 1Gi / limit 2Gi) and bump
the V8 heap to 1.5Gi on install-frontend-deps and all 4 test shards.

Confirmed-by: pipeline 2081 step states (test-shard-1..4 all
state=failure exit_code=137; build/deploy steps then skipped).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 19:43:38 +00:00
Viktor Barzin
4338a5c069 ci: retrigger frontend pipeline (queue still inconsistent) 2026-05-10 19:30:32 +00:00
Viktor Barzin
cca33829be ci: retrigger frontend pipeline (woodpecker queue inconsistency) 2026-05-10 19:18:36 +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
ebc58cd8a1
remove local agents, replaced by global dev team [ci skip] 2026-03-23 00:17:54 +02:00
Viktor Barzin
f5a92dc0c9
fix: increase install-api-deps memory to 1Gi for CI
LimitRange defaults to 192Mi which is too low for pip installing
~30 packages. Set 512Mi request / 1Gi limit.
2026-03-15 23:47:30 +00:00
Viktor Barzin
546f6957e3
ci: migrate from plugins/docker to woodpeckerci/plugin-docker-buildx
Replace the old Docker-in-Docker build approach (plugins/docker) with
the modern buildx plugin that natively supports BuildKit. This fixes:
- "Cannot connect to the Docker daemon" errors (no DinD needed)
- "the --mount option requires BuildKit" errors (buildx = BuildKit)
- OOM in publish step (skopeo no longer needed, buildx pushes directly)

Also removes the intermediate build-tag/skopeo-copy/publish dance —
buildx pushes both versioned and latest tags in a single step.
2026-03-15 22:52:01 +00:00
Viktor Barzin
55aff55025
ci: retrigger after woodpecker restart 2026-03-01 19:47:49 +00:00
Viktor Barzin
cfa8038bfa
ci: retrigger build 2026-03-01 19:16:11 +00:00
Viktor Barzin
94e3ec7408
ci: remove slack notification step (secret not configured) 2026-02-28 21:48:30 +00:00
Viktor Barzin
99e75177b5
ci: make slack notification non-blocking when secret is missing 2026-02-28 20:22:49 +00:00
Viktor Barzin
c5398d865a
ci: trigger build 2026-02-28 20:11:50 +00:00
Viktor Barzin
8fcd530d7f
feat: UI/UX redesign — map-first with modern chrome
- Horizontal filter bar replacing sidebar (full-width map)
- React Router with URL-based filter state and deep linking
- Teal-accented color palette
- Redesigned property cards with better visual hierarchy
- Tabbed listing detail sheet (Overview, Travel, Price History, Details)
- Error boundary
- Shared utility extraction (format, task utils)
2026-02-28 17:18:11 +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
706a60c741
docs: add UI/UX redesign implementation plan
11 tasks across 5 phases:
1. Foundation: Router, shared utils, color palette
2. Filter Bar: Replace sidebar with horizontal filter bar
3. Property Cards: Visual redesign with better hierarchy
4. Polish: Error boundary, cleanup
5. Verification: End-to-end visual check
2026-02-28 15:46:30 +00:00
Viktor Barzin
6f8eb8a0ba
docs: add UI/UX redesign design document
Approach C: Map-First with Modern Chrome
- Horizontal filter bar replacing sidebar
- React Router for URL state / deep linking
- Redesigned property cards with better visual hierarchy
- Refined color palette and typography
- Tabbed listing detail sheet
2026-02-28 15:44:13 +00:00
Viktor Barzin
c7c3331d30
Migrate CI from Drone to Woodpecker
Replace .drone.yml with .woodpecker/ pipeline configs (frontend.yml, api.yml).
Convert Drone env vars to Woodpecker equivalents (CI_PIPELINE_NUMBER, CI_COMMIT_SHA),
use woodpeckerci/plugin-git for clone with retry, woodpeckerci/plugin-slack for
notifications, and plugins/docker for image builds. Update all docs and skills.
2026-02-23 22:00:09 +00:00
Viktor Barzin
2357722e80
Add Server-Timing headers to all API endpoints for per-request latency breakdown
Instrument every API endpoint with Server-Timing headers so sub-operation
durations are visible in browser DevTools Network tab. Also adds Grafana
dashboard panels for per-endpoint latency comparison (p50/p95 timeseries
and p95 ranking bar gauge).
2026-02-23 21:30:51 +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
9b3d35669f
Fix API crash: status_code=204 with response body assertion error
FastAPI rejects status_code=204 on routes with a return type annotation.
Return an explicit Response(status_code=204) instead.
2026-02-23 19:22:10 +00:00
Viktor Barzin
7dc8dc736f Retrigger CI build 2026-02-22 23:59:21 +00:00
Viktor Barzin
6dcb0d8787 fix clone backoff duration format 2026-02-22 23:40:50 +00:00
Viktor Barzin
af29295793 fix clone backoff duration format 2026-02-22 23:40:49 +00:00
Viktor Barzin
f02e27e9ba test clone retry 2026-02-22 23:37:46 +00:00
Viktor Barzin
b2904c03c2 [ci skip] Add clone retry settings for DNS resilience 2026-02-22 23:37:21 +00:00
Viktor Barzin
c68841e499 [ci skip] Add clone retry settings for DNS resilience 2026-02-22 23:37:19 +00:00
Viktor Barzin
432ceb575d Remove custom clone override, use built-in Woodpecker clone 2026-02-22 23:30:38 +00:00