# 08 Anleitung für Entwickler Für jemanden, der das Projekt zum ersten Mal öffnet. Was man braucht, wie man es startet, wo man was ändert - und welche Fallen es gibt. ## In zwei Minuten zum Bild * **Ein** Vite-Frontend (React 19, TypeScript, Tailwind 4), **ein** Express-Backend, **zwei** MariaDB-Datenbanken. * Kein Router im Frontend: `App.tsx` hält die Ansicht in einem `useState`. Die App liegt hinter einer Anmeldung, tiefe Links braucht sie nicht. * Kein ORM im CRM-Teil - dort schreibt das Backend SQL mit `mysql2`. Der Scanner-Teil benutzt Sequelize, weil er 1:1 aus einer anderen Anwendung übernommen wurde. * Alles auf Deutsch: Bezeichner, Kommentare, Meldungen. Kommentare erklären das **Warum**, nicht das Was. ## Voraussetzungen | | Version | | --- | --- | | Node | 20 oder neuer (Server läuft 20.20) | | MariaDB | 10.11 oder neuer, erreichbar mit Benutzer und Passwort | | Browser | Chrome/Edge für WebGPU beim Diktat (sonst läuft es auf der CPU) | ## Lokal starten ```bash git clone http://192.168.1.199:3000/sascha/perinco-CRM.git cd perinco-CRM npm install # Frontend cd server && npm install # Backend cp .env.example .env # Werte eintragen, mindestens DB_* und JWT_SECRET npm start # Backend auf :5006 cd .. && npm run dev # Frontend auf :3100, /api wird geproxied ``` Was dabei von selbst passiert: * `perinco_CRM` muss **existieren**; die Tabellen legt `server/src/schema.sql` beim Start an (idempotent). * `perinco_scanner` legt das Backend **komplett selbst** an - Datenbank, Tabellen, Portale, Standardeinstellungen. * Fehlt `ADMIN_PASSWORD`, wird beim ersten Start ein Zufallspasswort erzeugt und **einmalig ins Log geschrieben**. Dort nachsehen, sonst kommt man nicht rein. Ohne MariaDB startet das Backend nicht. Das Frontend lässt sich trotzdem ansehen - siehe *Oberfläche prüfen ohne Datenbank* weiter unten. ## Prüfen ```bash npm run lint # = tsc --noEmit, das ist die Wahrheit npm run build # muss ohne Fehler durchlaufen cd server && npm test # Prüfungen der Postfachanbindung ``` Automatische Tests gibt es nur für **einen** Bereich: `server/test/mail.test.mjs` prüft die Kontaktextraktion aus Mails und den Graph-Client. Beides läuft unbeaufsichtigt im Hintergrund und lässt sich von Hand kaum nachstellen - ein falsch erkannter Name landet still in der Kontaktliste, und ein 429 vom echten Dienst kommt genau dann, wenn niemand hinsieht. Kein Test-Framework, reines Node, keine Datenbank. Für alles andere gilt: `tsc`, ein Build und ein Blick in die Oberfläche - notfalls mit dem Stub aus [Oberfläche prüfen ohne Datenbank](#oberflaeche-pruefen-ohne-datenbank). ## Aufbau ``` src/ Frontend App.tsx Ansicht, zentrale Daten, Layout types.ts Typen, Konstanten, Rechenhilfen (Volumen, Farben) lib/ API-Zugriff und Kontexte components/ je Bereich ein Ordner server/src/ Backend server.js Express-App, Reihenfolge der Routen db.js Verbindungspool, Schema-Init, Admin-Seed settings.js crm_settings lesen und schreiben (ohne Geheimnisse) routes/ ein Router je Ressource middleware/auth.js JWT prüfen, Adminrechte mail/ Postfach crm@perinco.de über Microsoft Graph scanner/ portierter Projekt Scanner (Sequelize, Playwright, OpenAI) docs/ diese Dokumentation deploy/ Apache-Vhosts, systemd-Unit, Deployskript ``` ## Wie eine Anfrage läuft ```mermaid sequenceDiagram participant K as Komponente participant A as lib/api.ts participant E as Express participant M as middleware/auth participant R as routes/*.js participant D as MariaDB K->>A: leadsApi.update(7, { status: 'Gewonnen' }) A->>A: Token anhängen, Content-Type setzen A->>E: PATCH /api/leads/7 E->>M: requireAuth M->>D: Benutzer laden, aktiv? M->>R: req.user gesetzt R->>R: Eingaben prüfen und normalisieren R->>D: UPDATE ... ; SELECT (angereichert) R-->>A: 200 JSON A-->>K: typisiertes Objekt ``` Jede Route liefert den **vollständigen, angereicherten Datensatz** zurück (mit `created_by_name`, `owner_name`, Ansprechpartnern, Teilnehmern). Das Frontend muss danach nichts nachladen. ## Etwas hinzufügen ### Neues Feld an einem Kontakt 1. `server/src/schema.sql`: Spalte mit `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` ergänzen - das Statement muss beim nächsten Start wiederholbar sein. 2. `server/src/routes/leads.js`: Feld in die Listen `TEXT_FIELDS` bzw. in `INSERT`/`UPDATE` und in den `SELECT` aufnehmen. 3. `src/types.ts`: Feld ins `Lead`-Interface. 4. `src/components/leads/LeadModal.tsx`: Eingabefeld, `inputClass` benutzen. 5. Wenn es in Kachel oder Liste sichtbar sein soll: `LeadGrid.tsx`, `LeadList.tsx`. ### Neuer Menüpunkt 1. `src/types.ts`: Wert in `ViewState` ergänzen. 2. `src/components/Sidebar.tsx`: Eintrag in `CATEGORY_ENTRIES` (Name + SVG-Pfad). 3. `src/App.tsx`: Komponente rendern und einen Untertitel setzen. Der Name in `ViewState` ist gleichzeitig die Beschriftung und der Titel in der Kopfzeile. Wer eine abweichende Beschriftung braucht, setzt `label` am `MenuItem` (so macht es der Unterpunkt *Postfach*, der auf die Ansicht `Mails` zeigt). Soll der Punkt aufklappen, kommt statt eines `MenuItem` eine `MenuGroup` in `CATEGORY_ENTRIES` - mit einer Liste ihrer Ansichten in `types.ts` (`CONTACT_VIEWS`, `MAIL_VIEWS`, `SCANNER_VIEWS`), damit die Gruppe offen bleibt, solange eine ihrer Ansichten aktiv ist. ### Neuer Reiter in den Einstellungen 1. `src/types.ts`: Wert in `SettingsTab`; nur für Admins zusätzlich in `ADMIN_SETTINGS_TABS`. 2. `src/components/settings/Settings.tsx`: Eintrag in `TABS` und Rendering. Ein Sprung dorthin aus einer beliebigen Tiefe: `goTo('Einstellungen', { tab: 'whisper' })`. ### Neuer Endpunkt 1. Router in `server/src/routes/` anlegen, `router.use(requireAuth)` nicht vergessen, `requireAdmin` wo nötig. 2. In `server/src/server.js` einhängen - **vor** dem `/api`-404-Fallback. 3. In `src/lib/api.ts` eine typisierte Funktion ergänzen. ### Neue Einstellung im CRM Nur für Werte, die im Betrieb verstellbar sein sollen. **Zugangsdaten gehören nicht dazu** - die stehen in `server/.env` und werden beim Start gelesen, damit ein Datenbank-Dump keine Geheimnisse enthält. 1. Schlüssel samt Vorgabe in das Vorgabenobjekt des Bereichs eintragen (Muster: `MAIL_SETTING_DEFAULTS` in `server/src/mail/config.js`). Es ist gleichzeitig die Positivliste des Speicher-Endpunkts. 2. Lesen mit `getSettings(defaults)` aus `server/src/settings.js`, schreiben mit `setSetting()`. Zahlen und Schalter mit `asInt`/`asBool` wandeln. 3. Feld im passenden Einstellungsreiter ergänzen. Die Tabelle `crm_settings` legt `schema.sql` an; ein neuer Schlüssel braucht keine Migration. ## Fallen Diese Punkte haben real Zeit gekostet: | Falle | Was passiert | | --- | --- | | **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` fängt alles, was danach registriert wird. Neue Router davor einhängen. | | **`Content-Type` bei FormData** | `apiRequest` setzt JSON nur, wenn der Body **keine** `FormData` ist - sonst fehlt die multipart-Grenze und der Upload scheitert. | | **Dateien in neuen Tabs** | Ein `` schickt kein Bearer-Token. Deshalb holt `openAgentCv()` das PDF per `fetch` und öffnet einen Blob. | | **Leerzeichen in Pfaden** | Der alte Scanner liegt unter `/var/www/Projekt Scanner/…`. In der `.env` muss der Wert gequotet sein, sonst schneidet systemd ihn am Leerzeichen ab. | | **`.env` ist gleichzeitig `EnvironmentFile`** | Also kein `export`, keine Shell-Ausdrücke, Werte mit Leerzeichen in Anführungszeichen. | | **Zeitstempel können nur vorziehen** | Ein „vorgealterter" `lastRunTime` verzögert keinen Cronlauf. Wer verzögern will, braucht eine eigene Frist. | | **Sperrfristen bei jedem Start neu** | Früher schob jedes Deployment den fälligen Scan um 30 Minuten - er fand nie statt. Deshalb liegt `last_scan_at` in der Datenbank. | | **`sequelize.sync({ alter: true })`** | Legt bei jedem Start Unique-Indizes neu an und sprengt irgendwann MySQLs Grenze von 64 Keys. Nur `sync()` benutzen. | | **Kaskaden beim Löschen** | Einen Suchagenten löschen löscht alle seine Treffer mit. Inaktiv setzen ist meist gemeint. | | **Doppelte Scanner-Läufe** | Scheduler und *Jetzt scannen* teilen eine Sperre. Ohne sie zahlt man jede KI-Bewertung zweimal. | | **`allow-same-origin` bei `srcDoc`** | Ein so eingebettetes Dokument erbt die Origin der Anwendung und kann `localStorage` lesen - also das Anmeldetoken. Fremdes HTML gehört in `sandbox=""` ohne jedes Flag. Der Preis: die Inhaltshöhe ist nicht auslesbar, deshalb hat der Mailrahmen eine feste Höhe. | | **`/me` unter App-only** | Es gibt keinen Benutzer im Token, also auch kein `/me` - jeder Graph-Aufruf muss `/users/{postfach}/…` lauten. Genau daran ist das Altsystem gescheitert, still, weil die Antwort nicht geprüft wurde. | | **UTC neben Wandzeit** | `crm_mail_messages.received_at` steht in UTC (so liefert Graph es), `crm_events.starts_at` in lokaler Wandzeit. Beim Lesen im Frontend `parseMailDate()` benutzen, sonst ist die Anzeige um Stunden verschoben. | | **Aufzählungen in der Datenbank sind deutsch** | `crm_mail_links.origin` (`manuell`/`automatisch`), Vorschlagsstatus (`offen`, `uebernommen`, `verworfen`, `dublette`). Wer englische Werte schickt, bekommt einen Datenbankfehler statt einer Validierungsmeldung. | ## Oberfläche prüfen ohne Datenbank Für Bildschirmfotos und Klickwege genügt ein Stub: ein kleiner `node:http`-Server, der `dist/` ausliefert und auf `/api/...` erfundene Daten zurückgibt. Token vorab setzen, dann ist die Anmeldung übersprungen: ```js await page.goto(BASE); await page.evaluate(() => localStorage.setItem('perinco_crm_token', 'stub-token')); await page.goto(BASE); ``` Playwright startet dabei am besten das **installierte Chrome** (`chromium.launch({ channel: 'chrome' })`) - dann braucht es keinen Browser-Download. ## Konventionen * **Deutsch** in Bezeichnern, Kommentaren, Meldungen. Fachbegriffe bleiben englisch (`Match`, `Opportunity`, `Lead`). * **Kommentare erklären das Warum.** Was der Code tut, steht im Code. * **Kein `dark:` im Markup.** Nur die Skalen `ink-*`, `brand-*`, `canvas`, `surface` benutzen, den Rest macht `index.css`. * **Gemeinsame Klassen** statt kopierter Utility-Ketten: `inputClass`, `labelClass` aus `components/leads/leadUi.tsx`, dazu `cardClass`, `primaryButtonClass` aus `components/scanner/scannerUi.tsx`. * **Meldungen als Toast**, Löschabfragen über `ConfirmDialog` - nie `alert()` oder `confirm()`. * **Rechte immer doppelt**: Oberfläche blendet aus, Backend antwortet mit 403. ## Deployment Siehe [06 Betrieb](06-betrieb.md). Kurzform: lokal bauen, `deploy/deploy.ps1` lädt `dist/` und `server/src` hoch und startet den Dienst neu. Auf dem Server wird **nicht** gebaut - dort sind RAM und Swap knapp.