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:
parent
64439fd42e
commit
abd614604f
3 changed files with 53 additions and 5 deletions
|
|
@ -202,6 +202,23 @@ def _has_running_application(user_id: int) -> bool:
|
||||||
return db.has_running_application(user_id)
|
return db.has_running_application(user_id)
|
||||||
|
|
||||||
|
|
||||||
|
# Apply returns success=False with one of these phrases when the listing has
|
||||||
|
# been taken down or deactivated. We treat that as "the flat is gone" rather
|
||||||
|
# than "the apply pipeline is broken" — no circuit-breaker hit, no fail
|
||||||
|
# notification, and the flat is hidden from every user's list.
|
||||||
|
_OFFLINE_MESSAGE_MARKERS = (
|
||||||
|
"inserat offline", "inserat deaktiviert", # de
|
||||||
|
"ad offline", "ad deactivated", # en
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_offline_result(message: str) -> bool:
|
||||||
|
if not message:
|
||||||
|
return False
|
||||||
|
m = message.lower()
|
||||||
|
return any(marker in m for marker in _OFFLINE_MESSAGE_MARKERS)
|
||||||
|
|
||||||
|
|
||||||
def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,
|
def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,
|
||||||
profile: dict, submit_forms: bool) -> None:
|
profile: dict, submit_forms: bool) -> None:
|
||||||
"""Called on a worker thread AFTER the application row already exists.
|
"""Called on a worker thread AFTER the application row already exists.
|
||||||
|
|
@ -218,8 +235,14 @@ def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,
|
||||||
db.finish_application(app_id, success=success, message=message,
|
db.finish_application(app_id, success=success, message=message,
|
||||||
provider=provider, forensics=forensics)
|
provider=provider, forensics=forensics)
|
||||||
|
|
||||||
|
offline = not success and _is_offline_result(message)
|
||||||
|
if offline:
|
||||||
|
db.mark_flat_offline(flat_id)
|
||||||
|
|
||||||
prefs = db.get_preferences(user_id)
|
prefs = db.get_preferences(user_id)
|
||||||
if success:
|
if success or offline:
|
||||||
|
# Offline isn't a pipeline failure — reset the counter so a streak of
|
||||||
|
# stale listings doesn't trip the circuit breaker.
|
||||||
db.update_preferences(user_id, {"apply_recent_failures": 0})
|
db.update_preferences(user_id, {"apply_recent_failures": 0})
|
||||||
else:
|
else:
|
||||||
failures = int(prefs["apply_recent_failures"] or 0) + 1
|
failures = int(prefs["apply_recent_failures"] or 0) + 1
|
||||||
|
|
@ -241,10 +264,12 @@ def _finish_apply_background(app_id: int, user_id: int, flat_id: str, url: str,
|
||||||
"total_rent": flat["total_rent"] if flat else None}
|
"total_rent": flat["total_rent"] if flat else None}
|
||||||
if success:
|
if success:
|
||||||
notifications.on_apply_ok(user_id, flat_dict, message)
|
notifications.on_apply_ok(user_id, flat_dict, message)
|
||||||
else:
|
elif not offline:
|
||||||
notifications.on_apply_fail(user_id, flat_dict, message)
|
notifications.on_apply_fail(user_id, flat_dict, message)
|
||||||
|
|
||||||
db.log_audit("system", "apply_finished", f"app={app_id} success={success}", user_id=user_id)
|
outcome = "success" if success else ("offline" if offline else "failure")
|
||||||
|
db.log_audit("system", "apply_finished",
|
||||||
|
f"app={app_id} outcome={outcome}", user_id=user_id)
|
||||||
|
|
||||||
|
|
||||||
def _kick_apply(user_id: int, flat_id: str, url: str, triggered_by: str) -> None:
|
def _kick_apply(user_id: int, flat_id: str, url: str, triggered_by: str) -> None:
|
||||||
|
|
|
||||||
18
web/db.py
18
web/db.py
|
|
@ -265,6 +265,11 @@ MIGRATIONS: list[str] = [
|
||||||
CREATE INDEX IF NOT EXISTS idx_applications_user_flat_started
|
CREATE INDEX IF NOT EXISTS idx_applications_user_flat_started
|
||||||
ON applications(user_id, flat_id, started_at DESC);
|
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]:
|
def recent_flats(limit: int = 50) -> list[sqlite3.Row]:
|
||||||
return list(_get_conn().execute(
|
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())
|
).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]:
|
def get_flat(flat_id: str) -> Optional[sqlite3.Row]:
|
||||||
return _get_conn().execute("SELECT * FROM flats WHERE id = ?", (flat_id,)).fetchone()
|
return _get_conn().execute("SELECT * FROM flats WHERE id = ?", (flat_id,)).fetchone()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,14 @@
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<div class="flex items-center gap-2 flex-wrap">
|
<div class="flex items-center gap-2 flex-wrap">
|
||||||
{% if a.success == 1 %}<span class="chip chip-ok">ok</span>
|
{% if a.success == 1 %}<span class="chip chip-ok">ok</span>
|
||||||
{% elif a.success == 0 %}<span class="chip chip-bad">fehlgeschlagen</span>
|
{% elif a.success == 0 %}
|
||||||
|
{% set msg_lower = (a.message or '')|lower %}
|
||||||
|
{% if 'inserat offline' in msg_lower or 'inserat deaktiviert' in msg_lower
|
||||||
|
or 'ad offline' in msg_lower or 'ad deactivated' in msg_lower %}
|
||||||
|
<span class="chip chip-warn">offline</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="chip chip-bad">fehlgeschlagen</span>
|
||||||
|
{% endif %}
|
||||||
{% else %}<span class="chip chip-warn">läuft</span>{% endif %}
|
{% else %}<span class="chip chip-warn">läuft</span>{% endif %}
|
||||||
<span class="chip chip-info">{% if a.triggered_by == 'auto' %}automatisch{% else %}manuell{% endif %}</span>
|
<span class="chip chip-info">{% if a.triggered_by == 'auto' %}automatisch{% else %}manuell{% endif %}</span>
|
||||||
{% if a.provider %}<span class="chip chip-info">{{ a.provider }}</span>{% endif %}
|
{% if a.provider %}<span class="chip chip-info">{{ a.provider }}</span>{% endif %}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue