# Bestellverwaltung (eigener Server) – Architektur und Betriebshandbuch

Stand: 21.09.2026 (Paket 8). Zweck: Betrieb im Alltag **und** nahtloses Weiterarbeiten mit einer anderen KI/Entwicklerin.
Erst „1. Auf einen Blick“ und „9. Regeln“ lesen, dann den Rest bei Bedarf.

## 1. Auf einen Blick
- **Was:** Eingebettete Shopify-App (Admin) für Einkaufsbestellungen, Wareneingang (Bestandsbuchung), Bestellhistorie, Lagerbestand,
  Einkaufspreise, Druckansichten. Shop: `ceramique-peinture.fr` (`1eqrdf-tq.myshopify.com`), Einzelshop-App (nur dieser Shop, per Code erzwungen).
- **Ersetzt:** die Sidekick-App „Bestellverwaltung (Metafield)“ (Version 49, bleibt bis zum Ende der Umstellung als Rückfall unberührt).
- **Neue App:** „Bestellverwaltung Server“, Client-ID `b4ef245391336fddca6a8e2d29a7019c`, Dev-Dashboard-Organisation 162147713,
  benutzerdefinierte Verteilung. Konfiguration in `shopify.app.toml` (Ordner auf dem Mac: `~/bestellverwaltung-neu-cli`, Einspielen mit `shopify app deploy`; NIE im Ordner der alten Sidekick-App).
- **Stack:** Node 22, Remix v2 (Vite) + Express, React 18, Polaris-**Webkomponenten** (`<s-page>`, `<s-button>` …), TypeScript-Oberfläche (`app._index.tsx`),
  SQLite (better-sqlite3), `@shopify/shopify-app-remix` 3.x (shopify-api 11.x), Token-Exchange-Anmeldung (`unstable_newEmbeddedAuthStrategy`).
- **Hosting:** Novatrend Shared-Hosting (CloudLinux/LiteSpeed/Passenger), Subdomain `bestellverwaltung.ceramique-peinture.fr`, Nutzer `service8`,
  SSH `ssh -p 22 service8@193.33.128.139`. Kein Compiler auf dem Server; `better_sqlite3.node` wurde manuell bereitgestellt.
- **Shopify-API-Version:** `2026-07` (Zeichenkette; Enum kennt sie in shopify-api 11 nicht, funktioniert aber). Direct API Access:
  `shopify:admin/api/2026-07/graphql.json` (ohne Versionsangabe gilt eine ältere Standardversion, in der `@idempotent` fehlt).

## 2. Datenaufteilung
| Ort | Inhalt |
|---|---|
| **Shopify** | Bestände, Standorte, Artikel/Varianten, Einkaufspreis-Metafeld `custom.einkaufspreis`, Lagerort-Metafeld/-Definition `custom.lagerstandort`, Shop-Metafeld `custom.lagerstandort_liste` (Order-Printer-Vorlagen!) |
| **SQLite `DB_PATH`** | Einkaufsbestellungen, Positionen, Historie (wird nie gekürzt), Buchungsvorgänge, Fortschritt je Artikel, Einstellungen, Webhook-IDs, Druckansichten |
| **SQLite `SESSION_DB_PATH`** | nur Shopify-Sitzungen (getrennte Datei; darf NIE dieselbe Datei wie `DB_PATH` sein) |
| Alte Metafelder (`purchase_orders`, `bestellhistorie`, `bestellhistorie_2026`) | nur Quelle der einmaligen Migration, danach Archiv; nicht löschen |

## 3. Dateien (App-Ordner `~/ceramique-peinture.fr/bestellverwaltung/`)
```
server.js                 Express: Health, Rate-Limit, Header, statische Dateien, Remix-Handler, schreibt app-runtime.json
app/
  root.jsx, entry.server.jsx, shopify.server.js (Konfiguration, SHOP-Sperre)
  db.server.js            Schema, Pfade (absolut, per Umgebung überschreibbar), Rechte 600
  store.server.js         gesamte Datenlogik (Bestellungen, Wareneingang, Import, Druck, Diagnose) – ohne HTTP
  api.server.js           /api/* → Store-Funktionen (Router ohne Shopify-Bezug)
  session-storage.server.js  Shopify-Sitzungen auf better-sqlite3
  hmac.server.js          Webhook-Signaturprüfung
  lib/backend.ts          Browser-Client für /api (Wiederholungen, Fehlerklassen)
  lib/shopify-client.ts   Direct-API-Zugriff (shopifyQuery, API_VERSION), Wiederholung bei Drosselung
  lib/print.ts            Fenster-Voraböffnung für Druckansichten
  routes/app._index.tsx   gesamte Oberfläche (9 Menüpunkte)
  routes/app.selftest.jsx Selbsttest (Anmeldung, Direct API, Backend, Webkomponenten)
  routes/api.$.jsx        /api/* (Anmeldung Pflicht, Shop-Sperre, /api/diag)
  routes/api.webhooks.$.jsx  Pflicht-Webhooks (nur HMAC, idempotent)
  routes/print.$id.jsx    öffentliche, nicht erratbare Druckseite (2 h gültig, strenge CSP)
alerts.js  monitor.js  backup.js     Betrieb (Cron), unabhängig von der Web-App
ops/deploy.sh  ops/db-backup.sh  ops/db-change.sh  ops/bestellverwaltung.env.example
test/*.test.mjs           `npm test` (22 Tests: Datenschicht, Betrieb)
public/.htaccess          Passenger-Konfiguration + SetEnv (Zugangsdaten!) – wird NICHT im Code-Backup mitgesichert (extra Kopie bei jedem Deploy)
```

## 4. Umgebungsvariablen (cPanel Node.js-Manager **und** `public/.htaccess` – beide identisch halten!)
`SHOPIFY_API_KEY`, `SHOPIFY_API_SECRET` (Client-Secret, in Anführungszeichen), `SHOPIFY_APP_URL`, `SHOPIFY_SHOP`, `SCOPES`
(`read_products,write_products,read_inventory,write_inventory,read_locations,read_orders`), `NODE_ENV=production`,
`DB_PATH`, `SESSION_DB_PATH`. Bei Abweichungen gewinnt nicht vorhersagbar eine der Quellen (hat zu einer Datenbank-Verwechslung geführt).
Kontrolle des **laufenden** Prozesses:
`for p in $(pgrep -u service8 -f "lsnode:.*bestellverwaltung"); do tr '\0' '\n' < /proc/$p/environ | grep -E '^(DB_PATH|SESSION_DB_PATH|SHOPIFY_API_KEY)='; done`
In der App: Menü „Datenmigration“ → „0. Verbindung prüfen“ zeigt die aktive Datenbank; `/api/diag` (nur angemeldet) liefert Versionen und Zähler.
Betriebsskripte lesen zusätzlich `~/.bestellverwaltung.env` (chmod 600; SMTP-Zugang, Empfänger, optional Heartbeat-URL; Vorlage `ops/bestellverwaltung.env.example`).
Die Datenbankdatei holen sie aus `~/data/app-runtime.json` (schreibt die App beim Start) → immer dieselbe Datei wie die App.

## 5. API (alle unter `/api`, Shopify-Sitzungstoken Pflicht, nur Shop `1eqrdf-tq`)
`GET/POST /orders`, `GET/PUT/DELETE /orders/:id`, `POST /orders/:id/receiving/start|progress|finish`, `GET /orders/:id/receiving/unclear`,
`GET /orders/:id/booked`, `GET/POST /history`, `POST /import[?dryRun=1]`, `GET /export`, `GET /stats`, `GET /diag`, `POST /print`,
`GET/PUT /settings/:key` (nur `location_id`, noch nicht in der Oberfläche verdrahtet).
Öffentlich (bewusst): `GET /healthz`, `GET /print/<192-Bit-ID>`, `POST /api/webhooks/{customers/delete,customers/data_request,shop/delete}` (nur mit gültiger HMAC-Signatur).

## 6. Wareneingang – Ablauf und Absturzsicherheit
1. `receiving/start` (Sperre: Status `processing`, Buchung angelegt, ältere unfertige Buchungen → `abandoned`).
2. Je Artikel: `progress pending` (Absicht) → Shopify `inventoryAdjustQuantities` mit `@idempotent` und `changeFromQuantity` → `progress booked|skipped` (schreibt sofort die Historie).
   Ohne bestätigte Absicht wird nicht gebucht.
3. `receiving/finish` (idempotent über bookingId; setzt `completed` nur, wenn alle Artikel gebucht).
Bricht der Browser ab: bereits gebuchte Artikel stehen in der Historie; nur der Artikel „in Arbeit“ ist ggf. **unklar** (`booking_items.status='pending'`) –
die Maske belegt ihn mit 0 vor und warnt; Bestandsverlauf im Shopify-Admin prüfen. Hängende Buchungen erscheinen im Menü und lösen einen Alarm aus.
Bestände/Preise/Standorte liegen nie in der SQLite-Datenbank (nur die Bestellungen und ihre Historie).

## 7. Betrieb
**Neustart / Deployment (immer so):** `./ops/deploy.sh` – sichert Datenbank (`backup.js --label=vor-deploy`, Abbruch bei Fehler) und Code
(`~/backups/deploy/code_*.tgz`, inkl. `build/`, ohne `node_modules`/`public`), stoppt alle Node-Apps (`pkill -9 -f lsnode`, wegen Thread-Limit beim Bauen),
baut mit `GOMAXPROCS=2 UV_THREADPOOL_SIZE=2`, startet neu (`sleep 25`), prüft `/healthz`; bei Build-Fehler wird das alte `build/` automatisch zurückgeholt.
Nur Neustart: `./ops/deploy.sh --no-build`. Code-Rollback: `./ops/deploy.sh --rollback code_YYYYMMDD_HHMMSS.tgz` (Datenbank bleibt unberührt).
Nach Upload von Dateien per FTP: `npm run build` ist Pflicht; nach Änderung der `package.json`: cPanel → Node.js App → Stop → **NPM Install** (nie `npm install` im Terminal: Symlink-Problem).
Logs: `tail -50 ~/ceramique-peinture.fr/bestellverwaltung/stderr.log`.

**Backup:** Cron 02:30 täglich (`backup.js`), 30 Tage Aufbewahrung, `~/backups/bestellverwaltung/bestellverwaltung_*.db` (Integrität geprüft, chmod 600).
Zusätzlich vor jedem Deploy und mit `./ops/db-backup.sh <grund>`. Direkte DB-Änderungen nur mit `./ops/db-change.sh <grund> "<SQL>"` (erzwingt Backup davor).
**Wiederherstellung:** Apps stoppen (`pkill -9 -f lsnode`), aktuelle Dateien zur Seite legen (`mv ~/data/bestellverwaltung.db ~/data/vorher_$(date +%s).db`; `-wal`/`-shm` ebenfalls),
gewünschte Sicherung nach `~/data/bestellverwaltung.db` kopieren, `chmod 600`, App starten, Menü „Verbindung prüfen“ prüfen. Nach Wiederherstellung Bestände im Shopify mit der Historie abgleichen (Historie kann hinter Shopify zurückliegen).

**Cron-Einträge (cPanel → Cron Jobs; Feld „Cron-E-Mail“ = admin@ceramique-peinture.fr):**
```
30 2 * * *    /home/service8/nodevenv/ceramique-peinture.fr/bestellverwaltung/22/bin/node /home/service8/ceramique-peinture.fr/bestellverwaltung/backup.js >> /home/service8/backups/backup.log 2>&1 || echo "BACKUP FEHLGESCHLAGEN, siehe backup.log"
*/15 * * * *  /home/service8/nodevenv/ceramique-peinture.fr/bestellverwaltung/22/bin/node /home/service8/ceramique-peinture.fr/bestellverwaltung/monitor.js >> /home/service8/backups/monitor.log 2>&1 || echo "MONITOR ABGESTUERZT, siehe monitor.log"
```
(alte Backup-Zeile mit festem `DB_PATH=` ersetzen). **Externe Überwachung** (unabhängig vom Server): kostenlose Dienste wie UptimeRobot (URL `https://bestellverwaltung.ceramique-peinture.fr/healthz`)
und healthchecks.io (Heartbeat-URL in `~/.bestellverwaltung.env` als `HEARTBEAT_URL`; meldet, wenn der Backup-Lauf ausbleibt).

**Was die Überwachung prüft (`monitor.js`, alle 15 Min):** better-sqlite3 lädt; Fingerabdruck (Node-Version, ABI, Plattform, better-sqlite3-Version) unverändert;
`/healthz` antwortet (eine Wiederholung nach 30 s); Datenbankdatei vorhanden; täglich `quick_check`; keine Buchung > 30 Min unfertig; keine unklaren Artikel;
Backup < 26 h alt; ≥ 500 MB frei. Mails an `admin@ceramique-peinture.fr` (Sperrfrist 6 h je Problem, Entwarnung bei Behebung). Ergebnis steht auch im App-Menü (Banner).

**Novatrend-Upgrade / Node-Wechsel:** Der Fingerabdruck-Vergleich meldet jede Änderung von Node/ABI/Binärdatei per Mail; lädt `better_sqlite3.node` danach nicht mehr, kommt zusätzlich ein KRITISCH-Alarm.
Vorbereitung: funktionierende Binärdatei sichern (`cp …/node_modules/better-sqlite3/build/Release/better_sqlite3.node ~/backups/binaries/better_sqlite3_node22_$(date +%Y%m%d).node`).
Bei neuem Node-ABI: passende Binärdatei für linux-x64 auf dem Mac beschaffen (`npx prebuild-install --platform=linux --arch=x64 --runtime=node --target=<Node-Version>` im better-sqlite3-Ordner) und nach `node_modules/better-sqlite3/build/Release/` hochladen.
Im cPanel Node.js-Manager die Node-Version der App fest auf 22 lassen, solange angeboten.

## 8. Sicherheit (Stand der Prüfung)
| Bereich | Maßnahme |
|---|---|
| Zugriff | Jede `/api`-Anfrage braucht ein gültiges Shopify-Sitzungstoken (Header, kein Cookie ⇒ kein CSRF); zusätzlich Shop-Sperre `1eqrdf-tq` |
| Geheimnisse | nur in cPanel/`.htaccess`/`~/.bestellverwaltung.env` (600); nie im Frontend, nie im Code/ZIP; Client-Secret nie in Chats |
| Webhooks | HMAC-SHA256 über den Roh-Body (zeitkonstanter Vergleich), Deduplizierung über Webhook-ID; `shop/redact` löscht nur Sitzungen, keine Betriebsdaten |
| Einbettung/CSP | `frame-ancestors` nur Shopify (Bibliothek); Druckseite: `default-src 'none'`, eigenes Skript nur mit Nonce, `frame-ancestors 'none'`, `noindex` |
| Header | `X-Content-Type-Options`, HSTS, `Referrer-Policy`, `Permissions-Policy`; **kein** `X-Frame-Options` (würde die Einbettung brechen) |
| Missbrauchsschutz | Rate-Limit 600/Min je IP auf `/api` und `/print`; Größenlimit 10 MB je Anfrage; Druckseiten 192-Bit-ID, 2 h gültig |
| Daten | Dateien `chmod 600` (umask 077), Verzeichnisse 700, Datenbanken außerhalb des Webordners; Backups 600 |
| Abhängigkeiten | `npm audit --omit=dev`: 18 Meldungen (1 kritisch, 11 hoch, 6 mittel), **alle in der Remix-v2-/Build-Kette** (`@remix-run/*`, `react-router`, `turbo-stream`, `vite`/`esbuild`, `tar`, `toml`), Bewertung siehe unten |
Erläuterung: Die Build-Werkzeuge (`vite`, `esbuild`, `tar`, `toml`) laufen nur beim Bauen, nicht als Server. Die Remix-Funde (`turbo-stream`-DoS bei „single fetch“, `react-router`-Redirect/Deserialisierung)
betreffen Funktionen, die diese App nicht nutzt (Single Fetch ist nicht aktiviert; keine `Link`/`navigate`-Ziele aus Nutzereingaben); zudem ist die App nur für angemeldete Shop-Mitarbeitende erreichbar.
Ein Fix existiert in der Remix-v2-Linie nicht (nur durch Wechsel auf React Router v7 – bewusst vermeiden: `@react-router/express` erzwingt in dieser Kombination `isSpaMode:true`). `nodemailer` ist auf 10.x (Funde behoben).
Vierteljährlich `npm audit` wiederholen; langfristig Migration auf das aktuelle Shopify-Template (React Router) einplanen.
**Offen/Empfehlung:** Zwei-Faktor-Anmeldung für Shopify-Admin und cPanel, starke SSH-Zugangsdaten bzw. Schlüssel, `curl -I https://bestellverwaltung.ceramique-peinture.fr/.htaccess` muss 403/404 liefern (nicht 200).

## 9. Regeln (aus den Serverpfaden und bisherigen Fehlern – bitte einhalten)
1. **Immer** `@remix-run/express` (nie `@react-router/express`); im Build `isSpaMode = false` prüfen: `grep "isSpaMode" build/server/index.js`.
2. `authenticate.admin()` nur in einzelnen Route-Loadern, **nie** in `root.jsx` oder Layout-Routen (`app.jsx`).
3. Datenbankpfade absolut; `DB_PATH` ≠ `SESSION_DB_PATH`; Umgebungsvariablen in cPanel und `.htaccess` identisch.
4. `better-sqlite3` bleibt `external` in `vite.config.js`; Version `^9.4.3` in `package.json` (Server nutzt die manuell kopierte Binärdatei); Build-Werkzeuge (`vite`, `@remix-run/dev`) gehören in `dependencies` (cPanel installiert keine Dev-Pakete).
5. Webkomponenten unter React 18: boolesche Attribute nur setzen, wenn wahr (`disabled={x || undefined}`); `change`-Ereignisse per `listen('change', …)`; nie `e.currentTarget` in verzögerten Callbacks lesen.
6. Druckansichten nie als `blob:`/`data:`-Link; immer über `/api/print` → `/print/<id>`.
7. Bestandswirksame Tests nur mit Testartikel und sofortiger Rückbuchung; Test-Datenbank nie mit der Produktiv-Datei verwechseln; echter Import nur einmal und nur in leere Datenbank (Serverseitig erzwungen).
8. Vor jedem Deployment und jeder Datenbankänderung ein zeitgestempeltes Backup (`ops/deploy.sh`, `ops/db-change.sh`).
9. `pkill -9 -f lsnode` beendet alle Node-Apps des Accounts (Transportversicherung, Wishlist, Rechnungsmailer); danach 25 s warten.

## 10. Umstellung auf Produktion (Ablauf, siehe Übergabe Abschnitt 8)
Abnahme-Checkliste komplett ✔ → Umstellungsfenster → Produktiv-Datei (`bestellverwaltung.db`) leer sichern → `DB_PATH` (cPanel + `.htaccess`) auf sie setzen, `./ops/deploy.sh --no-build` →
Menü „Verbindung prüfen“ (0 Bestellungen/0 Historie) → „Metafelder lesen“ → Roh-Backup speichern → Probelauf → **echter Import** → Zahlen mit Alt-App vergleichen (7 Bestellungen, 845 Historieneinträge am 20.09.2026, danach aktuell) →
Backup → Freigabe. Alte Metafelder und Sidekick-App bleiben 4–6 Wochen als Archiv/Rückfall; danach Sidekick-App deinstallieren.

## 11. Offene Punkte / Ideen
- Standort-Einstellung (`settings/location_id`) in Oberfläche verdrahten (bisher „erster aktiver Standort“ wie in der Alt-App).
- Phase 2 (optional): Wareneingang serverseitig in Stapeln statt im Browser.
- Prüfen, ob `read_products` zum Lesen der Shop-Metafelder in der Migration ausreicht (zeigt der Probelauf); sonst Scope in `shopify.app.toml` ergänzen und erneut `shopify app deploy`.
- Langfristig: Migration auf React Router v7 (Shopify-Template), sobald `isSpaMode`-Problem und Hosting-Limits geklärt sind.
