diff --git a/detector/__main__.py b/detector/__main__.py index ae7f47e..243ce19 100644 --- a/detector/__main__.py +++ b/detector/__main__.py @@ -644,8 +644,8 @@ def main(argv: list[str] | None = None) -> int: help="Token zum Zurueckmelden (derselbe wie beim Einliefern)") q.add_argument("--lese-token", default=env("LESE_TOKEN"), help="Token zum Abrufen der offenen Fotos") - q.add_argument("--alle", type=float, default=60.0, - help="Sekunden zwischen zwei Abfragen (Standard 60)") + q.add_argument("--alle", type=float, default=5.0, + help="Sekunden zwischen zwei Abfragen (Standard 5)") q.add_argument("--anzahl", type=int, default=20, help="Hoechstens so viele Fotos je Durchlauf (Standard 20)") q.add_argument("--einmal", action="store_true", diff --git a/detector/warteschlange.py b/detector/warteschlange.py index afd8498..64ce784 100644 --- a/detector/warteschlange.py +++ b/detector/warteschlange.py @@ -16,6 +16,18 @@ Foto. Die ganze Logik aus ``watcher.py`` — Bewegung als Auslöser, Besuch übe viele Einzelbilder, Mittelwert der Vogel-Werte — hat hier keine Entsprechung und wäre bei einem Einzelbild sogar falsch. Benutzt wird deshalb direkt ``Pipeline.run()``, genau wie bei ``python -m detector test``. + +🔴 **Das Intervall ist eine Wartezeit vor den Augen eines Menschen, keine +Hintergrundaufgabe.** Es stand auf 60 Sekunden — gedacht als schonende +Hintergrundabfrage, tatsächlich aber die Zeit, die jemand vor dem Bildschirm +„Der Detektor schaut sich das an …" wartet, nachdem er ein Foto geschickt +hat. Christian hat das zweimal bemängelt, bevor es jemand als Fehler gelesen +hat. + +Jetzt 5 Sekunden, und ein Durchlauf, der etwas gefunden hat, fragt **sofort** +noch einmal nach: Mehrere Fotos hintereinander sollen nicht je 5 Sekunden +kosten. Die Abfrage ist ein kurzes JSON gegen die eigene Instanz — bei 5 +Sekunden sind das 720 Anfragen in der Stunde, für einen Django-Server nichts. """ from __future__ import annotations @@ -39,7 +51,7 @@ class Warteschlange: lese_token: str, schreib_token: str, pipeline: Pipeline, - intervall: float = 60.0, + intervall: float = 5.0, anzahl: int = 20, timeout: int = 20, ): diff --git a/web/dex/scoring.py b/web/dex/scoring.py index f141b5e..26fba7b 100644 --- a/web/dex/scoring.py +++ b/web/dex/scoring.py @@ -343,9 +343,22 @@ def evaluate_badges(sighting: Sighting) -> list[Badge]: species = sighting.effective_species if species: - if "specht" in species.slug: + if species.typ == "specht" or "specht" in species.slug: add("klopfgeist") - if species.rarity in ("episch", "legendaer"): + # 🔴 **Hier stand die Seltenheit statt der Vogelgruppe.** Das Abzeichen + # heißt "Greifvogel-Alarm" und verspricht "ein Greifvogel oder eine + # Eule vor der Linse" — vergeben wurde es für jede epische oder + # legendäre Art. Von 148 Arten im Katalog sind 21 Greifvögel oder + # Eulen, aber 75 weitere epische; die bekamen es zu Unrecht. + # + # Aufgefallen am 24.10.2026, als Christian in der App einen + # Weißstorch bestimmte und fragte, ob der ein Greifvogel sei. Der + # Befund stand seit dem 01.10.2026 in meinen Notizen und war als + # "Nebenbefund" liegengeblieben. + # + # 🔑 Eine Regel, die etwas anderes tut als ihre Beschreibung, ist + # nicht ungenau — sie sagt dem Nutzer etwas Falsches. + if species.typ in ("greifvogel", "eule"): add("greifvogel_alarm") if species.rarity in ("selten", "episch", "legendaer"): add("seltener_gast") diff --git a/web/dex/tests.py b/web/dex/tests.py index c0eafff..e20f691 100644 --- a/web/dex/tests.py +++ b/web/dex/tests.py @@ -181,6 +181,49 @@ class BadgeTests(TestCase): EarnedBadge.objects.filter(badge__code="erster_gast").count(), 1 ) + def test_greifvogel_alarm_braucht_einen_greifvogel(self): + """🔴 Die Regel hing an der Seltenheit statt an der Vogelgruppe. + + Das Abzeichen verspricht „ein Greifvogel oder eine Eule vor der + Linse" und wurde für jede epische Art vergeben — von 148 Arten sind + 21 Greifvögel oder Eulen, 75 weitere sind episch. Aufgefallen am + 24.10.2026 an einem Weißstorch. + """ + bussard = make_species( + slug="maeusebussard", german="Mäusebussard", + scientific="Buteo buteo", rarity="episch", typ="greifvogel", xp=45, + ) + scoring.register(make_sighting(bussard)) + self.assertTrue( + EarnedBadge.objects.filter(badge__code="greifvogel_alarm").exists() + ) + + def test_storch_bekommt_keinen_greifvogel_alarm(self): + storch = make_species( + slug="weissstorch", german="Weißstorch", + scientific="Ciconia ciconia", rarity="episch", + typ="schreitvogel", xp=150, + ) + scoring.register(make_sighting(storch)) + self.assertFalse( + EarnedBadge.objects.filter(badge__code="greifvogel_alarm").exists(), + "Ein Schreitvogel ist kein Greifvogel — auch nicht, wenn er selten ist", + ) + # „Seltener Gast" hängt zu Recht an der Seltenheit und bleibt. + self.assertTrue( + EarnedBadge.objects.filter(badge__code="seltener_gast").exists() + ) + + def test_eule_zaehlt_mit(self): + kauz = make_species( + slug="waldkauz", german="Waldkauz", scientific="Strix aluco", + rarity="selten", typ="eule", xp=40, + ) + scoring.register(make_sighting(kauz)) + self.assertTrue( + EarnedBadge.objects.filter(badge__code="greifvogel_alarm").exists() + ) + def test_rare_species_earns_rare_guest(self): gimpel = make_species( slug="gimpel", german="Gimpel", scientific="Pyrrhula pyrrhula",