internal.py's flat-match path fed the raw scraper payload into flat_matches_filter, which has no "district" key. Combined with the match rule "active districts filter + unknown district → reject", this meant any user with a non-empty districts filter stopped receiving match notifications as soon as the 0011 migration ran. Extract the Bezirk once from payload.address before the per-user loop, so all users' filter evaluations see a concrete district. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
84 lines
2.8 KiB
Python
84 lines
2.8 KiB
Python
"""Internal service-to-service endpoints. Authenticated via INTERNAL_API_KEY
|
|
header; never called by browsers."""
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
import db
|
|
import enrichment
|
|
import notifications
|
|
from berlin_districts import district_for_address
|
|
from common import _auto_apply_allowed, _kick_apply, require_internal
|
|
from matching import flat_matches_filter, row_to_dict
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/internal/flats")
|
|
async def internal_submit_flat(
|
|
payload: dict,
|
|
_guard: None = Depends(require_internal),
|
|
):
|
|
if not payload.get("id") or not payload.get("link"):
|
|
raise HTTPException(400, "id and link required")
|
|
|
|
is_new = db.upsert_flat(payload)
|
|
if not is_new:
|
|
return {"status": "duplicate"}
|
|
|
|
# Kick LLM enrichment + image download for this fresh flat.
|
|
enrichment.kick(str(payload["id"]))
|
|
|
|
# Derive the Bezirk once so the districts filter can apply. Without
|
|
# this, flat_matches_filter sees district=None for every incoming
|
|
# flat and excludes it whenever a user has an active districts filter.
|
|
match_payload = dict(payload)
|
|
match_payload["district"] = district_for_address(payload.get("address"))
|
|
|
|
for u in db.list_users():
|
|
if u["disabled"]:
|
|
continue
|
|
filters = row_to_dict(db.get_filters(u["id"]))
|
|
if not flat_matches_filter(match_payload, filters):
|
|
continue
|
|
|
|
db.log_audit("alert", "flat_matched",
|
|
f"user={u['username']} flat={payload['id']}",
|
|
user_id=u["id"])
|
|
notifications.on_match(u["id"], payload)
|
|
|
|
prefs = db.get_preferences(u["id"])
|
|
if _auto_apply_allowed(prefs):
|
|
_kick_apply(u["id"], str(payload["id"]), payload["link"], "auto")
|
|
db.log_audit("system", "auto_apply_kick",
|
|
f"user={u['username']} flat={payload['id']}",
|
|
user_id=u["id"])
|
|
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.post("/internal/heartbeat")
|
|
async def internal_heartbeat(payload: dict, _g: None = Depends(require_internal)):
|
|
service = payload.get("service", "unknown")
|
|
db.set_state(f"last_{service}_heartbeat", db.now_iso())
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.post("/internal/error")
|
|
async def internal_report_error(
|
|
payload: dict,
|
|
_g: None = Depends(require_internal),
|
|
):
|
|
db.log_error(
|
|
source=payload.get("source", "unknown"),
|
|
kind=payload.get("kind", "error"),
|
|
summary=payload.get("summary", ""),
|
|
context=payload.get("context"),
|
|
)
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.get("/internal/secrets")
|
|
async def internal_secrets(_g: None = Depends(require_internal)):
|
|
"""Give sibling services (alert) the current runtime creds that the admin
|
|
may have edited via the UI, so no redeploy is needed when rotating."""
|
|
return db.all_secrets()
|