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:
EiSiMo 2026-04-21 14:57:11 +02:00
parent eb66284172
commit e0ac869425
4 changed files with 65 additions and 19 deletions

View file

@ -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 %}
· <span data-rel-utc="{{ f.discovered_at|iso_utc }}" title="{{ f.discovered_at|de_dt }}"></span>
{% 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">

View file

@ -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; }