Files
Rotatory/.vch/PLATFORM.md
T
ferdi2goandClaude Fable 5.1 95756778d8 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>
2026-09-16 06:39:37 +00:00

327 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.
# 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.