DMN/Software

ProjekteFRITZ!Box-Verlauf

FRITZ!Box-Verlauf

Die FRITZ!Box behält nur wenige hundert Protokollzeilen und kennt in ihrer Geräteliste nur den Zustand „jetzt“. Ein Collector in Go holt beides regelmäßig ab und schreibt jedes Verbinden und Trennen dauerhaft weg. Die Oberfläche zeigt dann über Monate, wann welches Gerät im Netz war.

Privat, nur im eigenen VPN erreichbar. Der Quellcode ist nicht öffentlich.

Detailseite des erfundenen Geräts Beispiel-Handy: online seit 2 Stunden, verbunden über WLAN am Repeater Flur. Darunter eine Leiste der letzten 7 Tage mit verbundenen Zeiten und einer markierten Lücke ohne Daten, eine Tabelle der Sitzungen mit Beginn, Ende und Dauer, rechts die Einstellungen für Mails bei Verbinden und Trennen und die zwei MAC-Adressen des Geräts
Ein Gerät mit Verlauf, Sitzungen und den Mails, die es auslöst. Alle Geräte, Adressen und Zeiten sind erfunden.

Eckdaten

Stand

Betrieb
seit 19.10.2026 auf dem eigenen Server, die Box hängt per WireGuard daran
Tests
108 Go-Tests, mit Untertests 246, dazu 212 pgTAP-Prüfungen der Datenbank, 92 Vitest-Fälle und 10 Playwright-Tests mit axe
Zugang
ein Besitzer, Anmeldung nur auf Einladung, keine Registrierung

Das Gerät ist die Einheit, nicht die Zeile

Aus Protokollzeilen und Geräteliste werden Ereignisse, aus Ereignissen Sitzungen mit Beginn, Ende und Dauer. Erkannt wird ein Gerät an seiner MAC-Adresse. Handys wechseln oft auf eine neue, private Adresse, deshalb lassen sich Adressen von Hand zu einem Gerät zusammenführen und wieder lösen.

Für ausgewählte Geräte kommt eine Mail, wenn sie sich verbinden oder trennen, mit mindestens 15 Minuten Abstand. Unbekannte Geräte lösen keine Mail aus.

Geräteliste mit erfundenen Geräten wie Beispiel-Handy, Beispiel-Laptop, Beispiel-Fernseher und Beispiel-Drucker, je mit Status online oder offline seit, MAC-Adresse und Verbindung über WLAN oder LAN. Ein Gerät trägt die Marke Gast, eines hat keinen Namen
Ausschnitt der Geräteliste, erfundene Geräte.

Eine Lücke ist kein „offline“

Erreicht der Collector die Box eine Zeit lang nicht, weiß niemand, was in dieser Zeit passiert ist. Die Oberfläche zeigt das als Lücke und nicht als Zeit, in der alle Geräte weg waren.

Nach einer Lücke gleicht der Collector Protokoll und Geräteliste ab, damit Ereignisse zwischen zwei Abfragen nicht verloren gehen. Die Rohereignisse werden nur angehängt, nie geändert. Sitzungen lassen sich daraus jederzeit neu berechnen.

Im Lasttest mit 4,6 Millionen Ereignissen brauchte die erste Abfrage nach einem Ausfall anfangs 333 Sekunden. Nach dem Umbau von Lückenlauf und Indizes sind es 0,18 Sekunden, mit kaltem Cache bis zu 10.

Seite Collector: Hostliste vor 20 Sekunden geholt, Protokoll vor 50 Sekunden, kein Fehler. Darunter die Lücken der letzten 90 Tage als Tabelle mit Quelle, Beginn, Ende und Dauer
Der Collector zeigt, wann er die Box zuletzt erreicht hat und wo Daten fehlen.

Vom Protokoll in die Datenbank

Die Box schreibt ihre Zeilen auf Deutsch und ohne Zeitzone. In der Nacht der Zeitumstellung gibt es die Stunde von 2 bis 3 Uhr zweimal. Der Parser löst das über den Zeitpunkt der Abfrage auf: Neuere Zeilen stehen oben, also kann keine ältere Zeile später liegen als die darüber.

Jede Zeile bekommt einen Schlüssel aus Meldungsnummer, Text und Zeitpunkt. Holt der Collector dieselbe Zeile noch einmal, verwirft die Datenbank sie. Lesen darf nur, wer als Besitzer eingetragen ist. Das prüft Postgres selbst über Row Level Security.

Auszüge aus dem echten Code, gekürzt.

Echter Code aus dem FRITZ!Box-Verlauf

collector/internal/logparse/logparse.go

func Resolve(lines []model.LogLine, loc *time.Location, observedAt time.Time) {  ceiling := observedAt.Add(jitter)  for i := range lines {    if lines[i].At.IsZero() {      if wall, ok := wallTime(lines[i].Raw); ok {        lines[i].At = localTime(wall, loc, ceiling)      }    }    if at := lines[i].At; !at.IsZero() && at.Add(jitter).Before(ceiling) {      ceiling = at.Add(jitter)    }  }}

collector/internal/logparse/logparse.go

func dedupeKey(id int, text string, at time.Time) string {  sum := sha256.Sum256(fmt.Appendf(nil, "%d|%s|%s", id, text,    at.UTC().Truncate(time.Second).Format(time.RFC3339)))  return hex.EncodeToString(sum[:])}

supabase/migrations/20261002000300_collector.sql

    …      format('hostlist:%s:%s:%s', hosts.mac,        case when hosts.active then 'connected'          else 'disconnected' end,        extract(epoch from p_observed_at))    from hosts    where hosts.active <> hosts.was_open    on conflict (dedupe_key) do nothing

supabase/migrations/20261002000500_access.sql

create policy "owner reads events"  on public.events  for select to authenticated  using ((select public.is_owner()));create policy "owner reads sessions"  on public.sessions  for select to authenticated  using ((select public.is_owner()));

Betrieb

Fünf Container auf dem eigenen Server: Datenbank, Anmeldung, REST-Schnittstelle, Oberfläche und Collector. Datenbank und Schnittstellen hängen in internen Netzen ohne Ausgang, nur der Collector darf zur Box und zum Mailserver.

Technik

Collector
Go 1.27, einzige direkte Abhängigkeit pgx
Daten
PostgreSQL 17 mit GoTrue und PostgREST aus dem Supabase-Stack, Logik als SQL-Funktionen, getestet mit pgTAP
Oberfläche
React 19, Vite, TanStack Query und Router, React Aria
Betrieb
Docker Compose hinter Caddy, WireGuard zur Box, Mails über SMTP
CI
go test -race, golangci-lint, govulncheck, pgTAP, Vitest, Playwright, Trivy und Suche nach Geheimnissen

Zugang

Das Werkzeug läuft im eigenen Heimnetz und zeigt echte Geräte. Deshalb gibt es keine Demo. Die Screenshots oben stammen aus der Entwicklung mit erfundenen Geräten.