Initial: Mesh-Skills, Configs, Scripts
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
---
|
||||
name: web-hosting-deployment
|
||||
description: "Nginx Multi-Domain-Hosting mit Cloudflare DNS, Let's Encrypt SSL und Mail-Preservation – Deployment-Workflow für mehrere Domains auf einem Server"
|
||||
version: 1.0.0
|
||||
author: Agent A (netcup VPS Deployer)
|
||||
platforms: [linux]
|
||||
---
|
||||
|
||||
# Web-Hosting Deployment – Multi-Domain-Setup
|
||||
|
||||
Richtet mehrere Domains mit name-based virtual hosting auf einem einzelnen nginx-Server ein, inklusive Cloudflare-DNS-Verwaltung per API, Let's-Encrypt-SSL und sicherer MX-Record-Migration.
|
||||
|
||||
## Workflow-Übersicht
|
||||
|
||||
1. **Server prüfen** → nginx installiert? Ports 80/443 frei? UFW aktiv?
|
||||
2. **DNS prüfen** → Wo zeigen die Domains hin? Welcher Provider?
|
||||
3. **nginx VHosts anlegen** → Eine Config pro Domain in `sites-available/`
|
||||
4. **DNS umstellen** → Per Cloudflare-API oder manuell A-Records auf Server-IP
|
||||
5. **SSL per certbot** → Let's Encrypt für alle Domains gleichzeitig
|
||||
6. **Mail schützen** → MX-Records 1:1 spiegeln, niemals proxien
|
||||
|
||||
---
|
||||
|
||||
## Schritt 1: nginx installieren & Basis-Check
|
||||
|
||||
```bash
|
||||
apt-get update -qq && apt-get install -y nginx certbot python3-certbot-nginx
|
||||
systemctl enable --now nginx
|
||||
ufw allow 80/tcp && ufw allow 443/tcp && ufw allow 22/tcp
|
||||
ufw --force enable
|
||||
```
|
||||
|
||||
**Prüfen:**
|
||||
```bash
|
||||
ss -tlnp | grep -E ':80 |:443 ' # Ports frei?
|
||||
nginx -t # Config ok?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schritt 2: Domain-Verzeichnisse & Platzhalter-Seiten
|
||||
|
||||
```bash
|
||||
# Für jede Domain:
|
||||
mkdir -p /var/www/example.de/{html,logs}
|
||||
chown -R www-data:www-data /var/www/example.de
|
||||
chmod -R 755 /var/www/
|
||||
```
|
||||
|
||||
> ⚠️ **Pitfall: 403 Forbidden** – Ohne `chown www-data:www-data` kann nginx die Dateien nicht lesen. Sofort nach `mkdir` ausführen!
|
||||
>
|
||||
> ⚠️ **Pitfall: `write_file` erstellt root-only Dateien!** Hermes' write_file-Tool legt Dateien mit `root:root` und `600`-Permissions an. nginx (www-data) bekommt 403. **Nach JEDEM write_file auf /var/www/ sofort:** `chown www-data:www-data <datei> && chmod 644 <datei>`
|
||||
|
||||
---
|
||||
|
||||
## Schritt 3: nginx Virtual Hosts (Name-based)
|
||||
|
||||
Pro Domain eine Config in `/etc/nginx/sites-available/`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name example.de www.example.de *.example.de;
|
||||
|
||||
root /var/www/example.de/html;
|
||||
index index.html index.htm;
|
||||
|
||||
access_log /var/www/example.de/logs/access.log;
|
||||
error_log /var/www/example.de/logs/error.log;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ **Pitfall: Wildcard-Subdomains** – `*.example.de` in `server_name` nicht vergessen, sonst landen Subdomains auf der nginx-Defaultseite!
|
||||
|
||||
Aktivieren:
|
||||
```bash
|
||||
ln -sf /etc/nginx/sites-available/example.de /etc/nginx/sites-enabled/
|
||||
nginx -t && systemctl reload nginx
|
||||
```
|
||||
|
||||
**Lokal testen (vor DNS-Umstellung):**
|
||||
```bash
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}\n" -H "Host: example.de" http://localhost/
|
||||
curl -s -H "Host: example.de" http://localhost/ | grep -oP '<title>\K[^<]+'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schritt 4: Cloudflare DNS per API verwalten
|
||||
|
||||
### Zonen auflisten
|
||||
```bash
|
||||
curl -s "https://api.cloudflare.com/client/v4/zones" \
|
||||
-H "Authorization: Bearer $CF_TOKEN" \
|
||||
-H "Content-Type: application/json" | python3 -c "
|
||||
import json, sys
|
||||
for z in json.load(sys.stdin)['result']:
|
||||
print(f\"{z['name']:35s} | {z['id']}\")
|
||||
"
|
||||
```
|
||||
|
||||
### Neue Zone anlegen
|
||||
```bash
|
||||
curl -s -X POST "https://api.cloudflare.com/client/v4/zones" \
|
||||
-H "Authorization: Bearer $CF_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"example.de","account":{"id":"ACCOUNT_ID"},"jump_start":false,"type":"full"}'
|
||||
```
|
||||
|
||||
### DNS-Einträge abrufen
|
||||
```bash
|
||||
curl -s "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
|
||||
-H "Authorization: Bearer $CF_TOKEN" | python3 -c "
|
||||
import json, sys
|
||||
for r in json.load(sys.stdin)['result']:
|
||||
print(f\"{r['type']:6s} {r['name']:40s} → {r['content']:25s} proxied={r['proxied']}\")
|
||||
"
|
||||
```
|
||||
|
||||
### A-Record setzen/aktualisieren (NEU)
|
||||
```bash
|
||||
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
|
||||
-H "Authorization: Bearer $CF_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"type":"A","name":"example.de","content":"SERVER_IP","proxied":false,"ttl":1}'
|
||||
```
|
||||
|
||||
### A-Record aktualisieren (BESTEHEND)
|
||||
```bash
|
||||
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
|
||||
-H "Authorization: Bearer $CF_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"content":"SERVER_IP","proxied":false,"type":"A","name":"example.de","ttl":1}'
|
||||
```
|
||||
|
||||
> ⚠️ **Pitfall: Cloudflare-Proxy (orangene Wolke)** – `"proxied":true` versteckt die echte Server-IP hinter Cloudflare. Das bricht:
|
||||
> - **Let's Encrypt HTTP-Challenges** (Zertifikat kann nicht validiert werden)
|
||||
> - **Direkte Server-Erreichbarkeit** für nicht-HTTP-Dienste
|
||||
> - **Mail-Server** (niemals MX-Records proxien!)
|
||||
>
|
||||
> **Regel:** Für Webserver, die Let's Encrypt nutzen: `"proxied":false`. Nach SSL-Erhalt kann Proxy optional wieder aktiviert werden (dann aber TLS-Mode auf "Full (strict)").
|
||||
|
||||
---
|
||||
|
||||
## Schritt 5: Let's Encrypt SSL
|
||||
|
||||
### Alle Domains gleichzeitig (ein Zertifikat)
|
||||
```bash
|
||||
certbot --nginx \
|
||||
-d domain1.de,www.domain1.de \
|
||||
-d domain2.de,www.domain2.de \
|
||||
--non-interactive --agree-tos \
|
||||
--email admin@domain1.de \
|
||||
--redirect
|
||||
```
|
||||
|
||||
> ⚠️ **Pitfall: CNAME/Proxy blockiert Challenge** – Wenn certbot mit `unauthorized` fehlschlägt:
|
||||
> 1. Prüfen ob CNAME-Records `proxied=true` haben → auf `false` setzen
|
||||
> 2. Sicherstellen dass ALLE Domains im Zertifikat auf den Server zeigen
|
||||
> 3. `dig +short A www.example.de` muss die Server-IP liefern, NICHT Cloudflare/GitHub-IPs
|
||||
|
||||
**Auto-Renewal prüfen:**
|
||||
```bash
|
||||
systemctl status certbot.timer
|
||||
certbot renew --dry-run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Schritt 6: MX-Records bei DNS-Migration schützen
|
||||
|
||||
**Goldene Regel:** MX-Records **niemals verändern**, nur **1:1 spiegeln**.
|
||||
|
||||
### Vor der Migration: Aktuelle MX-Daten sichern
|
||||
```bash
|
||||
dig +short MX example.de # MX-Records
|
||||
dig +short A mail.example.de # Mailserver-IP
|
||||
```
|
||||
|
||||
### In Cloudflare exakt spiegeln
|
||||
```bash
|
||||
# Mailserver A-Record (WICHTIG: proxied=false!)
|
||||
curl -s -X POST ".../dns_records" -d '{
|
||||
"type":"A","name":"mail.example.de",
|
||||
"content":"MAIL_SERVER_IP","proxied":false,"ttl":1
|
||||
}'
|
||||
|
||||
# MX-Record (darf NIE geproxied werden)
|
||||
curl -s -X POST ".../dns_records" -d '{
|
||||
"type":"MX","name":"example.de",
|
||||
"content":"mail.example.de","priority":10,
|
||||
"proxied":false,"ttl":1
|
||||
}'
|
||||
```
|
||||
|
||||
> ⚠️ **Pitfall: Mail-Ausfall** – MX-Records auch nur kurzzeitig zu verlieren bedeutet E-Mail-Bounce. Immer VOR dem Nameserver-Umzug sicherstellen, dass MX + Mail-A-Records in der neuen Zone existieren.
|
||||
|
||||
---
|
||||
|
||||
## Nameserver-Umzug: Schritt-für-Schritt
|
||||
|
||||
1. Zone bei Cloudflare anlegen
|
||||
2. **ALLE bestehenden DNS-Einträge spiegeln** (A, AAAA, MX, CNAME, TXT, SRV)
|
||||
3. MX & Mail-A-Records doppelt prüfen
|
||||
4. Cloudflare-Nameserver notieren (z.B. `jay.ns.cloudflare.com`, `perla.ns.cloudflare.com`)
|
||||
5. Beim alten Provider Nameserver ändern
|
||||
6. 1–48h auf Propagation warten
|
||||
7. SSL per certbot einrichten
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### 403 Forbidden nach VHost-Erstellung
|
||||
```bash
|
||||
chown -R www-data:www-data /var/www/
|
||||
chmod -R 755 /var/www/
|
||||
```
|
||||
|
||||
### Wildcard-Subdomains zeigen Default-Seite
|
||||
`server_name` muss `*.example.de` enthalten. Prüfen:
|
||||
```bash
|
||||
grep server_name /etc/nginx/sites-enabled/example.de
|
||||
curl -sI -H "Host: test.example.de" http://localhost/
|
||||
```
|
||||
|
||||
### certbot: unauthorized / Invalid response
|
||||
- Alle Domains im Zertifikat auf `proxied=false` prüfen
|
||||
- `www`-CNAME darf nicht auf GitHub Pages o.ä. zeigen
|
||||
- Sicherstellen dass ALLE Domains auf die Server-IP auflösen
|
||||
|
||||
### ⚠️ PITFALL – Certbot überschreibt SSL-Pfade in BESTEHENDEN vhosts
|
||||
|
||||
Siehe `nginx-vhost-deployment/SKILL.md` → **PITFALL – Certbot überschreibt SSL-Pfade in BESTEHENDEN vhosts**.
|
||||
|
||||
Kurzfassung: Nach jedem `certbot --nginx -d NEUE.DOMAIN.de` prüfen ob alle bestehenden Domains
|
||||
noch ihr RICHTIGES Zertifikat haben. Certbot ändert ggf. `ssl_certificate`-Pfade in anderen
|
||||
vhost-Dateien. Test: `curl -sk https://alte-domain.de/` → HTTP 200? Wenn nicht: SSL-Pfade
|
||||
in der betroffenen Config zurücksetzen.
|
||||
|
||||
### ERR_CERT_COMMON_NAME_INVALID: vhost nutzt fremdes Zertifikat (2026-08-04)
|
||||
|
||||
**Symptom:** `curl https://<sub>...` ohne `-k` schlägt fehl; Browser meldet `ERR_CERT_COMMON_NAME_INVALID`, auch wenn eine frühere `curl -k`-Probe 200 lieferte. Grund: Der Subdomain-vhost referenziert ein **anderes** Zertifikat, in dessen SANs der eigene Hostname **fehlt**.
|
||||
|
||||
**Diagnose (immer zuerst, dauert 2 Sekunden):**
|
||||
```bash
|
||||
# Welches Zert referenziert der vhost?
|
||||
grep -iE "ssl_certificate" /etc/nginx/sites-available/<sub>.perfekt-ohne-en.de
|
||||
# Welche Hostnames deckt DIESES Zert ab?
|
||||
openssl x509 -in /etc/letsencrypt/live/<cert>/fullchain.pem -noout -text \
|
||||
| grep -A1 "Subject Alternative Name"
|
||||
```
|
||||
→ `tictactoe.perfekt-ohne-en.de` war auf das `perfekt-ohne-en.de`-Zert gelinkt (San der `fitness.`/`clownfischserver.` etc.), `tictactoe.` fehlte → Browser-Warnung.
|
||||
|
||||
**Fix: eigenes Zertifikat NUR für die Subdomain anlegen** (statt `--expand` aufs alte Zert):
|
||||
```bash
|
||||
certbot --nginx -d <sub>.perfekt-ohne-en.de \
|
||||
--non-interactive --agree-tos -m <mail> --redirect
|
||||
```
|
||||
Erzeugt `/etc/letsencrypt/live/<sub>.perfekt-ohne-en.de/`, deployt automatisch in den vhost, Auto-Renew läuft. Danach ohne `-k` verifizieren:
|
||||
```bash
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}\n" https://<sub>.perfekt-ohne-en.de/
|
||||
openssl x509 -in /etc/letsencrypt/live/<sub>.perfekt-ohne-en.de/fullchain.pem -noout -text \
|
||||
| grep -A1 "Alternative"
|
||||
```
|
||||
|
||||
> ⚠️ **Warum NICHT `certbot --expand` aufs alte Zert?** Das alte Zert trägt ggf. Hostnames inaktiver/archivierter vhosts (z.B. eingestampfte Sites). `--expand` muss alle aufgelisteten Domains erneut validieren — scheitert leicht an deaktivierten vhosts und "budelt" in Sachen, die ruhen sollen. Ein frisches, schlankes Subdomain-Zert ist sauberer und kapselt die Domain. Faustregel: **ein Ziel-Hostname → eigenes Zert; nur verwandte aktive Hostnames in ein Zert bündeln.**
|
||||
|
||||
### Cloudflare API: Invalid API Token
|
||||
- Token ist ggf. Zonen-beschränkt → kann keine neuen Zonen erstellen
|
||||
- `/user/tokens/verify` funktioniert nur mit Account-weitem Token
|
||||
- Zonen-Operationen (DNS) funktionieren trotzdem mit Zonen-Token
|
||||
|
||||
### Neue Subdomain: certbot --nginx scheitert, wenn SSL-vhost schon auf fehlendes Zert zeigt
|
||||
|
||||
**Situation:** Für eine neue Subdomain legt man einen vhost an und schreibt die
|
||||
`ssl_certificate`-Zeilen **sofort** (aus Vorlage) hinein. `nginx -t` schlägt dann
|
||||
fehl: `cannot load certificate "…/live/<sub>/fullchain.pem": No such file` — und
|
||||
weil `nginx -t` bricht, wirft **`certbot --nginx` denselben Config-Fehler** (MisconfigurationError), bekommt kein Zert und es geht nicht weiter.
|
||||
|
||||
**Lösung (Reihenfolge zählt):**
|
||||
1. vhost-Backup: `cp sites-available/<sub>.… sites-available/<sub>.…bak`
|
||||
2. vhost auf **reines HTTP-80** ohne jede `ssl_certificate`-Zeile ersetzen →
|
||||
`nginx -t` wieder ok → `systemctl reload nginx`
|
||||
3. Zert **per webroot-Challenge** holen (KEIN `--nginx`-Plugin, das braucht eine
|
||||
ladende Config):
|
||||
```bash
|
||||
certbot certonly --webroot -w /var/www/<sub> -d <sub>.perfekt-ohne-en.de \
|
||||
--non-interactive --agree-tos --register-unsafely-without-email
|
||||
```
|
||||
4. SSL-vhost (aus dem `.bak`) zurückspielen → `nginx -t` + reload
|
||||
5. Mit `curl -s -o /dev/null -w "%{http_code}" https://<sub>/` verifizieren
|
||||
(HTTP 000 + Exit 60 = nur curl-Zert-Warnung, kein Serverfehler; `-k` zeigt dann 200).
|
||||
|
||||
### Statische datengetriebene Website mit PDF-Download je Eintrag
|
||||
|
||||
Für JSON-datengetriebene Listen-Seiten mit per-Eintrag-PDF (wkhtmltopdf) →
|
||||
**`references/static-data-site-pdf.md`** (Datei-Struktur, Slug-Helper, PDF-Generator-Skript, Verifikation).
|
||||
|
||||
### Mehrstufige (Drill-Down) datengetriebene Website: Flask + SQLite + Reverse-Proxy
|
||||
|
||||
Für Sites mit verschachtelter Navigation über mehrere Ebenen (URL `/2/<stadt>`,
|
||||
Klick auf ein Ziel erzeugt die nächste Liste) → **nicht statisch bauen**, sondern
|
||||
**Flask + SQLite hinter nginx-Reverse-Proxy** (sonst ~1000–2000 statische Dateien).
|
||||
Deckung: systemd-Service, Slug↔Umlaut-Match, PDF-on-demand (**je Einzel-Verbindung
|
||||
`von→nach`, nicht Liste** — Nutzererwartung 2026-08-09), `/healthz`. Details →
|
||||
**`references/flask-sqlite-drilldown-site.md`**.
|
||||
|
||||
### Docker-Interne-Dienste deployen (Gitea, Vaultwarden)
|
||||
|
||||
Für Docker-basierte Web-Apps (Gitea, Vaultwarden, Passwort-Manager, Git-Server)
|
||||
hinter nginx-Reverse-Proxy mit Let's Encrypt SSL:
|
||||
**`references/docker-internal-services.md`** — Deployment-Workflow, Setup-Schritte,
|
||||
Admin-Token-Sicherung, Registrierung öffnen/schließen, Mirror auf zweitem Server.
|
||||
|
||||
### Echte Fahrplan-/Zugdaten automatisieren: DB blockt Bots
|
||||
|
||||
Für Reise-/Verbindungs-Sites mit realen deutschen Nahverkehrs-Zeiten: bahn.de +
|
||||
DB-API blocken den Server-Zugriff hart (Bot-Erkennung Fehler 753, `OPS_BLOCKED`),
|
||||
VRR-EFA deckt nur den eigenen Verbund, andere GTFS-Endpunkte waren unerreichbar.
|
||||
Funktionierende Wege: regionale EFA, Nutzer liefert echte Zeiten manuell, oder
|
||||
klar-labelierte Schätzwerte ("vor Fahrt auf bahn.de prüfen"). Nie erfundene Zeiten
|
||||
als real ausgeben. Details → **`references/deutsche-bahn-timetable-data.md`**.
|
||||
|
||||
### Subdomain mit Wildcard-DNS schnell deployen
|
||||
|
||||
Wenn die Hauptdomain bereits per Wildcard (`*.example.de`) auf den Server zeigt, braucht eine neue Subdomain KEINE DNS-Änderung:
|
||||
|
||||
```bash
|
||||
# 1. Verzeichnis + nginx-Config
|
||||
mkdir -p /var/www/sub.example.de/{html,logs}
|
||||
chown -R www-data:www-data /var/www/sub.example.de
|
||||
|
||||
cat > /etc/nginx/sites-available/sub.example.de << 'NGINX'
|
||||
server {
|
||||
server_name sub.example.de;
|
||||
root /var/www/sub.example.de/html;
|
||||
index index.html;
|
||||
access_log /var/www/sub.example.de/logs/access.log;
|
||||
error_log /var/www/sub.example.de/logs/error.log;
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
listen 80; listen [::]:80;
|
||||
}
|
||||
NGINX
|
||||
|
||||
ln -sf /etc/nginx/sites-available/sub.example.de /etc/nginx/sites-enabled/
|
||||
nginx -t && systemctl reload nginx
|
||||
|
||||
# 2. HTML erstellen + Permissions fixen (CRITICAL!)
|
||||
# ...write_file oder echo in index.html...
|
||||
|
||||
chown www-data:www-data /var/www/sub.example.de/html/index.html
|
||||
chmod 644 /var/www/sub.example.de/html/index.html
|
||||
|
||||
# 3. SSL
|
||||
certbot --nginx -d sub.example.de --non-interactive --agree-tos \
|
||||
--email admin@example.de --redirect
|
||||
```
|
||||
|
||||
Lokal testen: `curl -s -H "Host: sub.example.de" http://localhost/ | grep '<title>'`
|
||||
|
||||
### Multi-Site-Pattern: Verlinkte Websites mit Datenübergabe
|
||||
|
||||
Für mehrere zusammenhängende Single-Page-Apps auf Subdomains (z.B. Fitness-Rechner + Kalorienrechner) mit Cross-Linking, URL-Parameter-Datenübergabe, PDF-Export und Sponsor-Badge → **`references/multi-site-pattern.md`**
|
||||
|
||||
### Site einstampfen / deaktivieren / archivieren (2026-08-04)
|
||||
|
||||
Nutzer-Anweisung „einstampfen, ins Archiv, ruhen lassen" → **deaktivieren, nicht zerstören** (nicht-destruktive Grundregel):
|
||||
|
||||
```bash
|
||||
# 1. nginx vhost deaktivieren (Symlink raus; available bleibt als Sicherung)
|
||||
rm /etc/nginx/sites-enabled/<domain>
|
||||
nginx -t && systemctl reload nginx
|
||||
|
||||
# 2. Code ins Archiv verschieben (nicht löschen!)
|
||||
mkdir -p /srv/work/archiv/<projekt>
|
||||
mv /root/<projekt> /srv/work/archiv/<projekt>/repo
|
||||
mv /root/<projekt>_extracted /srv/work/archiv/<projekt>/extracted # falls vorhanden
|
||||
|
||||
# 3. LIESMICH.md mit Repo-Link + Reaktivierungsweg ablegen
|
||||
# (Status, GitHub-URL, wie wieder aktivieren: Symlink + Code zurück)
|
||||
```
|
||||
|
||||
- **Keinen Symlink/Code löschen** — nur deaktivieren + ins Archiv legen. Reaktivierung = Symlink zurück + Code zurück.
|
||||
- **GitHub-Repo bleibt unangetastet** (extern); nur lokale Deployment-Artefakte werden archiviert. Repo-Link in `LIESMICH.md` festhalten.
|
||||
- Nach Abschluss: `mesh-log.sh`-Eintrag + **Memory aktualisieren** (Projektstatus ändert sich von „läuft" zu „eingestampft/archiviert") — sonst halluziniert ein späterer Session-Start fälschlich „läuft noch stabil".
|
||||
|
||||
### Autogenerierte Report-/Archiv-Websites (Cron-regeneriertes HTML, 2026-08-03)
|
||||
|
||||
Wenn eine Subdomain **automatisch generierte Inhalte** zeigt (Live-Panel, Tages-Archive), die ein Script periodisch neu schreibt:
|
||||
|
||||
- **Kein PHP nötig.** Ein Shell-Script (z.B. `duo-master-report.sh`) baut die komplette `index.html` mit einem Heredoc zusammen und läuft per Cron (z.B. alle 60s für Live-Panel, täglich 23:50 für Tagesarchiv).
|
||||
- Struktur: `Live-Panel` (dokumentationen.*) zeigt aktuelle Zustände; `Archiv` (archiv.*) speichert pro Datum eine HTML-Seite `JJJJ-MM-TT.html` PLUS eine `index.html` mit Linkliste auf alle Archive.
|
||||
- Nach jedem Write: `chown www-data:www-data <webroot>` — sonst 403 (Hermes write_file erzeugt root:root).
|
||||
- Daily-Archiv-Name = ISO-Datum (`$(date +%Y-%m-%d).html`), Linkliste wird aus vorhandenen `.html`-Dateien (ausgenommen `index.html`) aufgebaut.
|
||||
- ⚠️ **Bash-Pitfall unter `set -euo pipefail`:** `ls /pfad/*.json | wc -l` bricht mit Exit 2 ab, wenn keine Dateien matchen (leerer Glob). Vorher `shopt -s nullglob` setzen (und danach `shopt -u nullglob`), sonst endet das gesamte Skript scheinbar ohne Fehler. Gleiches gilt für `ls "$WEB"/*.html` in der Linkliste.
|
||||
Reference in New Issue
Block a user