From f0ec8614b2ba43e938ec88b083dbac6ca4a04a35 Mon Sep 17 00:00:00 2001 From: fs Date: Fri, 31 Jul 2026 16:13:14 +0200 Subject: [PATCH] Deploy-Anleitung + Cron-Endpoint stilllegen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Tarif hat jetzt SSH und Shell-Cronjobs. Damit fällt die Begründung für public/cron.php weg: der Endpoint war ausschließlich der Ersatz dafür. cron.key steht in der Produktiv-Config auf '' (403 auf jeden Aufruf), der Code bleibt als Rückweg samt Begründung stehen. docs/deploy.md beschreibt den Weg in kleinen Schritten. Kern: die neue Seite läuft erst auf einer per Verzeichnisschutz geschlossenen Test-Subdomain mit DERSELBEN Produktiv-Config, die später live geht — kein Config-Wechsel beim Umschalten. Möglich, weil die Host-Weiterleitung in public/.htaccess nur beim exakten Host ohne www greift. Die alte CMS-Seite bleibt bis zuletzt online und ist der Rückweg. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NzeVVgMCrzCDJAKeLEdKr7 --- CLAUDE.md | 25 ++-- docs/deploy.md | 320 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 338 insertions(+), 7 deletions(-) create mode 100644 docs/deploy.md diff --git a/CLAUDE.md b/CLAUDE.md index 36b723c..c2d1ed9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,8 @@ selbst gehosteter Stack ohne Abhängigkeiten von Drittanbietern. - **Frontend:** Natives HTML, Vanilla CSS, Vanilla JS. Kein Framework, keine Build-Pipeline, kein Node. - **Backend:** Vanilla PHP ≥ 8.1, Composer nur für `phpmailer/phpmailer`. Keine Template-Engine. -- **Hosting-Ziel:** Apache Shared Hosting, PHP 8.x, `.htaccess`, Cronjobs verfügbar. +- **Hosting-Ziel:** All-Inkl Shared Hosting, PHP 8.x, `.htaccess`, SSH **und** Shell-Cronjobs + (seit 31.07.2026 im Tarif). Deploy-Anleitung Schritt für Schritt: **`docs/deploy.md`**. - **Lokales Dev:** `php -d upload_max_filesize=250M -d post_max_size=260M -S localhost:8000 -t public public/index.php` (index.php fungiert als Router-Script, weil `php -S` kein .htaccess kennt). Die beiden `-d`-Schalter brauchst du für die Dateiablage `/dateien`: der CLI-Server liest keine `.user.ini`, und mit den @@ -329,11 +330,21 @@ Keine rohen Werte in `base/layout/components/utilities.css` — es gibt für all - [ ] Keine Duplikate: Inhalte/Werte aus den Single-Source-Dateien beziehen - [ ] `php -l` sauber; Seite lokal geprüft -## Cron-Endpoint `public/cron.php` (Ausnahme 3b) +## Cron-Endpoint `public/cron.php` (Ausnahme 3b) — **stillgelegt** -**Der Tarif kennt keine zeitgesteuerten Aufgaben** — das KAS meldet „in deinem Tarif nicht verfügbar", -weder Shell- noch URL-Cronjobs (geprüft 27.07.2026). Die Sync-Skripte müssen aber getaktet laufen, also -kommt der Takt von außen: ein externer Cron-Dienst ruft +**Seit 31.07.2026 hat der Tarif SSH und echte Shell-Cronjobs.** Damit ist der Endpoint überflüssig: +er war ausschließlich der Ersatz für den fehlenden Cron. `cron.key` steht in der Produktiv-Config auf +`''`, der Endpoint antwortet also auf jeden Aufruf mit 403 — ein Geheimnis weniger, das bei einem +externen Dienst liegt und leaken kann. Der Takt läuft über die Crontab-Zeilen aus +`config/config.example.php` (Weg A), der Ablauf steht in **`docs/deploy.md`**, Teil D. + +**Der Code bleibt.** Er ist geprüft, kostet nichts (403 in drei Zeilen) und ist der Rückweg, falls der +Shell-Cron ausfällt: neuen Schlüssel in `cron.key`, fertig. Deshalb sind die Grenzen unten weiter +beschrieben, statt sie mit dem Code zu löschen — wer ihn je wieder aufschließt, braucht sie. +`bin/preflight.php` warnt bei leerem Schlüssel und weist damit auf genau diese Entscheidung hin; +die Warnung ist erwartet, kein Mangel. + +Die Begründung von damals, weiterhin gültig für den Fall der Reaktivierung: ein externer Cron-Dienst ruft `https://…/cron.php?job=matchcenter` bzw. `?job=instagram` auf, der Endpoint startet das Skript serverseitig. Schlüssel aus `config('cron.key')`, per Header `X-Cron-Key` **oder** als `key`-Parameter. @@ -363,8 +374,8 @@ Besucher keine Aktualisierung, und Fire-and-Forget ist auf Shared Hosting unzuve `$argv` wird im Endpoint gesetzt, weil es im Web-SAPI nicht existiert. `bin/preflight.php` prüft Schlüssellänge, Verwechslung mit `app_secret` und ob `min_interval` den -gewünschten Takt sperrt. **Wenn der Tarif je Cronjobs bekommt:** `cron.key` leeren, dann ist der -Endpoint zu, und der Takt läuft wieder wie in `config.example.php` dokumentiert. +gewünschten Takt sperrt. Der damals hier notierte Ausstieg („wenn der Tarif je Cronjobs bekommt, +`cron.key` leeren") ist am 31.07.2026 genau so eingetreten und vollzogen. **Offen:** ob der Instagram-Scraper von der Server-IP überhaupt durchkommt. Instagram sperrt Rechenzentren; hier lief er nur über einen privaten Anschluss. Einmal auf dem Server prüfen. Falls er diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 0000000..95ec4d4 --- /dev/null +++ b/docs/deploy.md @@ -0,0 +1,320 @@ +# Deploy auf All-Inkl (Shared Hosting, SSH + Cronjobs) + +Schritt für Schritt vom leeren Webspace zur Live-Seite. Die Reihenfolge ist Absicht: +**die alte CMS-Seite bleibt online, bis die neue auf einer geschützten Test-Subdomain +nachweislich läuft.** Umgeschaltet wird erst am Ende, und das Umschalten ist in einer +Minute rückgängig. + +Der Kern des Plans: die Test-Subdomain läuft mit **derselben** `config.php`, die +später live geht. Es gibt keinen Config-Wechsel beim Umschalten, also auch keinen +Fehler, der sich dabei einschleichen kann. Möglich ist das, weil die +Host-Weiterleitung in `public/.htaccess` nur beim exakten Host `tsv08kulmbach.de` +greift; eine Subdomain läuft unbehelligt durch. Einzige Abweichung: Canonical, OG +und Sitemap nennen auf der Test-Subdomain schon die echte Domain. Genau richtig für +den Umschaltmoment, und dank Verzeichnisschutz sieht es ohnehin keine Suchmaschine. + +Legende: `[lokal]` auf deinem Mac, `[SSH]` in der Server-Shell, `[KAS]` im +All-Inkl-Kundenmenü. + +--- + +## Teil A — Vorbereitung lokal + +### A1 [lokal] Die zwei offenen Werte in `config/config.production.php` setzen + +`php bin/preflight.php` bricht sonst auf dem Server ab. Beides betrifft nur die +Dateiablage `/dateien`, nicht die Website selbst. + +**Passwörter für die zwei Zugänge.** Als Hash, nicht als Klartext: + +```bash +php -r 'echo password_hash("DEIN-PASSWORT", PASSWORD_DEFAULT), "\n";' +``` + +Ausgabe (beginnt mit `$2y$…`) in `dateien.users.verein.password_hash` bzw. +`dateien.users.admin.password_hash` eintragen. Zwei verschiedene Passwörter: `verein` +gibt der Verein weiter, `admin` behältst du. + +**Lizenzschlüssel** der Files Gallery in `dateien.license_key`. Fehlt er, läuft die +Ablage trotzdem, zeigt aber den Hinweis der unlizenzierten Version (nur eine Warnung +in `preflight`, kein Blocker). + +Bereits erledigt: `cron.key` ist auf `''` gesetzt und der Endpoint `public/cron.php` +damit zu. Der Takt kommt ab jetzt aus echten Cronjobs (Teil D), der Endpoint war nur +der Ersatz für den fehlenden Cron. + +### A2 [lokal] Alles gepusht? + +```bash +git status --porcelain # muss leer sein +git push origin main +``` + +--- + +## Teil B — Server einrichten + +### B1 [SSH] Umgebung feststellen und notieren + +```bash +ssh @.kasserver.com +pwd # z. B. /www/htdocs/w01ca75d → das ist HOME +which php; php -v # PHP muss >= 8.1 sein +git --version +``` + +Drei Dinge, die du für später brauchst: den **Pfad aus `pwd`**, den **Pfad aus +`which php`** und die **PHP-Version**. Ist `php` älter als 8.1, prüfe nach +versionierten Binaries (`ls /usr/bin/php*`, `ls /usr/local/bin/php*`) und nimm den +passenden absoluten Pfad; die Cronjobs brauchen ihn ohnehin absolut. + +### B2 [SSH] Repo klonen + +```bash +cd ~ +git clone https://onion.breadcrumb-online.de/fs/tsv08kulmbach-website.git tsv08kulmbach +cd tsv08kulmbach && pwd # diesen Pfad notieren: das ist SITE +``` + +Der Klon lädt gut 460 MB (Website plus das Vereinsmaterial der Dateiablage), das +dauert ein paar Minuten. Die alte CMS-Seite liegt in einem anderen Verzeichnis und +wird nicht angefasst. + +### B3 [SSH] `config.php` platzieren + +Die Datei ist gitignored, also nicht im Klon. Vom Mac hochladen: + +```bash +# [lokal], in einem zweiten Terminal +scp config/config.production.php \ + @.kasserver.com:~/tsv08kulmbach/config/config.php +``` + +Wichtig: Ziel heißt `config.php`, nicht `config.production.php`. Danach [SSH]: + +```bash +chmod 600 config/config.php # enthält SMTP-Passwort und app_secret +``` + +### B4 [SSH] Composer-Abhängigkeiten + +```bash +cd ~/tsv08kulmbach +composer install --no-dev --optimize-autoloader +``` + +Kennt die Shell `composer` nicht, hol ihn dir lokal ins Verzeichnis: + +```bash +curl -sS https://getcomposer.org/installer | php +php composer.phar install --no-dev --optimize-autoloader +rm composer.phar +``` + +Es wird nur PHPMailer installiert. + +### B5 [SSH] Schreibrechte prüfen + +PHP läuft unter deinem Benutzer, meist passt es also schon. `preflight` sagt es dir +in Teil C4 verbindlich. Falls dort etwas fehlt: + +```bash +chmod -R u+rwX storage +``` + +Schreiben muss die Seite in `storage/logs`, `storage/cache`, `storage/ratelimit`, +`storage/dateien/uploads` und `storage/dateien/system`. + +--- + +## Teil C — Auf der Test-Subdomain prüfen + +### C1 [KAS] Subdomain anlegen und schützen + +1. **Domains** → Subdomain `neu.tsv08kulmbach.de` anlegen. +2. Als Verzeichnis **`/tsv08kulmbach/public`** eintragen, nicht das Repo-Root. + Damit liegen `app/`, `config/`, `data/`, `storage/` und `vendor/` außerhalb des + Docroots und sind grundsätzlich nicht per URL erreichbar. (Die Root-`.htaccess` + ist nur ein Notnagel für Hoster, die das nicht können.) +3. **PHP-Version** für die Subdomain auf 8.1 oder neuer stellen. +4. **SSL** aktivieren (Let's Encrypt). Ohne Zertifikat läuft die Seite in eine + Endlosweiterleitung, denn `public/.htaccess` erzwingt HTTPS. +5. **Verzeichnisschutz** für die Subdomain einschalten (Basic Auth). Das ist der + Grund, warum hier nichts indexiert werden kann. Nicht weglassen. + +### C2 Erreichbarkeit + +`https://neu.tsv08kulmbach.de` aufrufen. Nach der Passwortabfrage muss die +Startseite kommen. Weißes Blatt oder 500er? Dann [SSH] +`tail -30 storage/logs/php-errors.log`. + +### C3 [SSH] Datenstände erzeugen + +`data/matchcenter.json` und `data/instagram.json` sind bewusst nicht im Repo, die +holen die Syncs. Vor dem ersten Lauf fehlen Tabellen, Spiele und der +Instagram-Block (die Seite bricht deswegen nicht, sie zeigt Ersatzinhalte). + +```bash +cd ~/tsv08kulmbach +php bin/matchcenter-sync.php # Spiele, Ergebnisse, Tabellen, Wappen +php bin/instagram-sync.php # Posts und Bilder +``` + +**Der Instagram-Lauf ist der Wackelkandidat des ganzen Deploys.** Instagram sperrt +Rechenzentrums-IPs; bisher lief der Scraper nur über einen privaten Anschluss. +Schlägt er hier fehl, ist das kein Fehler im Deploy: dann bleibt der Weg, ihn von +zu Hause laufen zu lassen und `data/instagram.json` samt Bildern hochzuladen, oder +der Block wird redaktionell gepflegt. Die Website funktioniert in jedem Fall. + +### C4 [SSH] `preflight` bis null Fehler + +```bash +php bin/preflight.php; echo "Exit: $?" +``` + +Ziel ist **Exit 0**. Warnungen darfst du bewerten, Fehler nicht. Erwartbare +Warnungen hier: der Hinweis, dass `cron.key` leer ist (richtig so, siehe A1) und +die Uploadgrenze, die erst ein echter Upload beweist (C6). + +### C5 Durchklicken + +Die Seiten, die eigene Datenquellen haben: + +- [ ] Startseite: Banner-Slider, Termin-Karte im Hero, Instagram-Block +- [ ] `/matchcenter`: Tabellen und nächste Spiele mit Wappen +- [ ] `/termine`, `/news`, `/veranstaltungen` plus eine Detailseite +- [ ] `/stadionzeitung` und die Ausgabe durchswipen — **alle Wappen und + Instagram-Bilder da?** (Sie liegen im Repo, sollten also stimmen.) +- [ ] `/stadionzeitung/aktuell` landet auf der neuesten Ausgabe +- [ ] Die Druckfassung einer Ausgabe (`?druck=1`) +- [ ] `/impressum`: Webadresse zeigt `www.tsv08kulmbach.de`, nicht die Subdomain + +### C6 Dateiablage und Mailversand + +```bash +php bin/dateien-init.php # Jahr/Abteilung-Ordner, MUSS auf dem Server laufen +php bin/smtp-test.php # nur Verbindung und Login, kein Versand +``` + +`dateien-init.php` gehört auf den Server, weil Umlaute per FTP zu Doppelordnern +führen (macOS NFD gegen Linux NFC). + +- [ ] `/dateien` mit beiden Zugängen anmelden +- [ ] Eine große Datei hochladen (>10 MB) — beweist die 250-MB-Grenze +- [ ] Ein Formular abschicken (z. B. `/kontakt`) und die Mail im Postfach prüfen +- [ ] `php bin/logs.php` zeigt keine Fehler + +--- + +## Teil D — Cronjobs + +Erst jetzt, damit ab dem Umschalten schon frische Daten liegen. `SITE` und der +PHP-Pfad kommen aus B1/B2. + +[KAS] → **Cronjobs** → eigener Cronjob (Shell), vier Einträge: + +``` +# Instagram 2x/Tag +17 6,18 * * * cd SITE && PHP bin/instagram-sync.php >> storage/logs/instagram.log 2>&1 + +# Matchcenter Grundtakt 2x/Tag +37 7,19 * * * cd SITE && PHP bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1 + +# Matchcenter am Spieltag halbstuendlich (Sa+So 13-21 Uhr) +7,37 13-21 * * 6,0 cd SITE && PHP bin/matchcenter-sync.php >> storage/logs/matchcenter.log 2>&1 + +# Logpflege sonntags nachts +23 3 * * 0 cd SITE && PHP bin/log-rotate.php > /dev/null 2>&1 +``` + +`SITE` und `PHP` durch die echten absoluten Pfade ersetzen, etwa +`cd /www/htdocs/w01ca75d/tsv08kulmbach && /usr/bin/php bin/…`. Die Minuten sind +versetzt, damit sich zwei Läufe nicht überlappen. Alle Skripte sind CLI-only, +sperren sich per Lock gegen Parallelläufe und lassen bei Fehlern den letzten guten +Cache stehen — ein fehlgeschlagener Lauf schadet nie. + +Am Tag nach dem ersten Cron-Lauf einmal `php bin/logs.php` ansehen. Läuft nichts, +ist fast immer der PHP-Pfad falsch oder relativ. + +--- + +## Teil E — Umschalten auf die Hauptdomain + +Ab hier ist die Seite öffentlich. Alles davor muss sitzen. + +### E1 Sicherheitsnetz + +Notiere, auf welches Verzeichnis `www.tsv08kulmbach.de` **jetzt** zeigt (die alte +CMS-Seite). Das ist dein Rückweg. Verzeichnis und Datenbank der alten Seite bleiben +unangetastet, nicht löschen. + +### E2 [KAS] Domain umstellen + +**Domains** → `tsv08kulmbach.de` und `www.tsv08kulmbach.de` auf +**`/tsv08kulmbach/public`** zeigen lassen. PHP-Version prüfen, SSL muss für beide +aktiv sein. + +### E3 Sofort danach prüfen + +- [ ] `https://www.tsv08kulmbach.de` lädt +- [ ] `http://tsv08kulmbach.de` (ohne www, ohne s) landet per 301 auf + `https://www.tsv08kulmbach.de` +- [ ] `/sitemap.xml` und `/robots.txt` erreichbar +- [ ] `curl -sI https://www.tsv08kulmbach.de | head` — Statuszeile 200, kein + Redirect-Loop +- [ ] Ein Formular live abschicken +- [ ] **Einen gedruckten QR-Code mit dem Handy scannen.** Der Code der + Stadionzeitung zeigt auf `www.tsv08kulmbach.de/stadionzeitung/aktuell`. + Scannen ist die einzige echte Prüfung: die QR-Bibliothek arbeitet nicht + reproduzierbar, ein Bildvergleich beweist nichts. + +### E4 Verzeichnisschutz der Test-Subdomain lassen + +Nicht entfernen. Sonst steht die Seite zweimal im Netz und Google sieht doppelten +Inhalt. Wenn du die Subdomain nicht mehr brauchst, in KAS löschen. + +--- + +## Teil F — Danach + +### F1 Der Update-Ablauf ab jetzt + +**Die Reihenfolge ist nicht verhandelbar**, weil `storage/dateien/uploads/` im Repo +liegt und andere Menschen dort hochladen: + +```bash +# 1. [SSH] auf dem Server: neue Uploads sichern +cd ~/tsv08kulmbach +git add -A storage/dateien/uploads && git commit -m "Uploads vom Server" && git push + +# 2. [lokal] holen, dann entwickeln +git pull + +# 3. [lokal] fertig, gepusht → [SSH] ausrollen +cd ~/tsv08kulmbach && git pull && php bin/preflight.php +``` + +Nie in der anderen Reihenfolge. Details und Begründung in CLAUDE.md, Abschnitt +Dateiablage. + +Ändert sich `composer.json`, gehört ein `composer install --no-dev +--optimize-autoloader` dazu. Neue Formularfelder heißen: Datenschutzerklärung +mitziehen. + +### F2 Rückweg, falls etwas Grundlegendes bricht + +[KAS] Domain zurück auf das alte Verzeichnis aus E1. Nach ein paar Minuten ist die +alte Seite wieder da. Kein Datenverlust, weil die neue Seite in einem eigenen +Verzeichnis liegt und die alte Datenbank nicht anfasst. + +### F3 Offen, nach dem Livegang + +- **301-Weiterleitungen der alten URLs.** Die neue Seite hat andere Slugs; alte + Google-Treffer und fremde Links laufen ins Leere. Die alten Adressen stehen im + DB-Dump unter `.context_db`. Aufwand lohnt für die paar wichtigen Seiten, nicht + für alle. +- **Google Search Console:** Property anlegen, `sitemap.xml` einreichen. +- **Instagram-Sync** beobachten, siehe C3. +- Nach dem Sportfest (ab 07.09.2026) fallen zwei Anzeigen per `bis`-Datum aus dem + Bestand, die Stadionzeitung hat dann 38 Seiten statt 40 und ist nicht mehr durch + 4 teilbar. `bin/stadionzeitung-add.php` warnt beim nächsten Lauf.