# Deploy — Mittwald (klassischer Kundencenter-Weg, ohne mStudio)

Vollständige Anleitung: [`docs/hosting-mittwald.md`](../docs/hosting-mittwald.md).

## ⛔ Regel Nr. 1 — beide Prozesse: `stop` → `start`, niemals `reload`

Der Server lauscht auf einem **Unix-Socket**. `pm2 reload` startet den neuen Prozess, *bevor* es den
alten beendet → der neue kann nicht binden → `EADDRINUSE` → **der neue stirbt, der alte läuft mit
altem Code weiter.** pm2 meldet dabei `online` und zählt nur den Restart-Zähler hoch: es sieht wie ein
erfolgreicher Deploy aus und ist keiner.

```bash
PM2=/files/.mwsoftware/node/lib/node_modules/pm2/bin/pm2
$PM2 stop voco-server
ls -la /tmp/.mwservice/*.sock          # MUSS "No such file" sagen (sonst Fremdprozess -> mwservicectl restart)
$PM2 start voco-server

$PM2 stop voco-worker
node deploy/reste-aufraeumen.mjs
$PM2 start voco-worker
```

**Erfolg prüft man nie mit `pm2 list`**, sondern von außen gegen eine Route, die es vor diesem Deploy
nicht gab — beim Worker über die Logzeile `registered worker`:

```bash
curl -s -o /dev/null -w "%{http_code}\n" https://vocobackend.wfk-online.de/monitor
```

## Kurzfassung
```bash
# auf dem Space (SSH), im App-Ordner:
corepack enable && corepack prepare pnpm@9.15.9 --activate
pnpm install
pnpm --filter @adva/agent build          # -> dist/server/http.mjs + dist/voice/worker.mjs (kein tsx!)

# Secrets:
$EDITOR packages/agent/.env               # LIVEKIT_*, XAI_API_KEY, ANTHROPIC_API_KEY, ...

# Service(s):
#   Server:  deploy/mwservice.server.yaml  -> packages/agent/.mwservice.yaml
#   Worker:  deploy/mwservice.worker.yaml  -> eigenes Service-Verzeichnis  ODER
#   beides:  pm2 start deploy/ecosystem.config.cjs --no-daemon
mwservicectl enable && mwservicectl start
```
Danach im Kundencenter die Subdomain aufs App-Verzeichnis zeigen lassen (HTTPS automatisch) und
`ADVA_BASE_URL` der iOS-App auf `https://<subdomain>` setzen.

## Upload junk-sicher (macOS → Server)
`rsync`/`curl` gehen auf dem Space nicht → immer **tar + scp + ssh-extract**. macOS legt sonst
AppleDouble-Twins (`._datei`) und `.DS_Store` ins Paket, die der Server als echten Inhalt einliest
(passiert mit den Leitlinien/Prompts → Binär-Müll in `/leitung/leitlinien` + `/leitung/prompts`).
Daher **immer** so packen:
```bash
COPYFILE_DISABLE=1 tar --exclude='._*' --exclude='.DS_Store' -czf /tmp/paket.tgz <pfade>
```
`COPYFILE_DISABLE=1` verhindert die Twins schon beim Packen (macOS-`tar`); die `--exclude` fangen
Altbestände ab. Optional dauerhaft: `export COPYFILE_DISABLE=1` in `~/.zshrc`.

Zusätzlicher Schutz im Backend: die Verzeichnis-Scanner (`ladeLeitlinien`, `ueberschreibbarePrompts`)
ignorieren Dotfiles — selbst wenn doch Müll landet, erscheint er nicht mehr als Inhalt. Aufräumen
falls nötig: `find ~/html/vocodemo -name '._*' -delete`.

## Dateien hier
- `mwservice.server.yaml` — HTTP-Server (Inbound → Subdomain).
- `mwservice.worker.yaml` — Voice-Worker (nur outbound, Produktionsmodus `start`).
- `ecosystem.config.cjs` — PM2-Alternative: beide Prozesse in einem Service.

## Lokal denselben Prod-Build testen
```bash
pnpm --filter @adva/agent build
node packages/agent/dist/server/http.mjs        # :8787
node packages/agent/dist/voice/worker.mjs start # braucht LiveKit-Creds
```

## Warum stop → start (Hintergrund zu Regel Nr. 1)
Beide Prozesse halten eine Ressource, die sich nicht doppelt vergeben lässt:

- **Server** — den Unix-Socket des `mwservice`. Überlappender Start scheitert an `EADDRINUSE`; der
  Altprozess überlebt und bedient weiter Anfragen mit altem Code.
- **Worker** — LiveKit forkt Kindprozesse (Job-/Inference-Executor), die den geerbten Health-Port
  halten. Deshalb zusätzlich `deploy/reste-aufraeumen.mjs` dazwischen (`ps`/`pkill` fehlen auf dem
  Space). Der Port selbst ist unkritisch: der Worker weicht bei Belegung auf den nächsten freien
  aus (`freierPort` in `voice/worker.ts`).

Diagnose bei Verdacht auf altem Code trotz frischem Bundle:
```bash
$PM2 logs voco-server --lines 30 --nostream --err   # EADDRINUSE auf dem .sock-Pfad?
```
Nennen die Stacktraces **unterschiedliche Zeilennummern derselben Datei**, laufen alter und neuer
Bundle-Stand gleichzeitig — dann ist es genau dieser Fall.

**Und: immer BEIDE Prozesse.** Die Extraktion läuft im Worker — ein Deploy, der nur den Server
neu lädt, lässt die alte Extraktionslogik weiterlaufen.
