Tints districts the user's Bezirk filter has EXCLUDED in light yellow
(#fde68a, fillOpacity 0.35) so the active selection is obvious from
the map alone — and you can see at a glance whether a "rausgefiltert"
flat fell in a no-go district. When no district filter is set the
overlay stays off entirely (nothing is excluded).
Wiring:
- Berlin Bezirke GeoJSON checked in at web/static/berlin-districts.geojson
(12 features, name property matches our DISTRICTS list 1:1, props
stripped down to {name} and minified — 312KB raw, ~80KB gzipped).
- Route exposes the user's selected_districts_csv to the template.
- The flats-map-data <script> carries it on data-selected-districts so
it flows in alongside csrf and the marker payload.
- map.js fetches the GeoJSON once (cache normally), keeps the layer in
a module-level reference, and re-styles it via setStyle() on every
swap (cheap). Marker layer is kicked to the front so pins always
paint above the shaded polygons. fingerprintOf now also folds in
the selected-districts CSV so a Bezirk-only filter change still
triggers a re-render.
Branch only — kept off main while we see if this reads well.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- notifications: round sqm_price to whole € in Telegram match messages
(was emitting raw float like "12.345614 €/m²").
- wohnungen: remove the admin-only "Bilder nachladen (N)" button. It
flickered into view whenever a freshly-scraped flat was still in
pending state, which was effectively random from the user's point of
view, and the manual backfill it triggered isn't needed anymore — new
flats are auto-enriched at scrape time. Also drops the dead helpers
it was the sole caller of: enrichment.kick_backfill,
enrichment._backfill_runner, db.flats_needing_enrichment,
db.enrichment_counts.
- lightbox: the modal didn't appear because Tailwind's Play CDN injects
its own .hidden { display: none } rule at runtime, which kept fighting
our class toggle. Switch the show/hide to inline style.display so no
external stylesheet can mask it. Single-class .lightbox now only owns
the layout — the initial-hidden state is on the element via
style="display:none".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Below "Abgelehnte Wohnungen", surface flats that survived the time
filter and aren't rejected but failed at least one of the user's
filters. Same collapsed-card style. Action buttons are replaced by
chips naming each failed dimension — "Zimmer", "Preis", "Größe",
"WBS", "Bezirk" — so it's obvious which constraint to relax.
Refactored matching: flat_matches_filter now delegates to a new
flat_filter_failures(flat, f) that returns the failed-dimension
labels (empty list = full match). rooms_min and rooms_max collapse
to a single "Zimmer" chip; reasons emit in stable _REASON_ORDER for
consistent rendering. The section is suppressed entirely when the
user has no filters set, since "everything matches" makes the chips
meaningless.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a collapsible 12-Bezirk checkbox list to the filter tab. The UI
speaks Bezirke; internally the match runs on the PLZ extracted from
flat.address and resolved to a dominant Bezirk via a curated 187-PLZ
map (berlin_districts.py).
- Migration 0011 adds user_filters.districts (CSV of selected names)
- Empty stored value = no filter = all Bezirke ticked in the UI.
Submitting "all ticked" or "none ticked" both normalise to empty
so the defaults and the nuclear state mean the same thing.
- When a Bezirk filter is active, flats with an unknown/unmapped PLZ
are excluded — if the user bothered to narrow by district, sneaking
in unplaceable flats would be the wrong default.
- Filter summary on the Wohnungen page shows "N Bezirke" so it's
visible the filter is active.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>