- Rewrite csv_exporter.py to use stdlib csv.DictWriter instead of pandas DataFrame
- Rewrite notifications.py to use aiohttp direct Slack webhook instead of apprise
- Switch opencv-python to opencv-python-headless in pyproject.toml
- Move httpx from dev to prod dependencies
- Remove pandas and apprise from mypy ignore_missing_imports
- Remove watchdog (unused) and tqdm from pyproject.toml dependencies
- Replace tqdm.gather() with asyncio.gather() + logger.info() in
image_fetcher, floorplan_detector, and route_calculator services
- Replace tqdm progress bar with logger.info() in listing_repository
- Remove tqdm from mypy ignore_missing_imports overrides
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.
Requests to Rightmove API previously had no explicit timeout, causing
hung connections to block workers indefinitely. Add a configurable
request_timeout (default 30s) to ScraperConfig and apply it to all
aiohttp sessions. Also retry on TimeoutError in addition to
ThrottlingError for all API query functions.
- Update test mocks from _get_disliked_ids to _get_user_id_safe
- Fix decision service test method names (clear_decision -> remove_decision, etc.)
- Fix positional vs keyword arg assertion in set_decision test
- Add decision_filter param to non-streaming listing_geojson endpoint
- ListingDecision model with unique constraint on (user_id, listing_id, listing_type)
- Alembic migration for listingdecision table
- DecisionRepository with dialect-aware upsert (MySQL/SQLite)
- DecisionService with input validation
- Decision API routes: PUT/GET/DELETE on /api/decisions
- GET /api/listing/{id}/detail endpoint extracting full property info from additional_info
- Add listing ID to GeoJSON feature properties
- Decision filtering on GeoJSON stream endpoint (decision_filter param)
- 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
Tests were failing because the new decision filtering code in api/app.py
tries to query the database for disliked IDs, but test fixtures that mock
the ListingRepository didn't also mock _get_disliked_ids. Additionally,
rate limiter was not bypassed in TestListingGeoJsonEndpoint client fixture,
causing 429s when tests run in sequence.
- 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
Both /api/listing_geojson and /api/listing_geojson/stream now exclude
disliked listings by default. A decision_filter='everything' param
bypasses filtering. 2 integration tests verify the behavior.
PUT /api/decisions/{listing_id} to set decision,
GET /api/decisions to list all user decisions,
DELETE /api/decisions/{listing_id} to remove a decision.
All 6 API route tests pass.
Add ListingDecision SQLModel entity for tracking user like/dislike
decisions on properties, with unique constraint on (user_id, listing_id,
listing_type) and appropriate indexes.
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
- 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)
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.
Disable the default clone and use a custom clone step with up to 5
retry attempts in both frontend and api pipelines. This prevents
builds from failing due to transient "Could not resolve host" errors.
worker_ready fires in the main process after pool workers are forked,
so init_metrics() never ran in the child processes. Add a
worker_process_init handler to initialize OTel metrics in each worker.
Hook lives in .githooks/pre-push (tracked) and runs pytest inside the
Docker app container. start.sh auto-configures core.hooksPath so new
clones pick it up on first run.
Type-annotated metric variables (e.g. `geojson_cache_operations: Counter`)
don't exist as importable names until init_metrics() runs. Switch all
`from api.metrics import <metric>` to `import api.metrics as m` and
access instruments as attributes at runtime to avoid ImportError.
Structured logging via JsonFormatter replaces uvicorn's default format so
Loki can parse timestamps and fields. 14 business metrics (scrape stats,
throttle events, circuit breaker state, cache hit rate, OCR success rate,
Celery task lifecycle) are defined in a shared metrics module and
instrumented across the scraper pipeline, API, and workers. Celery
workers expose a Prometheus HTTP endpoint on configurable ports.
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.
plugins/docker uses Docker-in-Docker which consumes significant
ephemeral storage for image layers. On nodes with disk pressure
(k8s-node1), the build pod gets evicted before completing.
Switch to plugins/kaniko which builds OCI images without a Docker
daemon, using significantly less ephemeral storage. Enable kaniko's
built-in layer caching via a dedicated cache repo.
Both frontend and API pipelines had separate "Cache builder stage"
steps that built and pushed intermediate Docker targets purely for
layer caching. Running these alongside the actual build steps doubled
the Docker build work per pipeline, causing pods to be evicted when
the node hit its ephemeral-storage threshold (~20GB).
The "Build image" steps already use cache_from with the existing
:builder tags from previous builds, so the separate cache steps
are redundant for the common case (unchanged package files).