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>
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>
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>
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.
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>
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>
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.
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.
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.
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
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.
- 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)
- 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
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
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.
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).
- 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