presseportale/dev/betrieb/git-deploy.md
Kevin Adametz 6c6015f960 dev: Nachweise gesichtet, Vertragsübersicht BN-06, Deploy-Notiz v1.9.1
Screenshots und Verträge geprüft und in nachweise/ zusammengeführt;
Binärdateien per .gitignore ausgeschlossen (vertraulich, Kunden- und
Organisationsnummern, Größe). Vertragsübersicht mit Fassungen, Daten und
Drittlandgrundlagen. Versandfassung: DPF-Status Cloudflare vom 13.10.2026,
OpenAI-DPA unterzeichnet am 13.10.2026. Juli-Zahlen auf Live bestätigt.
Protokollzeile v1.9.0 im Git-Deploy-Dokument.

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

23 KiB
Raw Blame History

Git-Deploy

Allgemeine, projektunabhängige Fassung mit Vorlagen: ../git-deploy-monitoring/.

Stand: 06.10.2026 · Status: umgestellt am 06.10.2026, erster Git-Deploy v1.1.3 (Protokoll unten)

Bis v1.1.0 gingen Deploys als Paket per SFTP auf den Server (deploy-v1.1.0.md). Künftig ist ~/pressekonto.com ein Git-Checkout, und ausgeliefert wird ein Tag aus origin.

Grundsätze

  • Remotes: origin = ssh://git@forge.adametz.media:222/kadmin/pressekonto.git (Forgejo auf Hetzner, privat). Von dort holt der Live-Server. NAS = git.adametz.media im lokalen Netz, nur als Backup.
  • Ausgeliefert wird nur ein Tag (vX.Y.Z, SemVer wie bisher), nie ein Branch-Stand.
  • Deploy nur von Hand per SSH mit deploy.sh. Ploi-„Quick Deploy" und Push-Webhooks bleiben aus.
  • Assets werden lokal gebaut und committet. public/build ist versioniert (2,4 MB, 18 Dateien). Auf dem Server gibt es kein Node.
  • Auf dem Server wird nichts von Hand geändert. Das Skript bricht ab, sobald der Checkout vom Tag abweicht. Hotfixes laufen ebenfalls über Commit, Tag und Deploy.
  • Kein git clean auf Live. .env, storage/, vendor/, auth.json, RELEASE und public/storage sind ignoriert und bleiben unberührt.
  • Kein Zero-Downtime-Modus mit Release-Verzeichnissen. storage/app (Archiv, Quarantäne, Exporte) bleibt an seinem Ort. Deploy im selben Verzeichnis mit php artisan down, wie bisher.
  • dev/, tests/ und die Doku liegen damit auch auf dem Server, aber außerhalb von public/. Ausgeliefert werden sie nicht.

A — Einmalig: Zugang vom Server zu Forgejo

  1. Auf dem Server (Benutzer der App) prüfen: git --version.

  2. Deploy-Key erzeugen und in Forgejo nur lesend am Repository eintragen (Repository ▸ Einstellungen ▸ Deploy-Schlüssel):

    ssh-keygen -t ed25519 -f ~/.ssh/forge_pressekonto -N "" -C "pressekonto-live"
    cat ~/.ssh/forge_pressekonto.pub
    
  3. ~/.ssh/config anlegen bzw. ergänzen (chmod 600 ~/.ssh/config). Ploi verwaltet nur authorized_keys, die Datei kommt ihm nicht in die Quere:

    Host forge.adametz.media
        Port 222
        User git
        IdentityFile ~/.ssh/forge_pressekonto
        IdentitiesOnly yes
    
  4. Host-Key aufnehmen und den Fingerprint mit dem in Forgejo angezeigten vergleichen:

    ssh-keyscan -p 222 forge.adametz.media >> ~/.ssh/known_hosts
    ssh-keygen -lf <(ssh-keyscan -p 222 forge.adametz.media 2>/dev/null)
    
  5. Test: git ls-remote ssh://git@forge.adametz.media:222/kadmin/pressekonto.git zeigt refs/tags/v1.1.0.

  6. FPM-Neustart ohne Ploi testen: echo "" | sudo -S service php8.4-fpm reload. Geht das nicht, beim Deploy PK_FPM_RELOAD= setzen. Das Skript wartet dann, bis FPM in Ploi neu geladen ist.

    Getestet 06.10.2026: funktioniert. Kein Passwort nötig (Ploi erlaubt dem Benutzer ploi genau diesen Befehl), Exit-Code 0, keine Ausgabe. Prüfen lässt es sich mit ps -o pid,lstart,args -C php-fpm8.4 vorher und nachher: der master process behält PID und Startzeit, die pool www-Prozesse sind neu. PK_FPM_RELOAD deshalb nicht setzen und FPM nicht zusätzlich in Ploi über „Restart“ neu starten — ein Restart entfernt kurz den Socket, Nginx meldet dann connect() to unix:/run/php/php8.4-fpm.sock failed und liefert 502 (so beim Deploy von v1.3.0, 16:51).

B — Einmalig: Live-Verzeichnis in einen Checkout umwandeln

Live läuft auf v1.1.0. Das Verzeichnis wird an Ort und Stelle zum Checkout dieses Tags. Dabei ändert sich zunächst keine Datei. Git zeigt nur, wo der Server vom Tag abweicht.

  1. Dateibackup wie bei v1.1.0:

    cd ~ && tar czf backup-vor-git.tgz --exclude=./storage --exclude=./node_modules -C ~/pressekonto.com .
    
  2. Repository anlegen und auf v1.1.0 setzen, ohne Dateien anzufassen:

    cd ~/pressekonto.com
    git init -q
    git remote add origin ssh://git@forge.adametz.media:222/kadmin/pressekonto.git
    git fetch --tags origin
    git update-ref --no-deref HEAD "$(git rev-parse 'v1.1.0^{commit}')"
    git read-tree HEAD
    
  3. Abweichungen sichten:

    git status --porcelain | grep -v -E '^.. (dev/|docs/|_docs/|tests/|\.agents/|\.codex/|\.cursorrules|\.devcontainer/|\.github/|\.mcp\.json|AGENTS\.md|CLAUDE\.md|boost\.json|docker-compose\.yml|phpstan|phpunit\.xml|pint\.json)'
    

    Herausgefiltert sind die Entwicklerdateien, die nie im Paket waren. Was danach übrig bleibt:

    Zeile Bedeutung Vorgehen
    M datei Datei auf Live weicht vom Tag ab git diff -- datei ansehen. Fehlt die Änderung im Repository, erst lokal nachziehen, neuer Tag, dann weiter. Sonst ist es ein Paketrest und wird überschrieben.
    D datei Datei fehlt auf Live Meist Entwicklerdatei oder ein nie hochgeladener Teil. Einordnen, der Checkout legt sie an.
    ?? datei nur auf Live, nicht ignoriert Nicht löschen. Nach ~/vor-git/ verschieben, wenn sie nicht gebraucht wird. Wird sie gebraucht, gehört sie in die .gitignore (lokal, neuer Tag).
  4. Wenn nichts Unerklärtes übrig ist, auf den Tag auschecken:

    git checkout -q -f --detach v1.1.0
    git status --porcelain        # muss leer sein
    

    Der Code ist danach inhaltlich derselbe wie vorher. Ein Neustart ist nicht nötig.

C — Release lokal (jedes Mal)

  1. main sauber, volle Suite und Larastan grün (wie bisher).

  2. Assets bauen und mitcommitten:

    npm ci && npm run build
    git add public/build
    git diff --cached --quiet public/build || git commit -m "build: Assets für vX.Y.Z"
    

    Vite benennt Dateien nach ihrem Inhalt. Ohne Frontend-Änderung entsteht kein Diff.

  3. Tag setzen und auf beide Remotes pushen:

    git tag -a vX.Y.Z -m "vX.Y.Z — <Inhalt>"
    git push origin main --follow-tags
    git push NAS main --follow-tags
    

D — Deploy auf Live (jedes Mal)

  1. Bringt der Tag Migrationen mit: DB-Backup ziehen (ARCHIVE_KEY getrennt gesichert).

  2. Neue .env-Werte laut Release-Notiz vorher eintragen.

  3. Deploy:

    cd ~/pressekonto.com
    bash dev/betrieb/deploy.sh vX.Y.Z                   # ohne Migrationen
    bash dev/betrieb/deploy.sh vX.Y.Z --migrations      # mit Migrationen
    

    Das Skript macht Folgendes:

    • Es prüft, dass der Checkout sauber ist, und holt die Tags.
    • Es zeigt neue Migrationen an. Ohne --migrations bricht es dann ab, bevor etwas verändert ist.
    • Danach in dieser Reihenfolge: down, Checkout, RELEASE, composer install, gegebenenfalls migrate, Livewire-Assets, Caches, queue:restart, FPM, up, Version.
    • Zum Schluss php artisan monitoring:queue-probe --wait=60: ein Test-Job muss innerhalb einer Minute von einem Worker abgeholt werden. Sonst endet das Skript mit Exit 3 und dem Hinweis, Supervisor und den Worker in Ploi zu prüfen — die Seite ist dann schon live. (Seit v1.3.2; das Skript läuft aus der Kopie des vorherigen Stands, die Prüfung greift also ab dem Deploy nach v1.3.2. Beim Deploy von v1.3.2 selbst danach von Hand php artisan monitoring:queue-probe aufrufen.)

    --clear-schedule setzt zusätzlich die withoutOverlapping-Sperren zurück. Das ist nur nötig, wenn sich Sperrdauern im Scheduler geändert haben. Darf eine Migration länger auf Sperren warten: DB_LOCK_WAIT_TIMEOUT=300 bash dev/betrieb/deploy.sh vX.Y.Z --migrations.

  4. Prüfungen:

    • /health mit Token liefert "version":"vX.Y.Z".
    • Admin ▸ Reports ▸ Betriebsüberwachung ist grün.
    • storage/logs zeigt keine neuen Fehler.
    • Smoke-Test aus dem Runbook (Abschnitt 8).
    • Hat sich der Scheduler geändert: php artisan schedule:list.

Schlägt ein Schritt fehl, bleibt die Seite im Wartungsmodus. Das Skript nennt den Rollback-Befehl.

E — Rollback

cd ~/pressekonto.com
bash dev/betrieb/deploy.sh <vorheriger Tag>

Liegt die Abbruchstelle nach dem Checkout, ist der Checkout zwar sauber, aber vendor/ und die Caches können halb erneuert sein. Der Rollback-Aufruf baut beides für den alten Tag neu auf. Migrationen sind additiv. Der alte Code läuft mit den neuen Spalten, ein migrate:rollback gibt es auf Live nicht. Nur bei Datenproblemen das DB-Backup einspielen und danach Runbook Abschnitt 9 (Archiv) befolgen.

F — Erste Umstellung (erledigt 06.10.2026)

Gedacht war v1.1.1 als reines Test-Release. Live ging direkt v1.1.3. Dazwischen lagen ein Hotfix (v1.1.2) und ein Fehler, der erst bei der Umwandlung auffiel (v1.1.3).

Was bei der Umwandlung (B) auftrat, für den Fall, dass ein weiterer Server so umgestellt wird:

  • Auf Live fehlte die .gitignore, sie war nie im Paket. Deshalb zeigte Schritt 9 auch .env, vendor/, storage/ und public/build als ??. Nach dem Checkout verschwinden diese Zeilen.
  • Pfade mit Umlauten oder Leerzeichen gibt Git in Anführungszeichen aus. Sie rutschen am Filter in Schritt 9 vorbei. Das betraf nur Entwicklerdateien.
  • Per SFTP gelöschte Dateien liegen noch auf Live. Gefunden wurde app/Actions/Fortify/UpdateUserProfileInformation.php (seit 18.09. gelöscht). Sie ist nach ~/vor-git/ verschoben.
  • Eigene Flux-Icons waren nie im Repository. Das macOS-Muster Icon in der .gitignore traf wegen core.ignorecase auch resources/views/flux/icon/. Behoben in v1.1.3. Für den ersten Deploy war der Ordner per .git/info/exclude ausgeblendet, damit der Checkout die identischen Dateien aus dem Tag übernimmt.
  • Doku war nach dem Checkout noch per SFTP hochgeladen worden (dev/betrieb/). Die Abweichungsprüfung hat den Deploy gestoppt, bevor etwas verändert war. Die Dateien sind nach ~/vor-git/ gesichert und auf den Repo-Stand zurückgesetzt.
  • deploy.sh gab es in v1.1.0 noch nicht. Beim ersten Mal lief es aus dem Tag: git show v1.1.3:dev/betrieb/deploy.sh > /tmp/deploy.sh && bash /tmp/deploy.sh v1.1.3.
  • Alte Build-Dateien tauchten nach dem Deploy als ?? auf. Bis v1.1.0 war public/build ignoriert. Sie sind als ~/vor-git/build-reste.tgz gesichert und entfernt.

Aufräumen nach ein paar stabilen Tagen: ~/deploy-v1.1.0.tgz, ~/backup-vor-v1.1.0.tgz, ~/backup-vor-git.tgz, ~/vor-git/.

G — Sensible Daten aus der Historie entfernen

Wurden Kundendaten, Dumps, Logs oder Schlüssel committet, reicht Löschen oder Enttracken nicht: Die Datei bleibt in jedem Klon und auf jedem Remote abrufbar. Vorgehen, erprobt am 09.10.2026 (Protokoll unten):

  1. Schlüssel zuerst rotieren. Was einmal gepusht war, gilt als bekannt, auch nach der Bereinigung.

  2. Sichern: Branches, die nur auf einem Remote liegen, als lokale Branches anlegen, dann git bundle create <scratchpad>/vor-filter.bundle --all. Das Bundle enthält die Daten noch und wird am Ende gelöscht.

  3. Umschreiben aller Branches und Tags (git filter-repo ist besser, fehlt aber im Dev-Container):

    FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch -f \
      --index-filter 'git rm -r --cached --ignore-unmatch --quiet "<pfad>"' \
      --tag-name-filter cat -- --branches --tags
    
  4. Prüfen: Dateibaum (git rev-parse <ref>^{tree}) und Commitzahl (git rev-list --count <ref>) je Branch und Tag vorher und nachher gleich; git rev-list --objects --branches --tags | grep '<pfad>' ist leer.

  5. Hash-Zuordnung alt → neu ins Archiv legen, solange die alten Commits lokal noch vorhanden sind (Abgleich über Autor- und Committerzeit, Autor, Betreff). Doku und Commit-Nachrichten nennen weiter die alten Hashes.

  6. Push vom Host (der Dev-Container hat keinen SSH-Schlüssel), je Remote: git push --force-with-lease <remote> <branches…> und git push --force --tags <remote>. Lehnt Forgejo ab, kurz den Branch-Schutz lösen.

  7. Server: deploy.sh holt Tags ohne --force und scheitert an neu gesetzten Tags („would clobber existing tag“). Einmal von Hand:

    cd ~/pressekonto.com
    git status --porcelain                  # muss leer sein
    git fetch --force --tags --prune origin
    git checkout --quiet --detach <aktueller Tag>   # gleicher Inhalt, neuer Hash
    git reflog expire --expire=now --all && git gc --prune=now
    git count-objects -vH                   # size-pack prüfen
    

    RELEASE bleibt unverändert, ein Deploy ist nicht nötig.

  8. Forgejo und NAS: Speicherbereinigung des Repositorys anstoßen. Weitere Klone löschen und neu klonen.

  9. Lokal aufräumen: refs/original/*, verwaiste Remote-Refs und Refs anderer Werkzeuge (z. B. refs/codex/…, sie hielten am 09.10. die alten Objekte fest) löschen, dann git reflog expire --expire=now --all && git gc --prune=now. Mit git cat-file -e <blob-hash> nachweisen, dass die Objekte weg sind, dann das Bundle löschen.

Betrieb: PHP-FPM beim Deploy

Jeder Reload bricht Anfragen ab, die in dieser Sekunde laufen. Im Nginx-Fehlerlog (/var/log/nginx/pressekonto.com-error.log, für ploi lesbar) erscheinen dann einzelne recv() failed (104: Connection reset by peer), am 06.10. genau zu den Deploys von v1.1.3 (15:37, 1×) und v1.2.0 (16:06, 6×). Das sind kurze 502er, fast nur für Bots, kein Absturz.

Abhilfe wäre process_control_timeout = 10s: Die alten Worker dürfen laufende Anfragen noch zu Ende bringen. Die Einstellung gehört in den Abschnitt [global] von /etc/php/8.4/fpm/php-fpm.conf (root). Ploi bietet nur „Edit FPM ini“ (php.ini, dort wirkungslos), „Edit CLI ini“ und „Edit pool“ (www.conf). Nie in die Pool-Datei eintragen — dort ist der Schlüssel unbekannt, FPM startet beim nächsten Reload nicht mehr und die Seite ist offline. Entscheidung 06.10.2026: nicht gesetzt. Bei Bedarf über den Ploi-Support oder mit root-Zugang, danach php-fpm8.4 -t und erst dann neu laden.

Übrige Einträge im Nginx-Fehlerlog sind unbedenklich: access forbidden by rule (Ploi sperrt Punktdateien, Scanner suchen /.env u. Ä.) und directory index of …/public/api/ is forbidden (dort liegt die API-Spezifikation v1.yml, ein Aufruf von genau /api/ bekommt 403). Ein Zugriffsprotokoll für die Site führt Ploi nicht.

Später: Changelog

Der Changelog-Entwurf (Decision-Update, Stufe 2) dockt in deploy.sh nach up an. Ein Artisan-Befehl liest die Commits zwischen dem vorherigen und dem neuen Tag. Die liegen jetzt auf dem Server vor. Daraus legt er die Entwürfe an. Der LLM-Schritt läuft als Queue-Job, damit ein Ausfall des Anbieters keinen Deploy abbricht.

Protokoll

Datum Schritt Ergebnis
06.10.2026 Vorbereitung origin auf Forgejo (Hetzner) umgestellt, NAS als zweites Remote, Push erledigt. deploy.sh gegen ein simuliertes Repository geprüft: Abbruch bei neuen Migrationen ohne Option, Deploy, wiederholter Aufruf, Rollback, unbekannter Tag, Fehler mitten im Lauf. Die Umwandlung eines bestehenden Verzeichnisses (B) wurde ebenso simuliert: Abweichungen werden sichtbar, .env und ungetrackte Dateien bleiben, ignorierte Builds werden beim Checkout überschrieben.
06.10.2026 v1.1.1 lokal /public/build versioniert. Alte Build-Reste von 2025 lokal entfernt (nicht mehr referenziert). Suite 2.316 grün, einer übersprungen (parallel, 96 s); Larastan ohne Meldung. Build-Commit und Tag v1.1.1 gesetzt, Push auf origin/NAS nach Freigabe.
06.10.2026 v1.1.2 Hotfix nach GlitchTip-Auswertung (anderer Chat). Gepusht ohne Tags, Tags mit git push origin --tags nachgeholt. Künftig --follow-tags.
06.10.2026 Umwandlung Live (A, B) Server-Zugang zu Forgejo per Deploy-Key (nur lesen). Git 2.43. Befunde siehe F: fehlende .gitignore, Altdatei Fortify, Flux-Icons nicht im Repository.
06.10.2026 v1.1.3 .gitignore-Muster Icon korrigiert, vier Icons versioniert. Erster Git-Deploy v1.1.0 → v1.1.3: keine Migration, Composer „Nothing to install", Caches, Worker, FPM ohne Fehler, Version v1.1.3. Danach alte Build-Dateien entfernt. v1.1.1 und v1.1.2 waren nie einzeln live.
06.10.2026 v1.2.0 Volt entfernt (Livewire-4-Single-File-Components). Erster Deploy, der nur noch deploy.sh brauchte: keine Migration, Composer entfernt livewire/volt. Vom Betreiber durchgeklickt (Login, Kundenbereich, Admin), unauffällig.
06.10.2026 FPM und Nginx-Fehlerlog sudo -S service php8.4-fpm reload ohne Passwort bestätigt (Master bleibt, Worker neu) — deploy.sh lädt FPM künftig selbst, PK_FPM_RELOAD bleibt ungesetzt. Nginx-Fehlerlog ausgewertet: Verbindungsabbrüche nur zu den Deploy-Zeitpunkten, process_control_timeout bewusst nicht gesetzt (siehe „Betrieb: PHP-FPM beim Deploy“).
06.10.2026 Worker-Ausfall nach v1.3.0 Supervisor lief nicht. queue:restart beim Deploy von v1.3.0 (16:51) hat den Worker beendet, niemand startete ihn neu — KI-Prüfungen, Mails und eine Zahlungsbestätigung lagen über eine Stunde. Aufgefallen über die Systemstatus-Kachel. Supervisor in Ploi neu gestartet, Rückstau mit einem Ersatz-Worker (nohup) abgearbeitet. Ursache für den gestoppten Supervisor ohne root-Zugang nicht feststellbar. Folgen: Worker-Prüfung am Ende von deploy.sh (v1.3.2), externer Monitor auf /health (status.adametz.media).
06.10.2026 v1.3.2 Worker-Prüfung im Deploy. Von Hand nachgeholt (Skript lief aus der Kopie von v1.3.1): monitoring:queue-probe → Test-Job nach 4 s abgeholt. systemctl is-enabled supervisor → enabled; ein Neustart ohne Autostart scheidet als Ursache des Ausfalls damit weitgehend aus.
07.10.2026 v1.4.0 Sicherheitsaudit S-01–S-06, Passkeys, Zweitfaktor-Pflicht für Admins; Migration create_passkeys_table (--migrations). Vorher brach deploy.sh wegen Abweichungen ab: Die Doku-Commits nach v1.3.2 waren von Hand hochgeladen worden, deploy.sh hatte dabei sein Ausführungsrecht verloren. Mit git checkout -- dev/betrieb/… bereinigt. Danach entzog memberships:resync-auto 19.598 von 19.607 automatischen Firmenrechten: Bestandskonten ohne email_verified_at verloren pauschal alles.
07.10.2026 v1.4.1 Hotfix für den Abgleich: Entzogen wird nur noch, was keine Grundlage mehr hat; das Kommando läuft in einer Transaktion, mit --dry-run und Notbremse ab 5 %. Rechte aus dem Backup vor dem Deploy wiederhergestellt (INSERT IGNORE, 19.598 eingefügt, danach 19.607). Hinweis: mysql auf dem Server braucht -h/-P aus der .env, sonst „Access denied“ für …@'localhost'; ohne Passwort geht es über php artisan tinker. Probelauf 19.607 → 19.316, danach ausgeführt. Wiederholung am 08.10. ohne Änderung (19.316 Rechte, 14.911 User). Die Nacht verlief ohne Auffälligkeiten.
08.10.2026 v1.5.0 API-Zugang an die Buchung gebunden (403 ohne Abo/Buchung, Tokens bleiben), optionales Token-Ablaufdatum, sanctum:prune-expired täglich 04:10. Vorab auf Live geprüft: nginx antwortet direkt, kein Proxy/CDN; Cookies secure/httponly/samesite=lax, HSTS fehlte. Nach dem Deploy: strict-transport-security: max-age=31536000, trustedproxy.proxies = null. Auswirkung der Buchungsprüfung: 2 Konten haben die API in den letzten 30 Tagen genutzt, beide weiterhin mit Zugang. Keine Migration.
08.10.2026 v1.6.0 Mit Migration (users.sessions_revoked_at_ms, Batch 22). Härtung aus Phase 4 des Sicherheitsaudits: „Andere Geräte abmelden“, IP-weites Login-Limit (20 Fehlversuche in 15 Minuten), dauerhafte Session-Sperrmarke, Passwörter ab 12 Zeichen mit Leak-Abgleich, CSP im Melde-Modus (storage/logs/csp-*.log), npm shell-quote/source-map-js. Nach dem Deploy: Header content-security-policy-report-only vorhanden, Umami- und GlitchTip-Host in script-src/connect-src. CSP-Log in einigen Tagen auswerten.
08.10.2026 v1.6.1 Vorher auf dem Server php8.4-imagick installiert und FPM neu geladen: Die Erweiterung fehlte auf Live, Herkunftsprüfung und KI-Vorab-Check der Bilder liefen seitdem ins Leere. Release: Zeile „PHP-Erweiterung Imagick“ in der Betriebsüberwachung, gebaute Livewire-Anfragen von Scannern (GlitchTip 38, 39) als 400 statt 500/419, Browser-Filter für abgebrochene Alpine-Übergänge (GlitchTip 18). Keine Migration. Nach dem Deploy: Betriebsüberwachung „Imagick ist geladen“.
09.10.2026 v1.7.0 Spamschutz für Kontakt-Widget und Registrierung (Cloudflare Turnstile, Inhaltsfilter mit Spam-Quarantäne, Namensregel bei der Registrierung). Bei nicht erreichbarem Cloudflare wird durchgelassen und markiert. Keine Migration. Vorher: Turnstile-Keys in der .env, Widget im Cloudflare-Dashboard mit allen drei Live-Hostnamen. Nach dem Deploy: Kontakt-Widget im Browser getestet, Betriebsüberwachung „Spamschutz (Cloudflare Turnstile)“ grün. contact-messages:quarantine-spam: 152 Anliegen in die Quarantäne, 130 offen (Rest per Sammelaktion), echte Korrekturanfragen blieben offen. users:prune-bot-registrations nach freigegebenem Probelauf ausgeführt, danach USERS_PRUNE_BOT_REGISTRATIONS=true und config:cache.
09.10.2026 Historie bereinigt Die Produktiv-SQL-Dumps vom 23.04. (dev/migration 2026/sql, ~290 MB, Kundendaten) lagen seit 5b8bdf4 in der Historie und auf beiden Remotes. Mit git filter-branch aus allen Branches und Tags entfernt; Dateibäume und Commitzahlen aller 21 Refs unverändert, alle Hashes neu (Zuordnung alt → neu: commit-map-2026-10-09.txt). Tags v1.0.0–v1.7.0 neu gesetzt, gleicher Inhalt. Force-Push auf origin und NAS, Server-Abgleich nach G.7 (Server size-pack 33,4 MiB), Speicherbereinigung in Forgejo und auf dem NAS, keine weiteren Klone. Lokal zusätzlich drei Codex-Checkpoint-Refs entfernt, die noch auf die alte Historie zeigten; danach 33,5 MiB, beide Dump-Blobs nicht mehr vorhanden. Die betroffenen Live-Schlüssel waren bereits rotiert (Betreiber). Vorgehen allgemein in Abschnitt G.
12.10.2026 v1.8.0 Von v1.7.0, keine Migration, kein Asset-Diff. Sicherheitsseite mit „Geräte und Anmeldungen“, Breadcrumbs ohne Daten an GlitchTip (Livewire-Breadcrumbs aus), Löschfristen activity_events 395 / api_usage_logs 90 Tage (erster Lauf 04:05), Newsletter-API 410, Wortwahl „automatisiert geprüft“. Prüfung von außen: alle drei Domains melden Release v1.8.0 (Meta-Tag der Fehlererfassung), robots.txt und Sitemaps hostkorrekt, Veröffentlichen-Seite mit neuem Text, GlitchTip ohne Einträge unter v1.8.0. /health mit Token nicht geprüft (lokales Token ≠ Live). Befund: „Qualitätsgeprüft“ stand noch in config/domains.php (Meta und Taglines) und im Tarifvorteil „Starter“ (Datenbank) → Hotfix v1.8.1.
13.10.2026 v1.9.0 Von v1.8.1, keine Migration, mit Asset-Diff. Boost-Label „Gebuchte Platzierung“ mit Erklärung, „Aktuell“ statt „Top-Auswahl“, Admin-Aktion „Boost beenden“, audit:admins. Deploy durch den Betreiber; danach auf Live Rollenprüfung BN-02 mit audit:admins (ein Admin-Konto mit Passkey und TOTP) und Test-Boost für die Screenshots des Anwaltspakets. Befund aus den Screenshots: auf Mobil verdeckt das Label den Bildnachweis → v1.9.1.