wohnungen: drive list info from LLM JSON, tidy header
- Each row's info line now uses the enrichment JSON (rooms, size_sqm, rent_total/rent_cold, wbs_required/wbs_type). When enrichment is still running we show "Infos werden abgerufen…", on failure "Fehler beim Abrufen der Infos"; the scraper fields are no longer rendered in the list - Move the admin backfill button to the header row as a compact "Anreichern (N)" that only appears when there's pending/failed work, so it's findable right next to "X gefunden" - Countdown wobble: new .countdown class forces tabular-nums + 4.2em min-width, so neighbours stop shifting every second - Dot spacing: pull "·" out into its own .sep span so flex gap applies on both sides symmetrically Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
eb66284172
commit
e0ac869425
4 changed files with 65 additions and 19 deletions
10
web/app.py
10
web/app.py
|
|
@ -403,9 +403,16 @@ def _wohnungen_context(user) -> dict:
|
|||
}, filters):
|
||||
continue
|
||||
last = db.last_application_for_flat(uid, f["id"])
|
||||
flats_view.append({"row": f, "last": last})
|
||||
enrichment_data = None
|
||||
if f["enrichment_json"]:
|
||||
try:
|
||||
enrichment_data = json.loads(f["enrichment_json"])
|
||||
except Exception:
|
||||
enrichment_data = None
|
||||
flats_view.append({"row": f, "last": last, "enrichment": enrichment_data})
|
||||
|
||||
rejected_view = db.rejected_flats(uid)
|
||||
enrichment_counts = db.enrichment_counts()
|
||||
|
||||
allowed, reason = _manual_apply_allowed()
|
||||
alert_label, alert_chip = _alert_status(notif_row)
|
||||
|
|
@ -441,6 +448,7 @@ def _wohnungen_context(user) -> dict:
|
|||
return {
|
||||
"flats": flats_view,
|
||||
"rejected_flats": rejected_view,
|
||||
"enrichment_counts": enrichment_counts,
|
||||
"map_points": map_points,
|
||||
"has_filters": _has_filters(filters_row),
|
||||
"alert_label": alert_label,
|
||||
|
|
|
|||
17
web/db.py
17
web/db.py
|
|
@ -479,6 +479,23 @@ def flats_needing_enrichment(limit: int = 100) -> list[sqlite3.Row]:
|
|||
).fetchall())
|
||||
|
||||
|
||||
def enrichment_counts() -> dict:
|
||||
row = _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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -81,7 +81,19 @@
|
|||
<div class="flex items-center gap-3 text-xs text-slate-500">
|
||||
<span>{{ flats|length }} gefunden</span>
|
||||
{% if next_scrape_utc %}
|
||||
<span>· nächste Aktualisierung <span data-countdown-utc="{{ next_scrape_utc }}">…</span></span>
|
||||
<span class="sep">·</span>
|
||||
<span>nächste Aktualisierung <span class="countdown" data-countdown-utc="{{ next_scrape_utc }}">…</span></span>
|
||||
{% 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="Altbestand jetzt durch Haiku nachträglich anreichern? Kann einige Minuten dauern.">
|
||||
Anreichern ({{ enrichment_counts.pending + enrichment_counts.failed }})
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="view-toggle ml-2">
|
||||
<label>
|
||||
|
|
@ -126,11 +138,26 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
<div class="text-xs text-slate-500 mt-0.5">
|
||||
{% if f.rooms %}{{ "%.1f"|format(f.rooms) }} Z{% endif %}
|
||||
{% if f.size %} · {{ "%.0f"|format(f.size) }} m²{% endif %}
|
||||
{% if f.total_rent %} · {{ "%.0f"|format(f.total_rent) }} €{% endif %}
|
||||
{% if f.wbs %} · WBS: {{ f.wbs }}{% endif %}
|
||||
{% if f.enrichment_status == 'pending' %}
|
||||
Infos werden abgerufen…
|
||||
· <span data-rel-utc="{{ f.discovered_at|iso_utc }}" title="{{ f.discovered_at|de_dt }}">…</span>
|
||||
{% elif f.enrichment_status == 'failed' %}
|
||||
Fehler beim Abrufen der Infos
|
||||
· <span data-rel-utc="{{ f.discovered_at|iso_utc }}" title="{{ f.discovered_at|de_dt }}">…</span>
|
||||
{% else %}
|
||||
{% set e = item.enrichment or {} %}
|
||||
{% set parts = [] %}
|
||||
{% if e.rooms %}{% set _ = parts.append('%g Z'|format(e.rooms)) %}{% endif %}
|
||||
{% if e.size_sqm %}{% set _ = parts.append('%.0f m²'|format(e.size_sqm)) %}{% endif %}
|
||||
{% set rent = e.rent_total or e.rent_cold %}
|
||||
{% if rent %}{% set _ = parts.append('%.0f €'|format(rent)) %}{% endif %}
|
||||
{% if e.wbs_required is true %}
|
||||
{% set _ = parts.append('WBS: ' ~ (e.wbs_type or 'erforderlich')) %}
|
||||
{% elif e.wbs_required is false %}
|
||||
{% set _ = parts.append('ohne WBS') %}
|
||||
{% endif %}
|
||||
{{ parts|join(' · ') }}{% if parts %} · {% endif %}<span data-rel-utc="{{ f.discovered_at|iso_utc }}" title="{{ f.discovered_at|de_dt }}">…</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
|
|
@ -176,18 +203,6 @@
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{% if is_admin %}
|
||||
<section class="flex justify-end">
|
||||
<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="Altbestand jetzt durch Haiku nachträglich anreichern? Kann einige Minuten dauern.">
|
||||
Altbestand anreichern
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
{% if rejected_flats %}
|
||||
<section class="card">
|
||||
|
|
|
|||
|
|
@ -48,6 +48,12 @@
|
|||
transition: border-color .15s, box-shadow .15s; }
|
||||
.input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(47,138,224,.18); }
|
||||
.chip { padding: .2rem .7rem; border-radius: 999px; font-size: .75rem; font-weight: 500; display: inline-block; }
|
||||
/* Countdown — tabular digits + fixed width so the text doesn't wobble
|
||||
while the seconds tick down (3 → 10 → 59 → 1 min). */
|
||||
.countdown { font-variant-numeric: tabular-nums; display: inline-block;
|
||||
min-width: 4.2em; text-align: left; }
|
||||
/* Neutral separator in flex rows so the '·' has equal gap on both sides. */
|
||||
.sep { color: var(--muted); user-select: none; }
|
||||
.chip-ok { background: #e4f6ec; color: #1f8a4a; border: 1px solid #b7e4c7; }
|
||||
.chip-warn { background: #fff4dd; color: #a36a1f; border: 1px solid #f5d48b; }
|
||||
.chip-bad { background: #fde6e9; color: #b8404e; border: 1px solid #f5b5bf; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue