Rotatory anlegen: Morning-Meeting-Moderation auslosen und nach Nextcloud schreiben

Express-App mit JSON-Speicher unter VCH_DATA_DIR, Monatskalender mit Fotos,
Personen mit Rollen und Teilzeit-Freitagen, Abwesenheiten, Feiertage je
Bundesland, faire Zufallsauslosung mit Fixieren und Neu-Auslosen.

Nextcloud: CalDAV-Discovery, Zielkalender wählbar, Termine je Werktag
schreiben und aktualisieren, Abwesenheiten aus Kalender importieren,
Personen samt Fotos aus dem Systemadressbuch übernehmen.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
ferdi2goandClaude Fable 5.1 committed 2026-09-16 06:39:37 +00:00
commit 95756778d8
26 files changed
+4697

No files matched your search

+326
View File
@@ -0,0 +1,326 @@
# VCH-Plattform — die Regeln dieser Umgebung
Diese Datei gehört VCH und wird bei jedem Abgleich **überschrieben**. Projektwissen gehört
in die `CLAUDE.md` daneben, nicht hierher.
Sie beschreibt, was VCH mit diesem Projekt macht — vor allem die Stellen, an denen ein
Verstoß **nicht als Fehler auffällt, sondern als falsches Ergebnis**: ein Deploy, der grün
meldet und nichts ausliefert, eine Datenbank, die niemand verbindet, ein Audit, das etwas
prüft, das du gar nicht gepusht hast.
## 1. Zwei Maschinen, nicht eine
- **Hier (DEV):** ein Debian-LXC. Nutzer `vchuser`, Heimat `/home/vchuser`, Projekte unter
`/home/vchuser/projects/<name>`. code-server auf Port 8080, Claude Code als `claude`,
Git gegen Gitea vorkonfiguriert.
- **Dort (PROD):** eine **andere** Maschine. VCH klont dein Repository dorthin und startet es.
Daraus folgt die wichtigste Regel überhaupt: **VCH deployt und auditiert deinen gepushten
Commit, nie dein Arbeitsverzeichnis.** Nicht committete Änderungen existieren für Deploy,
Audit und Veröffentlichung nicht — auch dann nicht, wenn hier alles läuft.
## 2. Vorschau im Browser läuft über einen Proxy
Deine Entwicklungs-Server sind nicht direkt erreichbar. VCH erkennt lauschende Ports und
stellt zwei Wege bereit:
1. **Subdomain** (bevorzugt): `https://lxc<VMID>-<PORT>.<domain>/` — deine App liegt auf der
Wurzel, alles verhält sich normal.
2. **Pfad-Proxy** (Rückfallweg): `https://<host>/proxy/<instanz>/p/<port>/` — deine App liegt
hinter einem Pfadpräfix.
Damit beides funktioniert: **relative Pfade verwenden.**
- `css/style.css` ✅ — nicht `/css/style.css` ❌
- `fetch('api/daten')` ✅ — nicht `fetch('/api/daten')` ❌
- Wenn absolute Pfade unvermeidbar sind: `<base href="./">` in den `<head>`.
Rahmenwerke: **Vite** `base: './'` · **Create React App** `"homepage": "."` in der
`package.json` · **Next.js** Vorgabe belassen (Subdomain) oder `basePath` setzen (Pfad-Proxy)
· **Express** `express.static` genügt, solange das HTML relativ verweist.
## 3. Dateien, die VCH im Projektstamm liest
| Datei | Pflicht | Wozu |
|---|---|---|
| `README.md` | ja | Audit-Prüfung `readme` — blockiert die Veröffentlichung |
| `.vch-description` | ja | Audit-Prüfung `description`; der Text steht später im Showcase |
| `.env.example` | wenn die App Umgebungsvariablen liest | die **Deklaration** aller Variablen (Abschnitt 6) |
| `.vch/deploy.yaml` | wenn die Autoerkennung nicht reicht | wie gebaut und gestartet wird (Abschnitt 5) |
| `CLAUDE.md` | — | dein Projektwissen; VCH fasst sie nicht mehr an |
`README.md` muss beschreiben, **was** das Projekt tut, wie man es installiert, wie man es
startet und worauf es aufbaut. Die unveränderte Vorlage besteht die Prüfung nicht.
`.vch-description` ist **nicht technisch**: was das Projekt tut, welches Problem es löst,
für wen. Kein Technologie-Stapel, keine Installationsanleitung — die stehen im README.
Halte beide aktuell, wenn sich Zweck oder Aufbau ändern.
## 4. Der Port gehört VCH
VCH weist jedem Projekt auf der PROD-Maschine einen festen Port zu (Bereich `13000–13999`)
und hält ihn über alle Deploys hinweg stabil. Du wählst ihn nicht und stimmst dich mit
niemandem ab.
**Deine App MUSS auf `PORT` aus der Umgebung lauschen:**
- Node/Express: `app.listen(process.env.PORT || 3000)`
- Next.js: `next start` beachtet `PORT` von selbst
- Python: `gunicorn app:app -b 0.0.0.0:$PORT` bzw. `uvicorn main:app --port $PORT`
- Go/andere: den Wert aus der Umgebung lesen, nicht hartkodieren
Ein fest verdrahteter Port läuft hier und scheitert dort am Healthcheck. `PORT` und
`VCH_DATA_DIR` sind **VCH-eigene** Schlüssel: setzt du sie selbst — in `.env.example`, im
Env-Editor oder im Compose — wird dein Wert verworfen.
## 5. `.vch/deploy.yaml` — die vollständige Referenz
Ohne diese Datei versucht VCH, das Projekt selbst zu erkennen (Abschnitt 5.3). Reicht das
nicht, bricht der Deploy ab **und legt dir eine fertige Vorlage vor**, die du nur noch
committen musst.
### 5.1 Alle Felder
| Feld | Typ | Vorgabe | Bedeutung |
|---|---|---|---|
| `runtime` | `auto` \| `systemd` \| `docker` | `auto` | Wie gestartet wird. `auto`: Dockerfile oder Compose vorhanden → `docker`, sonst `systemd`. |
| `build` | Zeichenkette | — | Shell-Befehl im Release-Verzeichnis. **Nur `systemd`** — bei `docker` baut das Dockerfile. |
| `start` | Zeichenkette | — | Startbefehl des Dienstes. **Nur `systemd`.** Muss `$PORT` binden. |
| `port` | Ganzzahl 1–65535 | `3000` | **Nur `docker`:** der Port, auf dem deine App **im Container** lauscht. VCH bildet den zugewiesenen Host-Port darauf ab. |
| `health` | Pfad | `/` | Pfad, der 2xx oder 3xx liefert, sobald die App bereit ist. |
| `env` | Liste von Zeichenketten | `[]` | Zusätzliche Variablennamen, die VCH ausliefern soll. Wird mit den Schlüsseln aus `.env.example` vereinigt. |
| `compose` | Pfad | — | Welche Compose-Datei gilt, wenn mehrere gefunden werden. |
Unbekannte Felder und Tippfehler in `runtime` brechen den Deploy mit einer benannten
Fehlermeldung ab — sie werden nicht still ignoriert.
### 5.2 Beispiele
Node mit eigenem Build (systemd):
```yaml
runtime: systemd
build: "npm ci && npm run build"
start: "npm start"
health: /
```
Python (systemd) — beachte `$PORT` im Startbefehl:
```yaml
runtime: systemd
build: "python3 -m venv .venv && .venv/bin/pip install -r requirements.txt"
start: ".venv/bin/gunicorn app:app -b 0.0.0.0:$PORT"
health: /
```
Statische Seite (systemd) — irgendetwas muss lauschen:
```yaml
runtime: systemd
build: "npm ci && npm run build"
start: "python3 -m http.server $PORT --directory dist"
```
Monorepo, Code in einem Unterordner:
```yaml
runtime: systemd
build: "cd backend && npm ci && npm run build"
start: "cd backend && npm start"
```
Dockerfile:
```yaml
runtime: docker
port: 3000 # der Port INNERHALB des Containers
health: /
```
Mehrere Compose-Dateien im Repository:
```yaml
runtime: docker
compose: docker/compose.yaml
```
### 5.3 Was ohne die Datei passiert
In dieser Reihenfolge:
1. Ein echtes `Dockerfile` gewinnt → `runtime: docker` mit dem Image.
2. Eine Compose-Datei, die `${VCH_PORT}` veröffentlicht → Compose-Deploy.
3. Eine Compose-Datei, die **nur Infrastruktur** startet (Postgres, Redis, … und keinen
App-Dienst), wird als Entwicklungs-Beiwerk erkannt und **nicht** deployt — VCH sucht
weiter nach der Sprache.
4. Eine Compose-Datei mit App-Dienst, die `${VCH_PORT}` **nicht** veröffentlicht → Abbruch
mit Vorlage (Abschnitt 10).
5. `package.json` im Stamm: `build`-Skript → Build, `start`-Skript bzw. `main` → Start.
Paketmanager wird am Lockfile erkannt (`pnpm`/`yarn`/`npm`).
6. Alles andere — Python, PHP/Laravel, Go, reines Frontend ohne `start`, Code nur in
Unterordnern — endet mit einer passenden Vorlage zum Committen.
## 6. Umgebungsvariablen: `.env.example` ist die Deklaration
VCH liest keine Umgebungsvariablen aus deinem Code. **Was in einer Beispieldatei steht,
existiert; alles andere nicht.**
- Erkannte Dateinamen: `.env.example`, `.env.sample`, `.env.template` sowie dieselben ohne
führenden Punkt (`env.example`, …). Der erste Fund je Schlüssel gewinnt.
- Gesucht wird **im ganzen Repository** (ohne `node_modules`, `.git`, `dist`, `build`,
`.next`, `out`, `vendor`, `coverage`, `.turbo`), Stammdateien zuerst, höchstens 25 Dateien.
- Jede gefundene Beispieldatei bestimmt ein Ziel: `backend/.env.example` → VCH schreibt die
Produktionswerte nach `backend/.env`. Für Monorepos ist das der Weg, mehrere `.env` zu
bekommen.
**Was den Deploy blockiert:** ein Schlüssel, der **keinen Wert nennt** (`KEY=`) und für den
noch kein Produktionswert hinterlegt ist. Nicht blockierend sind:
```dotenv
API_URL=https://beispiel.test # Vorgabewert vorhanden
OPTIONAL_TOKEN="" # ausdrücklich leer erlaubt
FEATURE_FLAG= # optional
```
Der Marker muss direkt hinter dem `#` stehen (`# optional`); Fließtext, in dem das Wort
vorkommt, zählt nicht. Die Vorgabewerte sind **Hinweise für den Menschen** — in Produktion
wird nichts davon verwendet.
Im Zweifel:
```bash
grep -RhoE "process\.env\.[A-Z_]+" src/ | sort -u
```
und jeden Treffer in die `.env.example` schreiben.
**Bei Docker:** nur Variablen mit dem Präfix `NEXT_PUBLIC_`, `VITE_` oder `PUBLIC_` gehen als
`--build-arg` in den Build (sie sind ohnehin im Auslieferungsstand sichtbar) — dafür brauchst
du passende `ARG`-Zeilen im Dockerfile. Alle übrigen Werte kommen erst zur Laufzeit über
`--env-file` und landen nie im Image.
## 7. Datenbank
Deklariert deine App Verbindungsvariablen, bietet VCH eine verwaltete Datenbank an und trägt
die Werte **automatisch** in genau die Schlüssel ein, die du deklariert hast. Du musst deine
Variablennamen also nicht ändern — VCH erkennt sie.
**Erkannt wird** (Groß-/Kleinschreibung egal):
- Verbindungszeichenketten: `DATABASE_URL`, `DB_URL`, jeder Name auf `_DSN`
- Einzelteile mit Familienwort (`DB`, `DATABASE`, `POSTGRES`, `MYSQL`) plus Rolle:
`..._HOST`, `..._PORT`, `..._USER`/`..._USERNAME`, `..._PASSWORD`/`..._PASS`,
`..._NAME`/`..._DATABASE`/`..._DB`
- Die üblichen nackten Namen: `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE`,
`MYSQL_HOST`, `MYSQL_USER`, `MYSQL_PASSWORD`, `MYSQL_DATABASE`, `POSTGRES_DB`
**Bewusst nicht erkannt** (und deshalb nie überschrieben):
- alles mit `ROOT` im Namen (`POSTGRES_ROOT_PASSWORD`, `MYSQL_ROOT_PASSWORD`) — das sind die
Zugangsdaten des Datenbankservers, nicht die deiner App
- `SENTRY_DSN` und verwandte Fehlermelder (`GLITCHTIP`, `BUGSNAG`, `ROLLBAR`, `AIRBRAKE`,
`RAYGUN`, `APPSIGNAL`) — ein DSN ist dort keine Datenbank
- `HISTORY_DB=/data/verlauf.sqlite` und Ähnliches: ein `..._DB` ohne Datenbank-Familienwort
im übrigen Namen gilt nicht als Verbindung
Die **Engine** (Postgres oder MySQL) leitet VCH aus deinen Schlüsselnamen und dem Schema in
`DATABASE_URL` ab; ist das mehrdeutig, entscheidest du im Projektarbeitsplatz.
**Bringt dein Compose seine eigene Datenbank mit**, startet VCH sie mit und bietet **keine**
zweite an. Achte dann selbst auf ein dauerhaftes Volume — ein Datenbankdienst ohne Volume
verliert seine Daten beim nächsten Neubau, und VCH warnt nur.
Eine Compose-Datei, die **nur** eine Datenbank startet, ist Entwicklungs-Beiwerk: sie läuft in
Produktion nicht mit. Nimm dort die verwaltete Datenbank.
## 8. Dauerhafte Daten: `VCH_DATA_DIR`
Jedes Release liegt in einem eigenen Verzeichnis und verschwindet wieder. Was überleben soll —
SQLite-Datei, Uploads, erzeugte Dateien — gehört in das Verzeichnis aus **`VCH_DATA_DIR`**.
```js
const datenVerzeichnis = process.env.VCH_DATA_DIR || './data';
```
VCH setzt die Variable bei allen Betriebsarten (bei Docker als Einhängepunkt `/data`). **Lies
sie zur Laufzeit**, schreibe den Pfad nirgends fest und trage sie nicht in `.env`-Dateien ein.
Ein Verzeichnis relativ zum Release (`./data`) ist beim nächsten Deploy weg.
Der Prüfling während des Audits (Abschnitt 9) bekommt diesen Speicher **nicht** — er läuft
gegen einen leeren. Das ist Absicht: zwei Prozesse dürfen nicht gleichzeitig auf dieselbe
SQLite-Datei schreiben.
## 9. Wie ein Deploy abläuft
```
Klonen → Bauen → Healthcheck (Prüfling) → Audit → Umschalten → Live
```
1. **Klonen.** VCH klont den Deploy-Branch auf die PROD-Maschine — genau den gepushten Stand.
2. **Torprüfung.** Fehlt ein blockierender Wert aus `.env.example` (Abschnitt 6), endet es hier,
bevor irgendetwas läuft.
3. **Bauen** nach `.vch/deploy.yaml` bzw. Autoerkennung.
4. **Healthcheck.** Der neue Stand startet als **Prüfling** auf einem freien Port aus dem
Bereich `14000–14999`, neben der weiterhin laufenden alten Fassung. VCH ruft dort deinen
`health`-Pfad auf: rund 30 Sekunden lang, alle 1,5 s ein Versuch, Erfolg beim ersten
2xx/3xx. Bis dahin muss die App antworten.
5. **Audit** (Abschnitt 10) gegen den deployten Commit.
6. **Umschalten.** Erst wenn das Audit besteht, wird der Prüfling zur laufenden Fassung auf
dem stabilen Port. **Ein durchgefallenes Audit heißt: der alte Stand bleibt live.**
Vor dem Umschalten legt VCH einen Datenschnappschuss an. Geht das Umschalten schief, werden
erst die Daten und dann der Code zurückgerollt. Frühere Releases lassen sich im
Projektarbeitsplatz per Rollback zurückholen.
**Auto-Deploy:** ist er eingeschaltet, löst jeder Push auf den konfigurierten Deploy-Branch
einen Deploy aus. Pushes auf andere Branches werden ignoriert.
## 10. Audit — was geprüft wird
Das Audit läuft nicht hier, sondern in einem eigenen Runner gegen einen **sauberen Checkout
des deployten Commits**. Nicht Gepushtes wird nicht geprüft — und ein `.gitignore`-Eintrag
macht eine Datei für das Audit unsichtbar.
| Prüfung | Schwere | Darf entfallen |
|---|---|---|
| `readme` | Fehler | nein |
| `secrets` | Fehler | nein |
| `description` | Warnung | nein |
| `lint` | Fehler | ja |
| `typescript` | Fehler | ja |
| `npm-audit` | Fehler | ja |
- Eine **Warnung** blockiert nie. `description` hält dich also nicht auf, kostet dich aber den
Platz im Showcase.
- „Darf entfallen" heißt: die Prüfung meldet `skipped`, wenn sie auf dein Projekt nicht passt
(kein TypeScript, kein npm). Eine Prüfung, die **nicht** entfallen darf, aber trotzdem
`skipped` meldet, gilt als **durchgefallen** — sie ist nie gelaufen.
- `secrets` sucht eingecheckte Zugangsdaten. Echte Werte gehören nie ins Repository: in die
`.env.example` nur Beispiele, die echten Werte in den Env-Editor von VCH.
## 11. Veröffentlichen
- **Intern erreichbar** ist dein Projekt, sobald ein Deploy läuft — ohne Audit, ohne weiteren
Schritt, unter `http://<prod-ip>:<port>`.
- **Öffentlich** unter einer Subdomain mit SSL wird es erst mit laufendem Deploy **und**
bestandenem Audit für genau diesen Deploy. Die Subdomain kommt oben drauf; entfernst du sie
wieder, bleibt das Projekt intern erreichbar.
- Im **Showcase** erscheinen veröffentlichte Projekte mit bestandenem Audit und einer
eingecheckten `.vch-description`.
## 12. Compose — die vier Regeln, an denen es sonst scheitert
1. **Der App-Dienst muss seinen Port als `ports: ["${VCH_PORT}:<intern>"]` veröffentlichen.**
VCH setzt `VCH_PORT` und kann so einen Prüfling neben deiner laufenden Fassung starten.
Ohne diese Zeile bricht der Deploy ab.
2. **VCH führt genau EINE Compose-Datei aus — keine `-f`-Ketten.** Gesucht wird
`compose.yaml`/`compose.yml`/`docker-compose.yaml`/`docker-compose.yml` im Stamm **oder in
einem Unterordner**. Eine Override-Datei (`docker-compose.ports.yml`, `*.override.yml`)
wird **ignoriert** — die `${VCH_PORT}`-Zeile gehört in die Hauptdatei. Bei mehreren
Fundstellen: `compose: <pfad>` in die `.vch/deploy.yaml`.
3. **Jeder Dienst, der Produktionswerte braucht, bekommt `env_file: [.env]`.** VCH schreibt
deine Werte in eine `.env` neben der Compose-Datei.
4. **Der ganze Stapel startet.** Datenbanken, Caches und Ähnliches aus deinem Compose laufen
automatisch mit — eine zusätzliche verwaltete Datenbank brauchst du dafür nicht. Dienste
hinter einem `profiles:`-Schlüssel bleiben aus.