fix: round €/m² in Telegram, drop "Bilder nachladen" admin button, fix lightbox visibility

- 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>
This commit is contained in:
EiSiMo 2026-04-23 12:48:14 +02:00
parent 787f848aba
commit ee7ba6c6ff
8 changed files with 14 additions and 76 deletions

View file

@ -596,32 +596,6 @@ def set_flat_enrichment(flat_id: str, status: str,
) )
def flats_needing_enrichment(limit: int = 100) -> list[sqlite3.Row]:
return list(_get_conn().execute(
"""SELECT id, link FROM flats
WHERE enrichment_status IN ('pending', 'failed')
ORDER BY discovered_at DESC LIMIT ?""",
(limit,),
).fetchall())
def enrichment_counts() -> dict:
row = _get_conn().execute(
"""SELECT
COUNT(*) AS total,
SUM(CASE WHEN enrichment_status = 'ok' THEN 1 ELSE 0 END) AS ok,
SUM(CASE WHEN enrichment_status = 'pending' THEN 1 ELSE 0 END) AS pending,
SUM(CASE WHEN enrichment_status = 'failed' THEN 1 ELSE 0 END) AS failed
FROM flats"""
).fetchone()
return {
"total": int(row["total"] or 0),
"ok": int(row["ok"] or 0),
"pending": int(row["pending"] or 0),
"failed": int(row["failed"] or 0),
}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Applications # Applications
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -232,19 +232,3 @@ def _spawn(coro) -> asyncio.Task:
def kick(flat_id: str) -> None: def kick(flat_id: str) -> None:
_spawn(asyncio.to_thread(enrich_flat_sync, flat_id)) _spawn(asyncio.to_thread(enrich_flat_sync, flat_id))
async def _backfill_runner() -> None:
rows = db.flats_needing_enrichment(limit=200)
logger.info("enrich backfill: %d flats queued", len(rows))
for row in rows:
try:
await asyncio.to_thread(enrich_flat_sync, row["id"])
except Exception:
logger.exception("backfill step failed flat=%s", row["id"])
def kick_backfill() -> int:
pending = db.flats_needing_enrichment(limit=200)
_spawn(_backfill_runner())
return len(pending)

View file

@ -118,7 +118,7 @@ def on_match(user_id: int, flat: dict) -> None:
rent_str = _fmt_num(rent) rent_str = _fmt_num(rent)
if sqm_price: if sqm_price:
rent_str += f" ({sqm_price} €/m²)" rent_str += f" ({sqm_price:.0f} €/m²)"
body = ( body = (
f"{line1}\n{line2}\n" f"{line1}\n{line2}\n"

View file

@ -79,7 +79,6 @@ def _wohnungen_context(user) -> dict:
flats_view.append({"row": f, "last": latest_apps.get(f["id"])}) flats_view.append({"row": f, "last": latest_apps.get(f["id"])})
rejected_view = db.rejected_flats(uid) rejected_view = db.rejected_flats(uid)
enrichment_counts = db.enrichment_counts()
partner = db.get_partner_user(uid) partner = db.get_partner_user(uid)
partner_info = None partner_info = None
@ -133,7 +132,6 @@ def _wohnungen_context(user) -> dict:
"flats": flats_view, "flats": flats_view,
"rejected_flats": rejected_view, "rejected_flats": rejected_view,
"filtered_out_flats": filtered_out_view, "filtered_out_flats": filtered_out_view,
"enrichment_counts": enrichment_counts,
"partner": partner_info, "partner": partner_info,
"map_points": map_points, "map_points": map_points,
"has_filters": _has_filters(filters_row), "has_filters": _has_filters(filters_row),
@ -360,19 +358,6 @@ async def action_submit_forms(
return RedirectResponse(request.headers.get("referer", "/einstellungen/profil"), status_code=303) return RedirectResponse(request.headers.get("referer", "/einstellungen/profil"), status_code=303)
@router.post("/actions/enrich-all")
async def action_enrich_all(
request: Request,
csrf: str = Form(...),
admin=Depends(require_admin),
):
require_csrf(admin["id"], csrf)
queued = enrichment.kick_backfill()
db.log_audit(admin["username"], "enrichment.backfill",
f"queued={queued}", user_id=admin["id"], ip=client_ip(request))
return _wohnungen_partial_or_redirect(request, admin)
@router.post("/actions/enrich-flat") @router.post("/actions/enrich-flat")
async def action_enrich_flat( async def action_enrich_flat(
request: Request, request: Request,

View file

@ -138,9 +138,8 @@ body:has(#v_map:checked) .view-map { display: block; }
image. Prev arrow is hidden on the first image; next arrow on the last. */ image. Prev arrow is hidden on the first image; next arrow on the last. */
.lightbox { position: fixed; inset: 0; z-index: 1000; .lightbox { position: fixed; inset: 0; z-index: 1000;
background: rgba(8, 18, 32, .92); background: rgba(8, 18, 32, .92);
display: flex; align-items: center; justify-content: center; align-items: center; justify-content: center;
animation: lightbox-fade .15s ease-out; } animation: lightbox-fade .15s ease-out; }
.lightbox.hidden { display: none; }
.lightbox-image { max-width: 92vw; max-height: 88vh; object-fit: contain; .lightbox-image { max-width: 92vw; max-height: 88vh; object-fit: contain;
border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,.5); border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,.5);
background: #0c1726; } background: #0c1726; }

View file

@ -131,9 +131,16 @@ document.addEventListener("DOMContentLoaded", openDeepLinkedFlat);
// Image lightbox — single global modal in base.html, opened by clicking any // Image lightbox — single global modal in base.html, opened by clicking any
// .flat-gallery-tile. Click handler is delegated so it survives HTMX swaps. // .flat-gallery-tile. Click handler is delegated so it survives HTMX swaps.
// Visibility is driven by inline style.display rather than a `hidden` class
// because Tailwind's CDN injects its own `.hidden { display: none }` rule
// at runtime, which conflicted with our class toggle and kept the modal
// invisible after open().
(function () { (function () {
const overlay = document.getElementById("lazyflat-lightbox"); const overlay = document.getElementById("lazyflat-lightbox");
if (!overlay) return; if (!overlay) {
console.warn("[lazyflat.lightbox] #lazyflat-lightbox not in DOM; viewer disabled");
return;
}
const imgEl = overlay.querySelector(".lightbox-image"); const imgEl = overlay.querySelector(".lightbox-image");
const counterEl = overlay.querySelector(".lightbox-counter"); const counterEl = overlay.querySelector(".lightbox-counter");
const prevBtn = overlay.querySelector("[data-lightbox-prev]"); const prevBtn = overlay.querySelector("[data-lightbox-prev]");
@ -154,14 +161,14 @@ document.addEventListener("DOMContentLoaded", openDeepLinkedFlat);
if (!list.length) return; if (!list.length) return;
urls = list; urls = list;
idx = Math.max(0, Math.min(startIdx | 0, urls.length - 1)); idx = Math.max(0, Math.min(startIdx | 0, urls.length - 1));
overlay.classList.remove("hidden"); overlay.style.display = "flex";
overlay.setAttribute("aria-hidden", "false"); overlay.setAttribute("aria-hidden", "false");
document.body.classList.add("lightbox-open"); document.body.classList.add("lightbox-open");
render(); render();
} }
function close() { function close() {
overlay.classList.add("hidden"); overlay.style.display = "none";
overlay.setAttribute("aria-hidden", "true"); overlay.setAttribute("aria-hidden", "true");
document.body.classList.remove("lightbox-open"); document.body.classList.remove("lightbox-open");
imgEl.removeAttribute("src"); imgEl.removeAttribute("src");
@ -182,7 +189,7 @@ document.addEventListener("DOMContentLoaded", openDeepLinkedFlat);
if (ev.target === overlay) close(); if (ev.target === overlay) close();
}); });
document.addEventListener("keydown", (ev) => { document.addEventListener("keydown", (ev) => {
if (overlay.classList.contains("hidden")) return; if (overlay.style.display === "none") return;
if (ev.key === "Escape") close(); if (ev.key === "Escape") close();
else if (ev.key === "ArrowLeft") step(-1); else if (ev.key === "ArrowLeft") step(-1);
else if (ev.key === "ArrowRight") step(1); else if (ev.key === "ArrowRight") step(1);

View file

@ -84,17 +84,6 @@
<span class="sep">·</span> <span class="sep">·</span>
<span>aktualisiert <span class="countdown" data-counter-up-utc="{{ last_scrape_utc }}"></span></span> <span>aktualisiert <span class="countdown" data-counter-up-utc="{{ last_scrape_utc }}"></span></span>
{% endif %} {% endif %}
{% if is_admin and (enrichment_counts.pending or enrichment_counts.failed) %}
<span class="sep">·</span>
<form method="post" action="/actions/enrich-all"
hx-post="/actions/enrich-all" hx-target="#wohnungen-body" hx-swap="outerHTML">
<input type="hidden" name="csrf" value="{{ csrf }}">
<button class="btn btn-ghost text-xs" type="submit"
hx-confirm="Bilder für ausstehende Wohnungen nachladen? Kann einige Minuten dauern.">
Bilder nachladen ({{ enrichment_counts.pending + enrichment_counts.failed }})
</button>
</form>
{% endif %}
<div class="view-toggle ml-2"> <div class="view-toggle ml-2">
<label> <label>
<input type="radio" name="view_mode" id="v_list" value="list" checked> <input type="radio" name="view_mode" id="v_list" value="list" checked>

View file

@ -20,7 +20,7 @@
{% block body %}{% endblock %} {% block body %}{% endblock %}
{# Image lightbox — global so any flat-gallery on any page reuses the same modal. #} {# Image lightbox — global so any flat-gallery on any page reuses the same modal. #}
<div id="lazyflat-lightbox" class="lightbox hidden" aria-hidden="true" role="dialog" aria-label="Bildansicht"> <div id="lazyflat-lightbox" class="lightbox" style="display:none" aria-hidden="true" role="dialog" aria-label="Bildansicht">
<button class="lightbox-close" type="button" aria-label="Schließen" data-lightbox-close> <button class="lightbox-close" type="button" aria-label="Schließen" data-lightbox-close>
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg> <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="6" y1="6" x2="18" y2="18"/><line x1="18" y1="6" x2="6" y2="18"/></svg>
</button> </button>