Vogelalbum 1.0.0

Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine
Sammlung, die sich von allein fuellt.

Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die
Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert —
ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische
Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten,
gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte,
Stufen und 15 Abzeichen.

Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto,
keine Telemetrie; die Bilder verlassen das eigene Netz nicht.

Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die
Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und
braucht deshalb weder eine erreichbare Adresse noch einen offenen Port —
er laeuft hinter einem gewoehnlichen Router.

Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home
Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen
je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs.

AGPL-3.0.
This commit is contained in:
christian committed 2026-10-14 18:55:23 +00:00
commit cc9e0786c2
189 files changed
+29792

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
"""Dass die Absicherung im Betrieb auch wirklich anspringt.
🔑 **Warum das einen eigenen Test braucht.** Die Einstellungen hängen an
``DEBUG=0``, und in der Entwicklung *und in den Tests* ist ``DJANGO_DEBUG``
standardmäßig ``1``. Die ganze Absicherung läuft also in keinem einzigen
normalen Testlauf — sie stünde richtig in der Datei und wäre nie geprüft.
Genau die Sorte Sicherheitsnetz, die stumm kaputt ist.
Deshalb hier die Einstellungsdatei einmal frisch mit ``DJANGO_DEBUG=0``
einlesen und nachsehen, was dabei herauskommt.
"""
from __future__ import annotations
import importlib
import os
from unittest import mock
from django.test import SimpleTestCase
def betriebs_einstellungen():
"""Lädt ``vogelalbum.settings`` neu, als liefe es im Betrieb."""
umgebung = {
"DJANGO_DEBUG": "0",
# Im Betrieb ist ein eigener Schlüssel Pflicht, sonst bricht der
# Import ab — hier einer, der nur in diesem Test lebt.
"DJANGO_SECRET_KEY": "nur-fuer-diesen-test-" + "x" * 40,
}
with mock.patch.dict(os.environ, umgebung):
modul = importlib.import_module("vogelalbum.settings")
return importlib.reload(modul)
class BetriebsEinstellungenTests(SimpleTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.betrieb = betriebs_einstellungen()
@classmethod
def tearDownClass(cls):
# ⚠️ Zurückladen, sonst sieht jeder nachfolgende Test die
# Betriebsfassung — und scheitert an Dingen, die mit ihm nichts zu
# tun haben.
import vogelalbum.settings
importlib.reload(vogelalbum.settings)
super().tearDownClass()
def test_debug_ist_aus(self):
self.assertFalse(self.betrieb.DEBUG)
def test_cookies_nur_ueber_https(self):
"""🔴 Der Kern: Die Umleitung auf https kommt zu spät.
Traefik antwortet mit 307 — da hat der Browser das Cookie aber schon
mitgeschickt. Erst ``Secure`` hält ihn davon ab.
"""
self.assertTrue(self.betrieb.SESSION_COOKIE_SECURE)
self.assertTrue(self.betrieb.CSRF_COOKIE_SECURE)
def test_hsts_ist_gesetzt(self):
self.assertGreaterEqual(self.betrieb.SECURE_HSTS_SECONDS, 31536000)
def test_hsts_nicht_fuer_fremde_namen(self):
"""Keine Zusagen für Namen, die es nicht gibt, und kein preload.
``includeSubDomains`` gilt auch für Namen, die noch niemand angelegt
hat, und ``preload`` nimmt einem die Entscheidung aus der Hand: Aus
fremden Listen kommt man nur langsam wieder heraus.
"""
self.assertFalse(self.betrieb.SECURE_HSTS_INCLUDE_SUBDOMAINS)
self.assertFalse(self.betrieb.SECURE_HSTS_PRELOAD)
def test_proxy_kopf_wird_ausgewertet(self):
"""Ohne das baut build_absolute_uri() http://-Adressen."""
self.assertEqual(
self.betrieb.SECURE_PROXY_SSL_HEADER,
("HTTP_X_FORWARDED_PROTO", "https"),
)
def test_referrer_bleibt_im_haus(self):
self.assertEqual(self.betrieb.SECURE_REFERRER_POLICY, "same-origin")
def test_in_der_entwicklung_gilt_nichts_davon(self):
"""⚠️ Sonst kann sich auf http://localhost niemand mehr anmelden.
Ein ``Secure``-Cookie wird über einfaches HTTP gar nicht gesetzt — die
Anmeldung schlüge fehl, und die Ursache wäre ausgerechnet die
Einstellung, die Sicherheit bringen soll.
"""
from django.conf import settings
# ⚠️ ``settings.DEBUG`` taugt hier NICHT als Probe: Der Testläufer
# setzt es grundsätzlich auf False, egal was die Datei gerechnet hat.
# Entscheidend ist, was der ``if not DEBUG``-Block beim IMPORT gesehen
# hat — und das war ``DJANGO_DEBUG=1``. Also die Flaggen prüfen.
self.assertFalse(getattr(settings, "SESSION_COOKIE_SECURE", False))
self.assertFalse(getattr(settings, "CSRF_COOKIE_SECURE", False))
self.assertEqual(getattr(settings, "SECURE_HSTS_SECONDS", 0), 0)