# Betriebsüberwachung, Bedienbarkeit, Nutzungsstatistik — Plan **Stand:** 05.10.2026 · **Phase 1 live seit 05.10.2026 (v1.1.0)** Ziel: Fehler fallen auf, bevor Kunden sie melden — Serverfehler aus der Programmierung ebenso wie Bedienungs- und Usability-Probleme. Dazu eine Nutzungsstatistik, aus der sich ablesen lässt, wer was tut und wo Arbeit aufhört. ## Entscheidungen (Betreiber, 02.10.2026) | Nr. | Frage | Entscheidung | |---|---|---| | M1 | Fehlerwerkzeug | **GlitchTip, selbst gehostet** (wie Umami auf sys.adametz.media), angebunden über die offiziellen Sentry-SDKs (`sentry/sentry-laravel`, `@sentry/browser`) | | M2 | Umami im Portal | **ja, mit Zuordnung zum Nutzer** (`umami.identify` mit der internen User-ID, keine E-Mail, keine Namen) | | M3 | Alarmweg | **Mail bleibt**, zusätzlich **ntfy** (HTTP-POST, JSON) | Bei der Umsetzung getroffen (Bestätigung offen): | Nr. | Frage | Entscheidung | |---|---|---| | M4 | Personenbezug in GlitchTip | Nur die interne User-ID, `send_default_pii` aus, Request-Bodys nie (`max_request_body_size = never`) | | M5 | Browserfehler-SDK | Wird erst beim ersten Fehler nachgeladen (dynamischer Import) — kein Ladegewicht auf fehlerfreien Seiten, dafür ohne Breadcrumbs | | M6 | Umami-Schalter | Getrennt: `UMAMI_ENABLED` (Webseiten), `UMAMI_PORTAL_ENABLED` (Kundenbereich), `UMAMI_IDENTIFY_USERS` (Zuordnung). Portal und Zuordnung erst nach DSE-Anpassung einschalten | | M7 | Umami im Admin, unter Impersonation, auf Auth-Seiten | Nicht getrackt: Admin-Nutzung verfälscht die Kundenstatistik, Impersonation würde Admin-Klicks dem Kunden zuschreiben, Auth-Seiten tragen Tokens im Pfad (`/reset-password/{token}`) | | M8 | Query-Parameter an Umami | Nur `utm_*`, `src`, `ref`, `q`, `page` gehen raus; alles andere (Signaturen, Tokens) filtert `data-before-send` — gilt auch für die Webseiten | | M9 | Fehlgeschlagene Logins | Im Verlauf mit User-Bezug (falls Konto existiert) und gekürztem HMAC der IP statt Klartext-IP | ## Phase 1 — Fehler fallen sofort auf | Schritt | Inhalt | Stand | |---|---|---| | 1.1 | GlitchTip-Anbindung Server: `sentry/sentry-laravel`, `Integration::handles()` in `bootstrap/app.php`, Release aus `SENTRY_RELEASE`, User-ID als einziger Personenbezug | erledigt | | 1.2 | Browserfehler (JS-Fehler, unbehandelte Promises) auf Webseiten, Auth-Seiten und Portal; eigener Vite-Eintrag `resources/js/error-reporting.js`, SDK erst beim ersten Fehler | erledigt | | 1.3 | Fehler ohne Exception als GlitchTip-Warnung: 404 mit Referrer der eigenen Domain (kaputte interne Links), 403 und 429 für angemeldete Nutzer, 419 (Seite abgelaufen, Arbeit verloren). Je Signal gedrosselt | erledigt | | 1.4 | Scheduler-Herzschlag (`monitoring:heartbeat`, minütlich) und Check im Systemstatus | erledigt | | 1.5 | Tiefer Health-Endpunkt `/health` mit Token: DB, Cache, Queue-Rückstau, Scheduler-Herzschlag, Plattenplatz → 200/503 JSON, für Uptime Kuma bzw. Ploi | erledigt | | 1.6 | ntfy als zweiter Alarmweg in `OperationalAlert` | erledigt | | 1.7 | Fehlgeschlagene Logins, Login-Sperren und 2FA-Fehler im Aktivitäts-Verlauf | erledigt | | 1.8 | `queue:prune-failed` (30 Tage) im Scheduler | erledigt | | 1.9 | Umami: Portal (Kundenbereich) mit `identify`, Query-Filter, Web Vitals; Schalter getrennt (M6) | erledigt | Signale, die **nicht** im Browser erfasst werden, weil der Server sie schon sieht: fehlgeschlagene Livewire-Requests mit 5xx (Exception in GlitchTip) und 419 (Schritt 1.3). ## Phase 2 — Bedienprobleme sichtbar machen - Validierungsfehler zentral zählen: Formular/Komponente × Feld × Regel, nie der Wert. - Schritt-Tracking für die Kernabläufe (Registrierung, Newsroom anlegen, PM erstellen/einreichen, Checkout, Bildbestätigung): `step_viewed` / `step_completed`, pseudonymer Ablauf-Schlüssel, eigene kleine Tabelle, Schreiben per `defer()`. - Benannte Umami-Events (`data-umami-event`) an den Kern-Buttons. - „Problem melden“ im Portal: URL, Browser, letzte GlitchTip-Event-ID, Freitext. - Geschäftstrichter aus vorhandenen Daten (Registrierung → erste PM → eingereicht → bezahlt → veröffentlicht → zweite PM), ohne neues Tracking. ## Phase 3 — Rückgänge und Abwanderung erkennen - Tägliche Kennzahlen-Tabelle (Datum × Kennzahl × Dimension → Anzahl), nächtlich verdichtet. - Alarme bei ausbleibender Aktivität: Einlieferer ohne Einlieferung, Tage ohne Registrierung/Login/Zahlung, Funktionen deutlich unter dem 4-Wochen-Mittel. - Überfällig-Liste je Kunde (übliches PM-Intervall × 2). - Admin-Seite „Betrieb & Nutzung“; `conversion:funnel` und `api:usage-report` mit Oberfläche. - Morgendliche Sammelmail. - Löschfristen für `activity_events` und `api_usage_logs` (Vorschlag 395 Tage). ## Datenschutz (Betreiber/Anwalt, vor dem Einschalten) - DSE §5 (Umami): Portal-Nutzung mit Zuordnung zur Nutzer-ID ist personenbezogen (pseudonym) — Abschnitt ergänzen, bevor `UMAMI_PORTAL_ENABLED`/`UMAMI_IDENTIFY_USERS` an. - Neuer Abschnitt Fehlerprotokollierung (GlitchTip, selbst gehostet; User-ID, URL, Browser, Fehlermeldung; Frist = GlitchTip-Aufbewahrung, Vorschlag 90 Tage). - Aktivitäts-Verlauf mit fehlgeschlagenen Anmeldungen (IP nur als gekürzter HMAC). - Unabhängig davon offen: `activity_events`/`api_usage_logs` ohne Löschfrist und ohne DSE-Erwähnung. ## Live-Schritte Phase 1 Ausgeführt mit Deploy v1.1.0 — vollständiger Ablauf in [`deploy-v1.1.0.md`](../archive/betrieb/deploy-v1.1.0.md). **Infrastruktur (einmalig)** 1. GlitchTip auf sys.adametz.media (Docker Compose: web, worker, Postgres, Redis; `GLITCHTIP_RETENTION_DAYS=90`, vor GlitchTip 6 `GLITCHTIP_MAX_EVENT_LIFE_DAYS`). Zwei Projekte anlegen: `pressekonto-server` (PHP/Laravel) und `pressekonto-browser` (JavaScript). Alert-Regel je Projekt: Mail an das Ops-Postfach bei neuem Fehlertyp. 2. ntfy: Server (ntfy.sh oder selbst gehostet), Topic mit Zugriffsschutz, Token anlegen. Optional GlitchTip-Webhook an ntfy über dessen Template-Funktion. 3. Uptime Kuma (oder Ploi-Monitor) auf `https://pressekonto.com/health` mit Header `Authorization: Bearer `, Intervall 1 min, Benachrichtigung über ntfy und Mail. **`.env` auf Live** ``` SENTRY_LARAVEL_DSN=… # Projekt pressekonto-server SENTRY_ENVIRONMENT=production SENTRY_RELEASE= # leer lassen, siehe RELEASE-Datei unten SENTRY_BROWSER_DSN=… # Projekt pressekonto-browser MONITORING_HEALTH_TOKEN= MONITORING_NTFY_URL=https://push.adametz.media MONITORING_NTFY_TOPIC=pressekonto-ops MONITORING_NTFY_TOKEN=… UMAMI_ENABLED=true # Webseiten UMAMI_PORTAL_ENABLED=false # erst nach DSE-Anpassung UMAMI_IDENTIFY_USERS=false # erst nach DSE-Anpassung ``` Danach `config:cache`, `npm run build`, `queue:restart`. **Bei jedem Deploy (SFTP/SSH, solange es kein Git-Deploy gibt):** lokal vor dem Upload `git describe --tags --always --dirty > RELEASE` und die Datei `RELEASE` mit hochladen, danach auf dem Server `php artisan config:cache`. GlitchTip ordnet Fehler so dem Stand zu (`…-dirty` = mit nicht committeten Änderungen deployt). Beim späteren Git-Deploy schreibt das Deploy-Skript dieselbe Datei. GlitchTip-Instanz: `errors.adametz.media`, Projekt 1 = Server, Projekt 2 = Browser. Umgebungen: `production` (Live), `local` (Entwicklung). ## Umsetzungsprotokoll ### 02.10.2026 — Phase 1 umgesetzt (Branch `technik/monitoring-phase-1`) Neue Abhängigkeiten: `sentry/sentry-laravel` ^4.28 (Composer, nur neue Pakete), `@sentry/browser` ^11.3 (npm). | Schritt | Dateien | |---|---| | 1.1 | `config/sentry.php` (veröffentlicht, gehärtet), `app/Support/Monitoring/ErrorReportScrubber.php`, `bootstrap/app.php` (`Integration::handles`) | | 1.2 | `resources/js/error-reporting.js` + `error-reporting-sdk.js` (Chunk 32 kB gzip, nur bei Fehler), `resources/views/components/monitoring/browser-errors.blade.php`, eingebunden in `partials/head` (Portal), `web/layouts/web-master`, `components/layouts/auth/pressekonto`; Vite-Einträge in beiden Configs | | 1.3 | `app/Support/Monitoring/ErrorSignal.php`, `app/Http/Middleware/ReportErrorSignals.php` (global) | | 1.4 | `app/Console/Commands/MonitoringHeartbeat.php`, Scheduler minütlich, Check `scheduler` im Systemstatus | | 1.5 | `app/Services/Monitoring/HealthProbe.php`, `app/Http/Controllers/HealthController.php`, Route `/health` (ohne Gruppe, throttle 30/min), Basic-Auth-Ausnahme; Check `disk` im Systemstatus | | 1.6 | `app/Support/OperationalAlert.php` (Mail und ntfy unabhängig, beide Best-Effort) | | 1.7 | `ActivityType::LoginFailed/LoginLockout/TwoFactorFailed`, `RecordAuthActivity` (Grund `unknown_account`/`no_password`/`wrong_password`, `ip_hash`), Login-Komponente feuert `Failed` und `Lockout` mit Adresse | | 1.8 | `queue:prune-failed --hours=720` täglich 03:55 | | 1.9 | `components/web/umami.blade.php` (Web + Portal, `data-tag`, `data-before-send`, `data-performance`, `identify('user-')`), `config/services.php` (`portal_enabled`, `identify_users`) | Nebenbefund: Das Umami-Snippet der Webseiten schickte bisher die volle Query mit — signierte Claim-Links (`?signature=…`) landeten so in Umami. Der Filter (M8) gilt jetzt auch dort. Tests: `tests/Feature/Monitoring/*` (neu), `tests/Feature/Auth/FailedLoginActivityTest.php` (neu), `OperationalAlertTest` und `UmamiSnippetTest` erweitert. Gesamtsuite 2312 grün (1 übersprungen), Larastan ohne Meldung, Pint sauber. Nicht im Browser geprüft (kein GlitchTip/ntfy angebunden): das tatsächliche Einliefern an GlitchTip und die Darstellung dort. Erster Test nach dem Aufsetzen: im Browser `setTimeout(() => { throw new Error('GlitchTip-Test') })` auf einer Portalseite, serverseitig `php artisan tinker --execute 'report(new RuntimeException("GlitchTip-Test"));'`. ### 05.10.2026 — Composer-Update `composer update` innerhalb der bestehenden Grenzen (48 Updates, u. a. Laravel 12.69.3, Livewire 4.4.7, Flux 2.20.1, Fortify 1.40.0); damit sind die beiden `league/commonmark`-Advisories behoben (2.10.3), `composer audit` sauber. - Livewire 4.4.7 rendert `ModelNotFoundException` im Testhelfer als 404, statt sie zu werfen — zwei Tests auf `assertNotFound()` umgestellt (`CustomerCompanyContextTest`, `ContactAccessTest`). Laufzeitverhalten unverändert. - **Deploy:** Liegt auf Live `public/vendor/livewire/` (veröffentlichte Assets), nach `composer install` `php artisan vendor:publish --tag=livewire:assets --force` ausführen — sonst läuft ein altes `livewire.js` gegen das neue Backend. Lokal war die Kopie schon vor dem Update veraltet (Hash 3dabe98a statt 8ea5922c). ### 05.10.2026 — GlitchTip angebunden (lokal) DSNs eingetragen (errors.adametz.media, Projekte 1 und 2), lokal `SENTRY_ENVIRONMENT=local`. Release-Kennung ohne Git auf dem Server: `App\Support\Monitoring\Release` liest `SENTRY_RELEASE`, sonst die beim Deploy hochgeladene Datei `RELEASE` (in `.gitignore`). Test-Events lokal abgeschickt: Server-Exception (`report()`), toter interner Link per HTTP. ### 05.10.2026 — ntfy angebunden (lokal) ntfy auf `push.adametz.media` (gleicher Server wie GlitchTip), `auth-default-access: deny-all` (anonymes Lesen → 403 geprüft). Eigener Benutzer `pressekonto-alerts` mit `write-only` auf `pressekonto-ops`, Token in `MONITORING_NTFY_TOKEN`; Lesen über den persönlichen Benutzer in der App. Test-Alert aus `OperationalAlert` ohne Fehler angenommen. ### 05.10.2026 — Versionsnummer Versionen sind Git-Tags nach SemVer (`v1.0.0` Go-Live 20.07., nächster Deploy `v1.1.0`). `config('app.version')` = `APP_VERSION` oder Datei `RELEASE` (`git describe --tags --always --dirty`); dieselbe Kennung als GlitchTip-Release, in `/health` (`version`), im Admin-Systemstatus und in `php artisan about`. Grundlage für die spätere Changelog-Seite. ### 05.10.2026 — Phase 1 live (v1.1.0) Mit Deploy v1.1.0 ausgeliefert ([`deploy-v1.1.0.md`](../archive/betrieb/deploy-v1.1.0.md)). GlitchTip (`production`), ntfy und Scheduler-Herzschlag auf Live aktiv. Offen aus Phase 1: externer Monitor auf `/health`; DSE-Absätze, danach `UMAMI_PORTAL_ENABLED`/`UMAMI_IDENTIFY_USERS`. ### 06.10.2026 — Erste Auswertung von GlitchTip Lesezugriff per API eingerichtet; Arbeitsanleitung, Regeln und Protokoll der Einträge in [`../glitch-tip/README.md`](../glitch-tip/README.md). Befunde: Testsuite meldete an die echte DSN (behoben, `2f02acc`), Search-Console-Lauf robuster (`a90f24a`), Registrierung mit Alpine-Fehler, Promise-Ablehnungen lesbar, Tippfehler bei artisan nicht mehr gemeldet (`16af374`). ### 06.10.2026 — Nachtrag 1.7: fehlgeschlagene Anmeldungen Befund auf Live (05.10. 15:26 – 06.10. 16:25): rund 250 Fehlversuche aus 53 Quellen, **alle** `unknown_account`, kein falsches Passwort, keine Sperre, gleichmäßig auch nachts — verteiltes, langsames Durchprobieren fremder Zugangsdaten („low and slow“, jede Quelle bleibt unter der Sperre von 5/Minute). Kein Konto betroffen. Die Einträge verdeckten im Verlauf die relevanten Fälle. Das Nginx-Zugriffsprotokoll der Site ist bei Ploi aus, der Einstieg ließ sich so nicht klären. Umgesetzt: - `meta.entry` an jedem Fehlversuch, jeder Sperre und jedem 2FA-Fehler: `livewire` (Formular), `fortify` (`POST /login`, alle Domains, auch JSON), `two-factor`, sonst `other`. - Unbekannte Adressen nicht mehr im Verlauf, sondern in `failed_login_tallies` (Tag × Einstieg × IP-Hash → Versuche, erster/letzter Zeitpunkt). Löschfrist 90 Tage über `model:prune` (täglich 04:05). - Systemstatus-Kachel „Anmeldeversuche mit unbekannter Adresse“ (heute/gestern, Versuche und Quellen); `warn` ab `MONITORING_FAILED_LOGIN_WARN_PER_DAY` (1000). Auswertung auf Live (nach dem Deploy): ```bash php artisan tinker --execute ' DB::table("failed_login_tallies") ->where("day", ">=", now()->subDays(7)->toDateString()) ->selectRaw("day, entry, count(*) as quellen, sum(attempts) as versuche") ->groupBy("day", "entry")->orderBy("day")->orderBy("entry") ->get() ->each(fn ($r) => print(sprintf("%s %-10s %4d Quellen %5d Versuche\n", $r->day, $r->entry, $r->quellen, $r->versuche))); ' ``` Danach entscheiden: überwiegend `fortify` → `POST /login` auf den Hub beschränken bzw. je IP drosseln (vorher klären, wer den JSON-Login nutzt); überwiegend `livewire` → Honeypot/Turnstile im Formular. ### Externer Monitor auf `/health` — Einrichtung (erledigt 06.10.2026, status.adametz.media) Ziel: Ausfälle melden, die die App selbst nicht melden kann (Server weg, PHP-FPM hängt, Scheduler steht, Platte voll). **Uptime Kuma** auf dem GlitchTip-Server (`178.105.154.106`); die App läuft auf `178.105.240.70` — ein anderer Server, also echte Außensicht. ntfy liegt auf demselben Server wie Uptime Kuma; fällt dieser aus, kommt kein Push (siehe Schritt 6). 1. **Installation** (Docker, neben GlitchTip, hinter demselben Reverse Proxy, z. B. `status.adametz.media`): ```yaml services: uptime-kuma: image: louislam/uptime-kuma:1 restart: unless-stopped volumes: - ./uptime-kuma:/app/data ports: - "127.0.0.1:3001:3001" ``` Admin-Konto beim ersten Aufruf anlegen, 2FA in den Einstellungen aktivieren. 2. **Benachrichtigung** (Settings ▸ Notifications ▸ ntfy): Server `https://push.adametz.media`, Topic `pressekonto-ops`, Priorität 4, Authentifizierung „Access Token“ mit dem Token von `pressekonto-alerts`. Zusätzlich SMTP-Mail an das Ops-Postfach. 3. **Monitor „pressekonto /health“**: Typ HTTP(s), URL `https://pressekonto.com/health`, Methode GET, Header (JSON) `{"Authorization": "Bearer "}`, Intervall 60 s, Wiederholungen 2 (Retry-Intervall 60 s), akzeptierte Statuscodes `200-299`. 503 = mindestens ein Check kritisch, Details in der JSON-Antwort. 4. **Monitore „Domains“** ohne Token, Typ HTTP(s) - Keyword oder HTTP(s): `https://presseecho.de/up`, `https://businessportal24.com/up`, `https://pressekonto.com/up` — prüft Nginx/SSL/PHP je Domain, Intervall 5 min. Zertifikatsablauf-Warnung aktivieren (Settings ▸ „Certificate Expiry Notification“, 14 Tage). 5. **Test:** Einen zweiten Monitor mit absichtlich falschem Token anlegen — `/health` antwortet dann 404, der Alarm muss auf dem Handy und per Mail ankommen. Danach diesen Test-Monitor löschen. Beim nächsten Deploy zeigt der echte Monitor die Wartungsphase (`down` → 503) als kurzen Ausfall. 6. **Zweite Ebene (optional, empfohlen):** ein kostenloser externer Dienst (z. B. UptimeRobot) mit einem einfachen Monitor auf `https://pressekonto.com/up` und Mail-Benachrichtigung. Er meldet, wenn der Monitoring-Server selbst ausfällt oder beide Server gemeinsam nicht erreichbar sind. Nach der Einrichtung: Punkt 29 in `../archive/betrieb/deploy-v1.1.0.md` und Phase 1 hier als erledigt eintragen. ### 06.10.2026 — Worker-Ausfall und Folgen (v1.3.2) Nach dem Deploy von `v1.3.0` (16:51) lief über eine Stunde kein Queue-Worker: Supervisor war gestoppt, `queue:restart` beendete den Worker, niemand startete ihn neu. Sichtbar war es nur in der Systemstatus-Kachel; einen Alarm gab es nicht — `/health` hätte 503 geliefert, der externe Monitor war noch nicht eingerichtet. Beim Abarbeiten scheiterten drei Veröffentlichungsmails vom 03./08.09., die erneut eingestellt worden waren (GlitchTip #23, alte Serialisierung ohne `$viaSyndication`) — entfernt, nicht nachgesendet. - **Externer Monitor** läuft seit 06.10.2026 (`status.adametz.media`, Uptime Kuma). Er deckt einen stehenden Worker ab: `/health` meldet `queue` kritisch ab 5 Minuten Wartezeit. Ein zusätzlicher Alarm aus dem Scheduler wurde deshalb **bewusst nicht** gebaut (wäre doppelt). - **Worker-Prüfung im Deploy** (`monitoring:queue-probe`, Ende von `deploy.sh`, Exit 3 ohne Worker). - **Runbook:** kein pauschales `queue:retry all` mehr; Vorgehen bei stehendem Worker. ### 07.10.2026 — Allgemeine Fassung für andere Projekte Git-Deploy und Betriebsüberwachung als übertragbarer Baukasten mit Vorlagen (Laravel-Dateien, `deploy.sh`, Docker-Compose für GlitchTip/ntfy/Uptime Kuma): [`../git-deploy-monitoring/`](../git-deploy-monitoring/README.md). In einem frischen Laravel 12 eingebaut und getestet.