presseportale/dev/betrieb/monitoring-plan.md
Kevin Adametz 8e3554bf91 dev: Doku aufgeräumt, neue Roadmap als einzige Liste offener Punkte
- dev/roadmap.md neu: alle offenen Punkte aus Plänen, Audits und Protokollen,
  gegen Code und Git abgeglichen (Stand v1.7.0)
- Abgeschlossenes ins Archiv: 2026-09-01 → archive/2026-09-nutzergewinnung,
  2026-09-11-audit → archive/2026-09-audit, lagacy-rechnungen →
  archive/legacy-rechnungen, Deploy-Protokoll v1.1.0, Umami-Snippets,
  Go-Live-Roadmap vom 20.07.
- Aktiv bleiben eigene Ordner bildrechte/ (Plan Fassung 2, Decision-Update)
  und changelog/ (Decision-Update Changelog)
- dev/README.md als Ordnerkarte neu, Verweise und zwei Code-Kommentare auf
  die neuen Pfade

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:25:09 +00:00

18 KiB
Raw Permalink Blame History

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.

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 <MONITORING_HEALTH_TOKEN>, 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=<zufällig, ≥ 32 Zeichen>
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-<id>')), 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). 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. 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):

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):

    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 <MONITORING_HEALTH_TOKEN aus der Live-.env>"}, 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/. In einem frischen Laravel 12 eingebaut und getestet.