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:
parent
787f848aba
commit
ee7ba6c6ff
8 changed files with 14 additions and 76 deletions
26
web/db.py
26
web/db.py
|
|
@ -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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue