Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
11 KiB
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.tsxhält die Ansicht in einemuseState. 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
git clone https://git.perinco.de/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_CRMmuss existieren; die Tabellen legtserver/src/schema.sqlbeim Start an (idempotent).perinco_scannerlegt 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
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.
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
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
server/src/schema.sql: Spalte mitALTER TABLE ... ADD COLUMN IF NOT EXISTSergänzen - das Statement muss beim nächsten Start wiederholbar sein.server/src/routes/leads.js: Feld in die ListenTEXT_FIELDSbzw. inINSERT/UPDATEund in denSELECTaufnehmen.src/types.ts: Feld insLead-Interface.src/components/leads/LeadModal.tsx: Eingabefeld,inputClassbenutzen.- Wenn es in Kachel oder Liste sichtbar sein soll:
LeadGrid.tsx,LeadList.tsx.
Neuer Menüpunkt
src/types.ts: Wert inViewStateergänzen.src/components/Sidebar.tsx: Eintrag inCATEGORY_ENTRIES(Name + SVG-Pfad).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
src/types.ts: Wert inSettingsTab; nur für Admins zusätzlich inADMIN_SETTINGS_TABS.src/components/settings/Settings.tsx: Eintrag inTABSund Rendering.
Ein Sprung dorthin aus einer beliebigen Tiefe:
goTo('Einstellungen', { tab: 'whisper' }).
Neuer Endpunkt
- Router in
server/src/routes/anlegen,router.use(requireAuth)nicht vergessen,requireAdminwo nötig. - In
server/src/server.jseinhängen - vor dem/api-404-Fallback. - In
src/lib/api.tseine 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.
- Schlüssel samt Vorgabe in das Vorgabenobjekt des Bereichs eintragen (Muster:
MAIL_SETTING_DEFAULTSinserver/src/mail/config.js). Es ist gleichzeitig die Positivliste des Speicher-Endpunkts. - Lesen mit
getSettings(defaults)ausserver/src/settings.js, schreiben mitsetSetting(). Zahlen und Schalter mitasInt/asBoolwandeln. - 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 <a href> 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:
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 Skalenink-*,brand-*,canvas,surfacebenutzen, den Rest machtindex.css. - Gemeinsame Klassen statt kopierter Utility-Ketten:
inputClass,labelClassauscomponents/leads/leadUi.tsx, dazucardClass,primaryButtonClassauscomponents/scanner/scannerUi.tsx. - Meldungen als Toast, Rückfragen über
ConfirmDialog- niealert()oderconfirm(). Rot (tone="danger", Standard) nur, wenn etwas verloren geht; sonsttone="neutral", damit die Farbe ihre Bedeutung behält. - Rechte immer doppelt: Oberfläche blendet aus, Backend antwortet mit 403.
Deployment
Siehe 06 Betrieb. 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.