# atcwatch

`atcwatch` beobachtet die lokal im ATCMatch-Tab **My Airports** gewählten Flugplätze und
meldet handlungsrelevante Änderungen. Das Tray-Icon zeigt dauerhaft, ob die Abfrage gesund ist
und ob gerade oder demnächst ATC verfügbar ist. Das Programm ist kein Dashboard, öffnet keinen
Port, führt keine Watchlist und meldet weder Statistikänderungen noch das Offline-Gehen eines
Platzes.

Voraussetzungen sind Python 3.11+, ein Linux-Desktop mit KDE Plasma oder macOS 12+ sowie der
deployte Endpoint `POST /api/watchlist-status`. Die einzige Laufzeitabhängigkeit ist
`PySide6-Essentials>=6.9,<6.10`; HTTP bleibt absichtlich bei `urllib` aus der Standardbibliothek.

## Installation unter Linux/KDE

Wer das Paket über den Download-Link im Webapp-Tab **My Airports** bezogen hat (statt aus
einem Repo-Checkout), entpackt es und führt `./install.sh` aus — das idempotente Installer-
Skript im Paket erledigt die unten beschriebenen Schritte automatisch (venv, systemd-Unit,
`config.json`-Vorlage, abschließender Test-Alarm) und aktiviert bewusst nichts automatisch;
die Befehle zum Aktivieren gibt es am Ende aus. Kein `curl | bash`: laden, entpacken, selbst
ausführen.

Die folgenden Schritte sind dasselbe manuell — als Referenz und für einen Repo-Checkout ohne
das fertige Paket. Die Anwendung wird aus dem Repository in ein stabiles Ziel kopiert. Die systemd-Unit enthält
deshalb keinen Worktree-Pfad. atcwatch ist ein Python-**Paket** mit relativen Imports: Es wird
als Paketverzeichnis `atcwatch/` unter dem Installationsroot installiert und immer über
`python -m atcwatch` gestartet — ein direkter Aufruf von `atcwatch.py` scheitert an den
relativen Imports.

```sh
install_root="$HOME/.local/share/atcwatch"
config_root="$HOME/atcwatch"

mkdir -p "$install_root" "$config_root" "$HOME/.config/systemd/user"
rm -rf "$install_root/atcwatch"
cp -R tools/atcwatch "$install_root/atcwatch"
python3 -m venv "$install_root/venv"
"$install_root/venv/bin/pip" install -r "$install_root/atcwatch/requirements.txt"

test -e "$config_root/config.json" || \
  install -m 600 "$install_root/atcwatch/config.example.json" "$config_root/config.json"
install -m 644 "$install_root/atcwatch/autostart/atcwatch.service" \
  "$HOME/.config/systemd/user/atcwatch.service"

mkdir -p "$HOME/.local/share/applications"
sed "s#%h/.local/share/atcwatch#$install_root#g" \
  "$install_root/atcwatch/autostart/atcwatch.desktop" \
  > "$HOME/.local/share/applications/atcwatch.desktop"
update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true

systemctl --user daemon-reload
systemctl --user enable atcwatch.service
```

Direkt nach der Installation den **Test-Alarm** auslösen — er prüft ohne Konfiguration,
Watchlist oder Netzwerk, ob Desktop-Notifications diese Sitzung erreichen:

```sh
(cd "$install_root" && PYTHONPATH="$install_root" "$install_root/venv/bin/python" -m atcwatch --test-alert)
```

Das `cd` in der Subshell ist kein Stilmittel: `python -m atcwatch` stellt das CWD an den
Anfang von `sys.path`. Steht man dabei noch im entpackten Paketverzeichnis (das selbst eine
`atcwatch.py` enthält), gewinnt diese Datei gegen das installierte Paket — Symptom siehe
*Betrieb und Diagnose*.

Erscheint die Meldung „Test alert — notifications are working." mit UTC-Zeitstempel, ist der
Zustellpfad in Ordnung (Exit 0). Andernfalls endet der Befehl mit Exit 1 und einer Diagnose
auf stderr (siehe *Betrieb und Diagnose*).

Die Unit ist an `graphical-session.target` gebunden: Das Icon läuft nur in der grafischen
Benutzersitzung und endet mit ihr. `loginctl enable-linger` ist weder nötig noch erwünscht.
Die Unit bindet `DBUS_SESSION_BUS_ADDRESS` an `%t/bus`, damit `notify-send` sicher denselben
Session-Bus wie die grafische Benutzersitzung erreicht.

## Installation unter macOS

Die komplette Installation liegt unter `~/Library/Application Support/atcwatch`. Das mitgelieferte
Bundle setzt `LSUIElement`, damit atcwatch als Menüleistenprogramm ohne eigenes Dock-Icon läuft.

```sh
support_root="$HOME/Library/Application Support/atcwatch"
launch_agents="$HOME/Library/LaunchAgents"

mkdir -p "$support_root" \
  "$support_root/atcwatch.app/Contents/MacOS" \
  "$launch_agents" "$HOME/atcwatch"
rm -rf "$support_root/atcwatch"
cp -R tools/atcwatch "$support_root/atcwatch"
python3 -m venv "$support_root/venv"
"$support_root/venv/bin/pip" install -r "$support_root/atcwatch/requirements.txt"

test -e "$HOME/atcwatch/config.json" || \
  install -m 600 "$support_root/atcwatch/config.example.json" "$HOME/atcwatch/config.json"
install -m 644 "$support_root/atcwatch/autostart/macos-app/Info.plist" \
  "$support_root/atcwatch.app/Contents/Info.plist"
install -m 755 "$support_root/atcwatch/autostart/macos-app/run.sh" \
  "$support_root/atcwatch.app/Contents/MacOS/run.sh"
sed "s|__HOME__|$HOME|g" \
  "$support_root/atcwatch/autostart/de.emvisio.atcwatch.plist" \
  > "$launch_agents/de.emvisio.atcwatch.plist"

```

Auch hier gilt: Das Paket wird über `python -m atcwatch` gestartet (so macht es das
mitgelieferte `run.sh`); `atcwatch.py` direkt aufzurufen scheitert an den relativen Imports.

Direkt nach der Installation den **Test-Alarm** auslösen — er prüft ohne Konfiguration,
Watchlist oder Netzwerk, ob Notifications ankommen (auf macOS insbesondere die
Mitteilungs-Freigabe, siehe *Wichtige Warnungen*):

```sh
(cd "$support_root" && PYTHONPATH="$support_root" "$support_root/venv/bin/python" -m atcwatch --test-alert)
```

Exit 0 plus sichtbare Meldung „Test alert — notifications are working." heißt: Zustellpfad in
Ordnung. Exit 1 nennt die Diagnose auf stderr.

Der LaunchAgent wird erst nach dem einmaligen Canary weiter unten geladen. Zum Entladen dient
`launchctl bootout "gui/$(id -u)/de.emvisio.atcwatch"`. `KeepAlive` startet nur nach einem
Fehler neu; **Quit** im Menü bleibt deshalb wirksam.

## Die zwei Dateien

Nur diese beiden Dateien sind für den Nutzer relevant. `state.json` ist interner, automatisch
verwalteter Laufzeitstatus.

| Datei | Linux | macOS | Besitzer und Regel |
|---|---|---|---|
| **`config.json`** | `~/atcwatch/config.json` | `~/atcwatch/config.json` | Von Hand gepflegt und ohne Geheimnis. atcwatch ändert daran genau einen Wert — `sticky_notifications`, und nur auf Klick im Menü. Der Rest der Datei bleibt Byte für Byte, wie er geschrieben wurde: Schlüsselreihenfolge, Einrückung, alles. |
| **`watchlist.json`** | daneben im selben Verzeichnis | daneben im selben Verzeichnis | Wird vom Chromium-Browser im Tab **My Airports** geschrieben. Nicht von Hand editieren; die nächste Änderung im Tab überschreibt die Datei. |

**Niemals einen API-Key eintragen.** atcwatch braucht keinen: Es holt sich wie ein Browser ein
eigenes `atc_session`-Cookie. Der Server-`api_key` ist das HMAC-Signatur-Secret und bleibt auf dem
Server. Auf macOS benötigt das Schreiben der Watchlist die File System Access API; Safari
unterstützt diesen Workflow nicht, ein Chromium-basierter Browser ist erforderlich.

Die ausgelieferte Konfiguration ist:

```json
{
  "url": "https://atc.emvisio.de",
  "poll_min": 5,
  "notify_lead_min": 45,
  "notify_upgrades": true,
  "sticky_notifications": ["live", "lead", "upgrade:APP", "upgrade:CTR"]
}
```

| Feld | Default | Bedeutung |
|---|---:|---|
| `url` | — | Pflichtfeld; Basis-URL ohne Pfad und ohne abschließenden Slash. |
| `poll_min` | `5` | Abfrageintervall in Minuten; Werte unter 5 werden abgelehnt. Der Default entspricht dem 5-Minuten-Takt, in dem der Server seine VATSIM-Daten erneuert — häufiger zu fragen liefert denselben Stand. |
| `notify_lead_min` | `45` | Vorlauf für Booking-Meldungen. |
| `notify_upgrades` | `true` | Radar-Upgrades an/aus. |
| `sticky_notifications` | `["live", "lead", "upgrade:APP", "upgrade:CTR"]` | Welche Meldungen stehen bleiben, bis man sie wegklickt. Erlaubt sind `live`, `lead`, `cancelled`, `event`, `upgrade` (alle Radarpositionen) und `upgrade:APP` / `upgrade:DEP` / `upgrade:CTR`. Leere Liste = nichts bleibt stehen. Unbekannte Einträge werden abgelehnt, statt wirkungslos zu bleiben; `health` und `recovery` sind nicht wählbar (eine Störungsmeldung wartet immer, die Entwarnung nie). Klebrige `live`- und `lead`-Meldungen tragen im Body zusätzlich ihre Beobachtungszeit („· since 17:42z“) — ein seit zwei Stunden stehendes „LPMA is live“ kann längst falsch sein. Bequemer als der Editor: das Untermenü **Keep visible until dismissed** (siehe unten). |
| `timeout_sec` | `15` | Netzwerk-Timeout in Sekunden. |
| `stale_max_min` | `20` | Maximales Alter der VATSIM-Daten. |
| `offline_cooldown_min` | `60` | Mindestpause vor einer erneuten Live-Meldung desselben Platzes. |

Der User-Agent ist **intern und nicht konfigurierbar** (Konstante `atcwatch/1.0`, wegen der
Serverhärtung); ein `user_agent`-Feld in `config.json` wird als unbekanntes Feld abgelehnt.

## Icon und Menü

Die Zustände unterscheiden sich durch ihre Form, nicht durch Farbe. Das ist für monochrome
macOS-Template-Images und von Plasma eingefärbte Icons notwendig.

| Zustand | Icon | Bedeutung |
|---|---|---|
| `idle` | ![Kontur-Flugzeug](icons/atcwatch-idle.svg) | Verbindung ok, nichts live und nichts im Vorlauf. |
| `live` | ![Gefülltes Flugzeug mit Punkt](icons/atcwatch-live.svg) | Mindestens ein Watchlist-Platz ist live. |
| `upcoming` | ![Kontur-Flugzeug mit Uhr](icons/atcwatch-upcoming.svg) | Nichts live, aber ein Slot liegt im Meldevorlauf. |
| `error` | ![Durchgestrichenes Flugzeug](icons/atcwatch-error.svg) | Poll, Konfiguration oder Watchlist fehlerhaft oder Daten stale. |

Die SVG-Dateien sind lokal gezeichnete Master ohne externe Assets. Mitgelieferte PNGs in 18/22 px
sowie 36/44 px als jeweilige @2x-Assets vermeiden einen Renderer zur Laufzeit. Unter Linux lädt das
Tray den Satz aus `icons/png/linux/`; der macOS-Satz wird als Template-Maske
(`QIcon.setIsMask(True)` über `platform_backend.icon_is_template()`) geladen, bleibt aber bis zur
realen Abnahme ungetestet.

**Start:** Ohne Argumente startet `python -m atcwatch` den Tray-Daemon; das ist der Normalfall
und der einzige Modus, der PySide6 importiert. Fehlt PySide6, endet der Start mit Exit 2 und einem
Installationshinweis — `--once` und `--status` funktionieren dann weiterhin. Eine zweite Instanz
endet ebenfalls mit Exit 2 („atcwatch is already running"). Für einen netzfreien Probelauf startet
`--fixture fixtures/seed.json` den Tray gegen eine Beispielantwort statt gegen den Server —
**nur mit gesetztem `ATCWATCH_HOME`** auf ein Wegwerfverzeichnis: Ohne die Variable verweigert
atcwatch den Fixture-Tray (Exit 2), weil er sonst das echte `state.json` mit Testdaten
kontaminieren würde.

Das Menü zeigt bis zu fünf aktive oder kommende Plätze (live vor upcoming, Rest als
„… and N more"; unbekannte ICAOs als eigene Hinweiszeile), darunter **Open My Airports**,
**Check now** (während eines aktiven Backoffs deaktiviert, mit Restzeit im Text),
**Send test notification** (Zustellpfad-Diagnose, wirkt auch bei pausierten Notifications),
das Untermenü **Keep visible until dismissed**,
**Pause notifications for 1 hour**/**Resume notifications**, die Zeile **Last check: …** und
**Quit**. Der Tooltip wiederholt Zustand und Zeitpunkt des letzten Erfolgs und nennt bei aktiver
Pause deren Ende. Ein Linksklick öffnet unter Linux direkt den My-Airports-Tab.

Der Poll-Takt kommt nicht aus einem 10-Minuten-Schlaf: Ein 30-Sekunden-Tick prüft gegen die
Wanduhr, ob der nächste Poll fällig ist. Nach Suspend/Resume oder einem Uhrensprung wird dadurch
sofort normal weitergepollt; laufen dabei mehr als zwei Ereignisse auf, fasst eine
Sammelmeldung sie zusammen.

**Plattformstatus:** Linux (KDE Plasma) ist der Abnahme-Pfad und funktional verifiziert. Die
macOS-Codepfade (Template-Icons, `osascript`-Notification, Pfade, Autostart) sind sauber hinter
`platform_backend` gekapselt, aber **ungetestet** — macOS gilt bis zur realen Abnahme nicht als
unterstützt.

## Was gemeldet wird

| Klasse | Auslöser |
|---|---|
| `live` | Ein Platz wechselt von `none`/`upcoming` zu `live`. |
| `lead` | Ein gebuchter Slot rückt in `notify_lead_min`. |
| `upgrade` | APP, DEP oder CTR kommt an einem bereits live gemeldeten Platz direkt hinzu; über `notify_upgrades` abschaltbar. |
| `cancelled` | Ein bereits per `lead` angekündigter, noch zukünftiger Slot fehlt in zwei Polls und hat keinen nahen Ersatzslot. |
| `event` | Ein neues Event liegt höchstens 30 Tage voraus. |

Mehr als zwei Ereignisse eines Polls werden zu einer Meldung zusammengefasst. Zusätzlich gibt es
genau eine Health-Warnung nach einer Stunde ohne Erfolg und nachfolgend eine Recovery-Meldung.

Bewusst **nicht** gemeldet werden DEL/GND-Upgrades, verschwindende Positionen, das Offline-Gehen,
reine Score-/Statistikänderungen, unbekannte ICAOs und selbst ausgelöste Watchlist-Änderungen.
Events weiter als 30 Tage voraus stehen nur im Tab; beim Eintritt in den Horizont melden sie sich
genau einmal. Ein Booking, dessen Startzeit bereits erreicht ist, verschwindet lautlos — eine
Absage ist nur vor dem Start sinnvoll.

Der erste Lauf und neu hinzugefügte, bereits aktive Plätze werden still in den Zustand übernommen.
Das Icon zeigt sie trotzdem sofort.

## Nicht am Platz — „was war los, während ich weg war?"

Meldungen, die niemand sehen und wegklicken kann, sind verloren. Deshalb beantwortet der
30-Sekunden-Tick bei jedem Durchlauf **eine** Frage: Kann der Nutzer gerade auf ein Popup reagieren?
Zwei DBus-Quellen antworten darauf, beide bewusst per Abfrage statt per Signal-Abo — so übersteht
die Erkennung Suspend/Resume und abgerissene Verbindungen, weil jeder Tick den echten Zustand neu
liest:

- `org.freedesktop.ScreenSaver.GetActive` — der gesperrte Bildschirm.
- die Property `Inhibited` von `org.freedesktop.Notifications` — der „Nicht stören“-Modus, den
  Plasma auch setzt, während eine Vollbildanwendung (X-Plane) Meldungen unterdrückt.

Beide Quellen werden zu **einem** Zustand verrechnet: Sagt eine „weg“, ist der Nutzer weg. Es gibt
genau eine Abwesenheitsphase, egal wie viele Gründe sich überlappen — kommt zur laufenden
Vollbildsitzung noch die Bildschirmsperre dazu, beginnt keine zweite.

Während der Abwesenheit läuft der Poll ganz normal weiter, jedes Ereignis wird erkannt und
persistiert, nur die **Zustellung** wird zurückgehalten. Bei der Rückkehr kommt alles Aufgelaufene
als **eine** Meldung „While you were away (2 h)"; jede Zeile trägt die Uhrzeit ihres Ereignisses,
damit ein „LOWI in 45 min" von vor zwei Stunden nicht wie eine Aussage über jetzt aussieht. Ist
genau ein Ereignis aufgelaufen, erscheint die normale Einzelmeldung — eine Sammlung von einem
Ereignis wäre Unsinn. Der Tooltip zeigt währenddessen „Away since HH:MMz — N notifications held“.
Die Logzeile `delivered N of M notifications held while away` ist der Beleg, dass der Rückstau
wirklich zugestellt wurde; sie erscheint bei jeder Rückkehr.

Die Pause hat Vorrang: Wer den Notifier pausiert, will Ruhe, keine Nachlieferung. Ereignisse, die
in eine laufende Pause fallen, werden wie bisher verworfen und tauchen bei der Rückkehr nicht auf.

Antwortet keine der Quellen (anderes Desktop-Environment, kein DBus, macOS), ist die Funktion
stillschweigend aus: Es wird nichts zurückgehalten, alles wird sofort zugestellt, und der Grund
steht je Quelle genau einmal im Log.

## Welche Meldungen stehen bleiben

Nicht jede Meldung verdient dieselbe Aufmerksamkeit. Dass sich an einem Platz überhaupt etwas
tut, darf kurz aufblitzen; dass irgendwo APP aufmacht, will man auch dann noch sehen, wenn man
gerade im Anflug ist. Welche Art zu welcher Gruppe gehört, entscheidet jeder anders — deshalb
steht die Wahl im Menü:

```
Keep visible until dismissed  ▸
    Airport goes live               live
    Booking starts soon             lead
    Radar upgrades  ▸
        Approach (APP)              upgrade:APP
        Departure (DEP)             upgrade:DEP
        Center (CTR)                upgrade:CTR
    Booking cancelled               cancelled
    New event                       event
    ────────────────────────────
    Monitoring unhealthy (always visible)
    Monitoring recovered (always brief)
```

Ein Klick schreibt `sticky_notifications` in `config.json` zurück — nichts anderes in der Datei
wird angefasst. Wer die Datei parallel im Editor offen hat, ist weitgehend geschützt: Das Menü
liest sie beim Aufklappen neu, und wenn sie sich zwischen Aufklappen und Klick geändert hat,
bricht der Schreibvorgang ab und lädt neu, statt die fremde Änderung zu überschreiben. Ein
letztes Zeitfenster von wenigen Millisekunden zwischen der Prüfung und dem Umbenennen bleibt
technisch offen — eine dateiübergreifende atomare Prüf-und-Setz-Operation gibt es portabel
nicht. Wer parallel von Hand editiert, sollte das Menü währenddessen zulassen.

Vier Dinge, die man dabei wissen sollte:

- **Die Änderung gilt ab der nächsten Meldung.** Ob eine Meldung stehen bleibt, wird beim
  Einreihen entschieden und wandert mit ihr in die Outbox. Was bereits wartet, behält sein
  Verhalten; während eines laufenden Polls ist das Untermenü deshalb kurz deaktiviert.
- **Eine Sammelmeldung kann sich nicht teilen.** Fallen mehr als zwei Ereignisse zusammen, wird
  daraus ein Digest — und der bleibt stehen, sobald *ein* Element darin klebrig ist.
- **`health` und `recovery` stehen nicht zur Wahl.** Ein blindes Werkzeug ist ein stiller
  Ausfall: Die Störungsmeldung wartet immer, die Entwarnung nie. Beide stehen als deaktivierte
  Zeilen im Menü, damit man die Zusage dort liest, wo man sie sucht.
- **Nur Linux.** `notify-send -t 0` fragt den Benachrichtigungsdienst nach einem Popup ohne
  Ablauf. `display notification` unter macOS kennt keinen solchen Schalter, also heißt der
  Menüpunkt dort „(Linux only)“ und ist deaktiviert, statt etwas zu versprechen, was das Backend
  nicht einlöst.

## Benachrichtigungen unter KDE einstellen

Zwei Ebenen, die man auseinanderhalten muss. atcwatch entscheidet pro Nachrichtenart, ob es beim
Benachrichtigungsdienst überhaupt eine dauerhafte Anzeige **anfragt** (siehe oben). Ob und wie
Plasma diese Anfrage umsetzt — und alles Übrige: Anzeigedauer der nicht-klebrigen Meldungen,
Verlauf, Verhalten im „Nicht stören“-Modus, Position auf dem Bildschirm — **entscheidet der
Desktop**, unter *Systemeinstellungen → Benachrichtigungen → Anwendungseinstellungen →
**ATCMatch Notifier***. Das ist bewusst nicht im Programm nachgebaut: Plasma kennt die Regeln des
Nutzers bereits und hält sie für alle Anwendungen einheitlich.

Damit das funktioniert, installiert `install.sh` den Desktop-Eintrag
`~/.local/share/applications/atcwatch.desktop`, und jede Meldung trägt den Hint
`desktop-entry=atcwatch`. Ohne beides ordnet Plasma die Meldung keiner Anwendung zu — dann fehlen
Glocke, Verlauf und der Eintrag in den Anwendungseinstellungen, obwohl die Meldung selbst kurz
erscheint. Taucht „ATCMatch Notifier“ dort nicht auf: prüfen, ob die Datei existiert, danach
`update-desktop-database ~/.local/share/applications` ausführen und einmal ab- und anmelden.

Klebrigkeit ist **nicht** dasselbe wie Priorität. Eine wartende Meldung wird mit
`-u normal -t 0` gesendet; nur die Störungsmeldung (`health`) nutzt `-u critical`. Der
Unterschied ist eine Zusage: `critical` durchbricht unter Plasma zusätzlich „Nicht stören“ und
Vollbild — das ist etwas anderes als „wartet auf mich“, und für Ruhe gibt es **Pause
notifications**.

## Einmaliger Produktions-Canary

Nach Konfiguration und verknüpfter Watchlist, aber vor dem regulären Betrieb, wird exakt **ein**
kopfloser Test gegen Produktion ausgeführt. Er sendet keine Meldung und schreibt keinen State:

```sh
(cd "$HOME/.local/share/atcwatch" && PYTHONPATH="$HOME/.local/share/atcwatch" \
  "$HOME/.local/share/atcwatch/venv/bin/python" -m atcwatch --once --verbose --dry-run)
```

Auf macOS wird in diesem einen Befehl nur das Installationsroot durch
`$HOME/Library/Application Support/atcwatch` ersetzt (PYTHONPATH und venv-Pfad). Den Canary nicht in einer
Schleife wiederholen. Ein 403 ist ein Anlass, User-Agent und Serverhärtung zu prüfen, nicht den
User-Agent als Browser zu tarnen.

Nach erfolgreichem Canary wird der reguläre Betrieb aktiviert: unter Linux mit
`systemctl --user start atcwatch.service`, unter macOS mit
`launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/de.emvisio.atcwatch.plist"`.

## Betrieb und Diagnose

`--version` nennt die installierte Version — dieselbe Nummer steht als unterste Zeile im
Tray-Menü. Sie wird beim Deploy aus dem Git-Tag in den Tarball gestempelt; ein direkt aus
dem Repository gestarteter Baum meldet `dev`, weil er zu keinem Release gehört.

Kopfloser Status ohne Netzwerkzugriff (Installationsroot = Verzeichnis mit `atcwatch/` und `venv/`):

```sh
(cd /pfad/zum/installationsroot && PYTHONPATH=/pfad/zum/installationsroot \
  /pfad/zum/installationsroot/venv/bin/python -m atcwatch --status)
```

**`ImportError: attempted relative import with no known parent package`** bei `python -m atcwatch`
bedeutet nicht „Installation kaputt", sondern „aus dem falschen Verzeichnis gestartet": `-m`
stellt das aktuelle Arbeitsverzeichnis an den Anfang von `sys.path`; steht man dabei noch im
entpackten Paketverzeichnis (das selbst eine Datei `atcwatch.py` enthält), gewinnt diese Datei
gegen das installierte Paket. Abhilfe: vorher in ein neutrales Verzeichnis wechseln, z. B. in
das Installationsroot (siehe die `(cd … && …)`-Beispiele oben) — `install.sh` selbst macht das
beim abschließenden Test-Alarm automatisch.

`--status` liefert Exit 0, solange kein Fehlzustand vorliegt — dazu zählt auch
`last_outcome=never polled` direkt nach der Installation, bevor der erste Poll gelaufen ist: Das
ist der normale Ausgangszustand, kein Fehler. Exit 1 gibt es nur bei einem echten Fehlzustand
(Transportfehler, stale Daten, defekte Config/Watchlist oder einer Quarantäne-Warnung beim
State-Laden).

`--status` zeigt in der Zeile `log=…` den genauen Pfad der Logdatei — das ist die erste
Anlaufstelle, wenn atcwatch von KRunner, dem Plasma-Autostart oder per Doppelklick gestartet
wurde: Dort gibt es kein Terminal, und stderr landet nirgendwo. Unter Linux liegt die Logdatei
in `$XDG_STATE_HOME/atcwatch/atcwatch.log` (Default `~/.local/state/atcwatch/atcwatch.log`),
unter macOS in `~/Library/Application Support/atcwatch/atcwatch.log` — jeweils dieselbe
Verzeichnisebene wie `state.json`. Sie rotiert bei 1 MB mit drei Backups (`atcwatch.log.1` …
`.3`) und wird beim Start jeder Kopfinstanz und jedes Tray-Laufs neu geöffnet; lässt sich die
Datei nicht anlegen (schreibgeschütztes Dateisystem, fehlende Rechte), ist das nicht fatal —
eine einmalige Warnung auf stderr, danach läuft atcwatch mit reiner Konsolenausgabe weiter.

Standard-Loglevel ist `INFO` (Zyklusergebnisse, Fehler und Warnungen); `--verbose` schaltet auf
`DEBUG` und zeigt zusätzlich jede einzelne Übergangs-/Dedup-Entscheidung des Reducers. Das
Session-Cookie und der `Cookie`-Header erscheinen in keinem Loglevel — auch nicht unter
`--verbose` (siehe `tests/test_atcwatch_logging.py`).

Linux/KDE:

```sh
systemctl --user status atcwatch.service
journalctl --user -u atcwatch.service -n 50
tail -n 50 "$XDG_STATE_HOME/atcwatch/atcwatch.log"      # oder ~/.local/state/atcwatch/…
```

Läuft atcwatch unter systemd, ist `journalctl` die bequemere Alternative — die Unit schreibt
stdout/stderr ohnehin dorthin (`StandardOutput=journal`); die Logdatei bleibt der Weg für einen
Handstart ohne Terminal.

macOS:

```sh
launchctl print "gui/$(id -u)/de.emvisio.atcwatch"
tail -n 50 "$HOME/Library/Application Support/atcwatch/atcwatch.log"
```

`--test-alert` sendet genau eine Test-Notification über den echten Zustellpfad (`notify-send`
unter Linux, `osascript` unter macOS) — ohne Konfiguration, Watchlist, Netz, Lock oder
State-Schreibzugriff; auch das Tray-Menü bietet dies als **Send test notification** an (bewusst
auch bei pausierten Notifications, denn es ist eine Diagnose, kein Fachereignis). Schlägt die
Zustellung fehl, endet der Befehl mit Exit 1 und der Meldung
`test alert could not be delivered — …`: unter Linux dann prüfen, ob `notify-send` installiert
ist und `DBUS_SESSION_BUS_ADDRESS` auf den Session-Bus der grafischen Sitzung zeigt; unter
macOS die Mitteilungs-Freigabe des sichtbaren Absenders und Fokus/„Nicht stören" prüfen.

`--dry-run` unterdrückt Notification und State-Schreibzugriff. Zum parallelen Entwicklertest den
laufenden Dienst vorher stoppen; der Singleton-Lock verhindert zwar eine zweite Instanz, aber eine
Handinstanz ersetzt nicht das vorhandene Tray-Icon.

## Wichtige Warnungen

**User-Agent nicht ändern und niemals `requests` einsetzen.** Apache blockiert unter anderem
`python-requests`, `curl`, `wget`, einen leeren User-Agent und als Browser getarnte Clients unter
Version 100. Drei Treffer innerhalb von zehn Minuten führen zu einer **24-stündigen IP-Sperre für
den gesamten Anschluss**; dann erreicht auch der Browser ATCMatch nicht mehr.

**Ein aktiver fail2ban-Bann sieht wie ein Timeout aus, nicht wie ein 403.** Bei reinen Timeouts
zuerst einmal über eine andere Verbindung, etwa Mobilfunk, prüfen. Wiederholte Tests vom gesperrten
Anschluss liefern keine bessere Diagnose.

**Nicht zweimal starten.** Autostart plus Handstart würde doppelte Meldungen riskieren; der Lock
fängt den zweiten Prozess ab. Für Tests den Dienst stoppen oder `--dry-run` nutzen.

**macOS-Benachrichtigungen brauchen eine Freigabe.** Der sichtbare Absender kann je nach
Startweise `osascript`, Script Editor oder die Bundle-App sein und muss auf dem Zielsystem real
ermittelt werden. Ohne Freigabe des dort sichtbaren Eintrags unter *Systemeinstellungen →
Mitteilungen* können Meldungen kommentarlos fehlen. Fokus/„Nicht stören“ unterdrückt sie ebenfalls;
`urgent` hat auf macOS keine besondere Wirkung.

**Nach einem HTTP 403 mindestens 60 Minuten nicht erneut abfragen.** **Check now**, `--once` und
`--dry-run` dürfen diese Schutzfrist nicht umgehen; sonst kann aus einem einzelnen Block sehr
schnell ein fail2ban-Bann des ganzen Anschlusses werden.

Eine Server-Key-Rotation erfordert keine Änderung am Client: Nach einem 401 holt er genau einmal
ein neues Cookie. Der Stale-Guard misst die Frische des VATSIM-Fetches, nicht separat die
Slot-Regeneration; hängt nur dieser Cron-Schritt, kann der Client die Slots fälschlich für frisch
halten.

## Manuelle Plattformchecks

Linux/KDE, in einer echten Plasma-Sitzung:

- Icon erscheint unter X11 und Wayland und bleibt ohne eigenes Fenster aktiv.
- Alle vier Formzustände sind in 22 px erkennbar; Menü, Tooltip und Linksklick funktionieren.
- **Check now**, Pause/automatisches Resume nach 60 Minuten und **Quit** funktionieren.
- Notifications erscheinen normal und als kritisch; eine zweite Instanz beendet sich sauber.
- Ab- und Anmeldung startet den User-Service erneut; ohne grafische Sitzung läuft er nicht.

macOS 12 oder neuer, in einer echten Aqua-Sitzung:

- Das Bundle erzeugt ein Menüleisten-, aber kein Dock-Icon (`LSUIElement`).
- Alle vier Template-Icons sind in heller und dunkler Menüleiste eindeutig erkennbar.
- Menü, Tooltip-Ersatzzeile, Linksklick, Pause, **Quit** und LaunchAgent-Neustart funktionieren.
- Notification-Berechtigung und Fokus-Verhalten sind geprüft; eine zweite Instanz endet sauber.

Lokal unter Linux lassen sich Python-Tests, SVG-/PNG-Abmessungen und XML-Syntax prüfen. Nicht
ehrlich automatisierbar sind das StatusNotifier-/DBus-Verhalten einer echten Plasma-Sitzung sowie
auf macOS LaunchAgent/Aqua, Dock-Unterdrückung, Template-Maskierung und Mitteilungsrechte. Diese
Punkte bleiben die obigen realen Plattformchecks.

Das macOS-Gerüst startet aus einem Bundle, wechselt dann aber per `exec` in den Python-Interpreter
des externen venv. Ob Cocoa den laufenden Prozess auf jeder OS-/Python-Kombination dauerhaft dem
Bundle zuordnet, ist ebenfalls eine reale Testgrenze. Für eine öffentliche Distribution ist ein
signiertes, eigenständig gebautes `.app`-Artefakt belastbarer.

## Bekannte Grenzen

Notifications besitzen noch keinen klickbaren Airport-Deep-Link, es gibt keine zeitgesteuerten
Quiet Hours und kein Windows-Backend. Das macOS-Bundle ist nicht signiert; deshalb bleibt
ein systemseitiger statt eines signierten atcwatch-Absenders bei Benachrichtigungen sichtbar.
