feat(apply): treat "Inserat offline" as its own outcome

Offline listings are a different class of result from real failures:
they mean "this ad is gone", not "the apply pipeline is broken".

- DB migration 0010 adds flats.offline_at; recent_flats() filters those
  out globally so they drop off every user's Wohnungen list
- _is_offline_result() matches the four known offline/deactivated
  phrases (DE + EN translations)
- On an offline result: mark the flat, reset the failure counter
  instead of incrementing, and skip the apply_fail notification
- Bewerbungen history renders a yellow "offline" chip in place of the
  red "fehlgeschlagen" one

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
EiSiMo 2026-04-23 09:56:04 +02:00
parent 64439fd42e
commit abd614604f
3 changed files with 53 additions and 5 deletions

View file

@ -265,6 +265,11 @@ MIGRATIONS: list[str] = [
CREATE INDEX IF NOT EXISTS idx_applications_user_flat_started
ON applications(user_id, flat_id, started_at DESC);
""",
# 0010: flats go offline — mark globally so they drop out of every user's list
"""
ALTER TABLE flats ADD COLUMN offline_at TEXT;
CREATE INDEX IF NOT EXISTS idx_flats_offline ON flats(offline_at);
""",
]
@ -551,10 +556,21 @@ def upsert_flat(payload: dict) -> bool:
def recent_flats(limit: int = 50) -> list[sqlite3.Row]:
return list(_get_conn().execute(
"SELECT * FROM flats ORDER BY discovered_at DESC LIMIT ?", (limit,)
"SELECT * FROM flats WHERE offline_at IS NULL "
"ORDER BY discovered_at DESC LIMIT ?", (limit,)
).fetchall())
def mark_flat_offline(flat_id: str) -> None:
"""Flag a flat as no longer reachable. Idempotent — the first offline
timestamp wins so we can tell how long it's been gone."""
with _lock:
_get_conn().execute(
"UPDATE flats SET offline_at = ? WHERE id = ? AND offline_at IS NULL",
(now_iso(), flat_id),
)
def get_flat(flat_id: str) -> Optional[sqlite3.Row]:
return _get_conn().execute("SELECT * FROM flats WHERE id = ?", (flat_id,)).fetchone()