mivita/dev/betrieb/README.md
Kevin Adametz 0b02b1b15c GlitchTip: Fehlerprotokoll fuer Server, Fehlersignale und Browser
- sentry/sentry-laravel 4.28; config/sentry.php gehaertet (keine PII,
  keine Request-Bodys, before_send ErrorReportScrubber)
- Handler: Integration::captureUnhandledException, CommandNotFoundException
  nicht melden
- ReportErrorSignals (global): toter interner Link (nach Pfad wegen
  Catch-all-Route), 403/429 fuer Angemeldete, 419; gedrosselt
- Browser: selbst gehostetes SDK (esbuild-Bundle 11.5.0), Loader ohne
  Build-Schritt und ohne Regex-Lookbehind, in 7 Layouts mit area-Tag
- phpunit.xml: DSNs und ntfy-URL leer, CACHE_DRIVER=array (Tests schrieben
  bisher in den lokalen Datei-Cache)
- dev/glitch-tip/README.md mit Instanz, Org, Projekt-IDs, API-Auswertung

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

299 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Betrieb mivita.care — Git-Deploy per Tag
**Stand:** 08.10.2026 · Vorlage und Hintergründe: [`dev/git-deploy-monitoring/`](../git-deploy-monitoring/README.md) ·
Protokoll der Umstellung mit Aufräum-Liste: [2026-10-08-protokoll-umstellung.md](2026-10-08-protokoll-umstellung.md)
Ausgeliefert wird ein **Git-Tag** (`vX.Y.Z`), nie ein Branch-Stand und keine Dateiliste per SFTP.
Das Live-Verzeichnis ist ein Git-Checkout dieses Tags; [`deploy.sh`](deploy.sh) führt den Deploy
von Hand per SSH aus und bricht ab, **bevor** es etwas verändert, wenn etwas nicht stimmt.
| | Wert |
|---|---|
| Server | Ploi, Benutzer `ploi`, PHP-FPM 8.4 (`php8.4-fpm`), Redis lokal |
| Queue | Horizon als Ploi-Daemon, `QUEUE_DRIVER=redis` (aktiv seit 08.10.2026) |
| Live-Verzeichnis | `/home/ploi/mivita.care` |
| Git-Server (Deploy liest hier) | `origin` = `ssh://git@forge.adametz.media:222/kadmin/mivita.git` |
| Release-Branch | `master` — Tags werden auf `master` gesetzt |
| Version in der App | Datei `RELEASE` (schreibt `deploy.sh`) → `config('app.version')`, `php artisan about --only=mivita` |
## Grundsätze
- **Nur Tags**, SemVer: Feature-Release erhöht die mittlere Stelle, Hotfix die letzte. Die
Tag-Nachricht ist die Release-Notiz (Inhalt, Migrationen, neue `.env`-Werte, Deploy-Befehl, Nacharbeiten wie Seeder).
- **Auf dem Server wird nichts von Hand geändert.** Kein SFTP-Upload mehr in den Code-Bereich. Das Skript
bricht ab, sobald der Checkout vom Tag abweicht. Auch Hotfixes laufen über Commit → Tag → Deploy.
- **Assets sind versioniert** (`public/js`, `public/css`, `public/assets`, `public/vendor`). Auf dem
Server wird nichts gebaut. Laravel Mix nur lokal, und nur wenn sich Quellen unter `resources/assets` ändern.
- **Migrationen additiv**, damit der Rollback einfach der vorherige Tag ist. Kein `migrate:rollback` auf Live.
- **Kein `route:cache`, kein `config:cache`, kein `optimize`.** Die Routen hängen vom Host ab
(`RouteServiceProvider::loadDomainAwareRoutes`): ein Routen-Cache von der Kommandozeile enthält nur die
`main`-Routen, Shop/CRM/Portal fallen aus. Außerdem liest Code `env()` direkt (`env('APP_KEY')` in
`App\User`, `UserRepository`, `CustomerRepository`; `BUSINESS_FORCE_EXECUTE`), das liefert bei
gecachter Config `null`. Das alte `clean_script_server.sh` ruft `optimize` auf — **nicht verwenden**.
---
## A — Einmalig: Server-Zugang zum Git-Server
Als `ploi` auf dem Server:
```bash
git --version
ssh-keygen -t ed25519 -f ~/.ssh/deploy_mivita -N "" -C "mivita-live"
cat ~/.ssh/deploy_mivita.pub # in Forgejo am Repository kadmin/mivita als Deploy-Key, NUR LESEN
```
`~/.ssh/config`:
```
Host forge.adametz.media
Port 222
User git
IdentityFile ~/.ssh/deploy_mivita
IdentitiesOnly yes
```
```bash
ssh-keyscan -p 222 forge.adametz.media >> ~/.ssh/known_hosts
ssh-keygen -lf <(ssh-keyscan -p 222 forge.adametz.media 2>/dev/null) # mit Forgejo vergleichen
git ls-remote ssh://git@forge.adametz.media:222/kadmin/mivita.git | grep tags # zeigt die Tags
```
**PHP und Composer prüfen** (die `composer.lock` verlangt PHP 8.4):
```bash
php -v && composer --version
cd /home/ploi/mivita.care && composer check-platform-reqs --no-dev
```
**FPM-Reload ohne Passwort testen** — immer Reload, nie Restart (Restart → kurz 502):
```bash
ps -o pid,lstart,args -C php-fpm8.4 | head -3
echo "" | sudo -S service php8.4-fpm reload; echo "Exit: $?"
sleep 2; ps -o pid,lstart,args -C php-fpm8.4 | head -3 # master-PID gleich, pool www neu
```
Fragt `sudo` nach einem Passwort: beim Deploy `DEPLOY_FPM_RELOAD= bash dev/betrieb/deploy.sh …`,
das Skript wartet dann auf Enter, bis FPM in Ploi neu geladen ist.
In Ploi: **„Quick Deploy“ und Deploy-Webhook aus.** Horizon einrichten: Abschnitt F.
## B — Einmalig: Live-Verzeichnis an Ort und Stelle zum Checkout machen
Umgestellt wird direkt auf **`v1.0.1`** (nicht `v1.0.0`): ab `v1.0.1` sind `storage/fonts`,
`storage/reports`, `storage/oauth-*.key` und `_static/` nicht mehr versioniert — ein Checkout von
`v1.0.0` nach `v1.0.1` würde sie auf Live löschen.
```bash
cd /home/ploi
tar czf backup-vor-git-$(date +%F).tgz --exclude=./storage --exclude=./node_modules --exclude=./vendor -C mivita.care .
cd mivita.care
git init -q
git remote add origin ssh://git@forge.adametz.media:222/kadmin/mivita.git
git fetch --tags origin
git config core.fileMode false # Dateirechte (SFTP 644 vs. Repo 755) nicht als Änderung werten
git update-ref --no-deref HEAD "$(git rev-parse 'v1.0.1^{commit}')"
git read-tree HEAD # Index = Tag, Dateien bleiben unangetastet
git show HEAD:.gitignore > .git/info/exclude # Live hatte keine .gitignore; bis zum Checkout
git status --porcelain | cut -c1-2 | sort | uniq -c
```
| Zeile | Bedeutung | Vorgehen |
|---|---|---|
| ` M datei` | Live weicht vom Tag ab | `git diff -- datei`. Ist es ein Live-Hotfix, der im Repository fehlt: erst lokal nachziehen, neuer Tag. Ist Live nur älter: wird beim Checkout aktualisiert |
| ` D datei` | fehlt auf Live | meist Doku/Tests (`dev/`, `tests/`); der Checkout legt sie an |
| `?? datei` | nur auf Live | **nicht löschen** — nach `~/vor-git/` verschieben oder (neuer Tag) in `.gitignore` aufnehmen |
Erwartete Funde bei mivita:
- **`public/vendor/…` als `??`**: Das Repository enthält nur noch die tatsächlich eingebundenen
Theme-Dateien (390 statt ~1300). Die übrigen (Demos, Tests, ungenutzte Bibliotheken, PHP-Beispielskripte
von slim-image-cropper) nach `~/vor-git/public-vendor/` verschieben:
```bash
mkdir -p ~/vor-git
git status --porcelain -- public/vendor | grep '^??' | cut -c4- | while read -r p; do
mkdir -p ~/vor-git/"$(dirname "$p")" && mv "$p" ~/vor-git/"$p"; done
```
- Per SFTP gelöschte Altdateien und alte Deploy-Archive (`storage/` ist ignoriert, bleibt unberührt).
- Pfade mit Umlauten/Leerzeichen setzt Git in Anführungszeichen — beim Filtern beachten.
Wenn nichts Unerklärtes übrig ist:
```bash
git checkout -q -f --detach v1.0.1
: > .git/info/exclude # ab jetzt gilt die .gitignore aus dem Checkout
echo v1.0.1 > RELEASE
git status --porcelain # muss leer sein
composer install --no-dev --optimize-autoloader --no-interaction
php artisan config:clear && php artisan route:clear && php artisan view:clear
php artisan horizon:terminate
echo "" | sudo -S service php8.4-fpm reload
php artisan about --only=mivita && php artisan monitoring:queue-probe --wait=60
```
Bringt `v1.0.1` gegenüber Live neue Migrationen mit: vorher DB-Backup, nach dem Checkout `php artisan migrate --force`.
---
## C — Release lokal (jedes Mal)
```bash
# 1. sauber und grün
git status # nichts offen
php artisan test --compact # bzw. die betroffenen Tests
# 2. nur wenn sich resources/assets geändert hat: Assets bauen und committen
npm run production && git add public/js public/css public/vendor public/mix-manifest.json
# 3. auf master, Tag mit Release-Notiz, pushen (Tags mitnehmen!)
git checkout master && git merge --ff-only <arbeitsbranch>
git tag -a vX.Y.Z -m "vX.Y.Z — <Inhalt>
Deploy: <keine Migration | N Migrationen>, <neue .env-Werte>, <Seeder/Commands danach>
bash dev/betrieb/deploy.sh vX.Y.Z [--migrations]"
git push origin master --follow-tags
git push nas master --follow-tags # Spiegel auf dem NAS
```
## D — Deploy (jedes Mal, auf dem Server)
1. Bringt der Tag Migrationen mit: **DB-Backup**.
2. Neue `.env`-Werte laut Release-Notiz vorher eintragen.
3. Deploy:
```bash
cd /home/ploi/mivita.care
bash dev/betrieb/deploy.sh vX.Y.Z # ohne Migrationen
bash dev/betrieb/deploy.sh vX.Y.Z --migrations # mit Migrationen
```
4. Nacharbeiten aus der Release-Notiz (Seeder, Snapshot-Neuaufbau, …).
| Schritt im Skript | Bei Fehler |
|---|---|
| Arbeitsverzeichnis sauber? Tag auf origin? Neue Migrationen ohne `--migrations`? | Abbruch, nichts verändert |
| `down` → Checkout → `RELEASE` → `composer install --no-dev` → `migrate` → Caches leeren → `horizon:terminate` → FPM-Reload → `up` | Seite bleibt im Wartungsmodus, Skript nennt den Rollback-Befehl |
| `about --only=mivita` (Version) | — |
| `monitoring:queue-probe --wait=60` (entfällt bei `QUEUE_WORKER="none"`) | **Exit 3**: live, aber Horizon holt keine Jobs ab — `horizon:status`, Daemon in Ploi prüfen |
Exit-Codes: `0` fertig · `1` abgebrochen · `2` falscher Aufruf · `3` kein Worker.
Option `--clear-schedule` setzt `withoutOverlapping`-Sperren zurück (nur wenn nötig).
Lange Migrationen: `DB_LOCK_WAIT_TIMEOUT=300 bash dev/betrieb/deploy.sh vX.Y.Z --migrations`.
Das Skript läuft aus einer Kopie des **vorher** ausgecheckten Stands — Änderungen an `deploy.sh`
greifen erst beim übernächsten Deploy.
**Prüfen:** `php artisan about --only=mivita` zeigt den Tag; Startseite, ein Berater-Shop, CRM-Login und
Checkout laden; `storage/logs/laravel-*.log` ohne neue Fehler; `/horizon` ohne neue Failed Jobs.
## E — Rollback
```bash
bash dev/betrieb/deploy.sh <vorheriger Tag>
```
Baut `vendor/` für den alten Tag neu auf. Nur bei Datenproblemen das DB-Backup einspielen.
## F — Einmalig: Horizon aktivieren (Umstellung von `QUEUE_DRIVER=sync`)
Bis Oktober 2026 lief Live mit `QUEUE_DRIVER=sync`: Jobs liefen im Request mit (Stripe-Webhook
wartete z. B. auf den Rechnungsnummer-Sync und SMTP). Mit Horizon laufen sie im Hintergrund.
Betroffen: `SyncStripeInvoiceReference`, Fehlermails aus `MyLog` (`MailLog`), `PaymentIncidentAlert`,
DHL-Jobs (nur bei `DHL_USE_QUEUE`).
Voraussetzung: Der Tag enthält die Horizon-Einstellungen (alle Queues inkl. `high-priority`/`dhl-*`,
`after_commit` an der Redis-Verbindung, Gate für System-Admins, `horizon:snapshot`).
```bash
cd /home/ploi/mivita.care
redis-cli ping # PONG
grep -E '^(APP_ENV|QUEUE_DRIVER|REDIS_HOST)=' .env # APP_ENV=production
```
1. In Ploi → Site → **Daemons**: Befehl `php8.4 artisan horizon`, Verzeichnis `/home/ploi/mivita.care`,
Benutzer `ploi`, 1 Prozess. Prüfen: `php artisan horizon:status` → `running`.
2. In `.env`: `QUEUE_DRIVER=redis` (`config/queue.php` liest `QUEUE_DRIVER`, nicht `QUEUE_CONNECTION`).
Ohne Config-Cache wirkt das sofort für Web-Requests; Horizon neu starten: `php artisan horizon:terminate`.
3. `php artisan monitoring:queue-probe --wait=60` → „Queue-Worker läuft“.
4. `/horizon` als System-Admin (admin ≥ 4) öffnen: Supervisor aktiv, keine Failed Jobs.
5. Nach der nächsten Stripe-Zahlung: Rechnungsnummer in den PaymentIntent-Metadaten.
Zurück: `QUEUE_DRIVER=sync` in `.env`, in `deploy.sh` `QUEUE_WORKER="none"`. Liegen gebliebene Jobs
vorher mit `php artisan queue:work redis --stop-when-empty` abarbeiten.
## G — Überwachung: Systemstatus und `/health`
**Systemstatus** im CRM unter Sysadmin → „Systemstatus“ (`https://my.mivita.care/sysadmin/status`, nur admin ≥ 4):
Datenbank, Cache, Queue (sync / database / Horizon), fehlgeschlagene Jobs der letzten 24 h,
Scheduler-Herzschlag, Plattenplatz, Version, zuletzt fehlgeschlagene Jobs.
**`/health`** liefert dieselben Prüfungen als JSON für einen externen Monitor, auf jeder Domain
(z. B. `https://mivita.care/health`). 200 = in Ordnung oder Warnung, 503 = kritisch. Ohne Token 404.
Einrichtung auf Live (einmalig):
```bash
cd /home/ploi/mivita.care
openssl rand -hex 24 # Wert als MONITORING_HEALTH_TOKEN in die .env
curl -s -H "Authorization: Bearer <token>" https://mivita.care/health
```
Der **Scheduler-Herzschlag** (`monitoring:heartbeat`, minütlich) zeigt, ob der Cron `schedule:run`
wirklich läuft. Steht er nach dem Deploy auf „Kritisch“, ist in Ploi kein Scheduler-Eintrag
(`php artisan schedule:run` jede Minute) aktiv.
Schwellen in `.env` (optional): `MONITORING_QUEUE_BACKLOG_SECONDS=300`, `MONITORING_DISK_MIN_FREE_PERCENT=10`.
**Fehlerprotokoll GlitchTip:** Exceptions, Fehlersignale (toter interner Link, 403/429, 419) und
Browserfehler — Einrichtung, Werte und Auswertung in [`dev/glitch-tip/README.md`](../glitch-tip/README.md).
Live-`.env`: `SENTRY_LARAVEL_DSN`, `SENTRY_BROWSER_DSN`, `SENTRY_ENVIRONMENT=production`,
`MONITORING_NTFY_URL`, `MONITORING_NTFY_TOPIC`, `MONITORING_NTFY_TOKEN`.
**Alarm-Mails** (`App\Support\OperationalAlert`): Schlägt eine geplante Aufgabe fehl (Exit-Code ≠ 0,
z. B. `business:store-optimized`, `user:make_abo_order`) oder ein Queue-Job endgültig, geht eine Mail an
`MONITORING_ALERT_EMAIL` (Standard: `EXCEPTION_MAIL`). Gleiche Meldungen höchstens einmal pro Stunde
(`MONITORING_ALERT_THROTTLE_MINUTES`); ins Log `storage/logs/monitoring-*.log` kommt jede. Optional
zusätzlich Push über ntfy (`MONITORING_NTFY_URL`, `_TOPIC`, `_TOKEN`). Test auf Live:
```bash
php artisan tinker --execute 'App\Support\OperationalAlert::send("Test-Alarm");'
```
Sperrdauern von `withoutOverlapping` sind kurz (10–60 Min.) statt 24 h: ein abgebrochener Lauf
blockiert die Aufgabe sonst einen ganzen Tag.
## H — Git-Historie bereinigt (08.10.2026)
Die Historie wurde mit `git filter-repo` neu geschrieben (7,2 GB → 41 MB, **alle Commit-Hashes und Tags
neu**). Entfernt: `.env`, `storage/oauth-*.key`, `storage/reports/`, Kundenliste, `KasSLLController.php`
(SSL-Schlüssel), `_ide_helper*` (eine Version 10 GB), Archive, `_static/`, `dev/app-bak/`.
Alte Klone sind unbrauchbar: neu klonen (bzw. `.git` ersetzen). Auf Live wurde `.git` neu aufgesetzt
(`git init`, `fetch --tags`, `update-ref` auf den Tag aus `RELEASE`, `read-tree`).
Hashes in älteren Doku-Dateien (`dev/…`) beziehen sich auf die alte Historie.
`deploy.sh` läuft nur noch, wenn `php artisan env` `production` meldet (Schutz gegen versehentliches
Ausführen auf dem Mac; zum Testen `DEPLOY_ALLOW_NON_PRODUCTION=1`).
## Fallstricke
- **FPM-Restart statt Reload** → 502 für Sekunden.
- **Horizon nicht neu gestartet** → Worker arbeiten mit altem Code (neue Job-Klassen unbekannt).
Das Skript ruft `horizon:terminate`; die Worker-Prüfung am Ende fängt einen nicht zurückgekehrten Horizon ab.
- **Neue Queue** (`onQueue('…')`) → in `config/horizon.php` unter `queue` eintragen, sonst bleibt der Job
liegen. Ein Test (`HorizonSetupTest`) prüft das für `app/Jobs`.
- **Nie `queue:retry all`**: alte Jobs tragen den Klassenstand von damals. Erst `queue:failed`, dann gezielt.
- **Push ohne Tags** → `deploy.sh` findet den Tag nicht. Immer `--follow-tags`.
- **Übersetzungen im Admin** (`TranslationFileController`) schreiben direkt nach `resources/lang` auf Live.
Danach bricht `deploy.sh` mit ` M resources/lang/…` ab. Dann die Datei von Live holen, lokal committen,
neuen Tag deployen (die Live-Änderung ist damit im Tag) — **nicht** auf Live verwerfen.
- **Nichts per SFTP in versionierte Ordner** (auch keine Bilder unter `public/img/…`): neue Dateien
gehören in einen Commit, sonst bricht `deploy.sh` wegen `??` ab.
- **Von Laravel erzeugte Dateien** (`bootstrap/cache/*.php`, `storage/fonts`, …) gehören nie ins
Repository — sie unterscheiden sich zwischen lokal (mit Dev-Paketen) und Live.
- **`.gitignore` enthält `Icon`** (macOS). Bei `core.ignorecase=true` trifft das auch Ordner `icon/` —
neue Ordner dieses Namens werden still ignoriert. Prüfen mit
`git ls-files --others --ignored --exclude-standard --directory | grep -v -E '^(vendor|node_modules|storage)/'`.
- **Wartungsmodus** gilt für alle Domains inkl. Webhooks (Stripe, PAYONE). Stripe und PAYONE wiederholen
Zustellungen; der Wartungsmodus dauert nur Sekunden.