diff --git a/README.md b/README.md index 9d8f38a..895b29e 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,11 @@ CRM im perinco Corporate Design. Vite/React-Frontend, Express-Backend, MariaDB. Enthaelt den portierten *Projekt Scanner*. -> **Technische Dokumentation mit Funktionszeichnungen:** [`docs/`](docs/README.md) +> **Dokumentation mit Funktionszeichnungen:** [`docs/`](docs/README.md) > — Architektur, Datenmodell, Scanner-Ablauf, Diktat, Frontend, Betrieb. +> Zum Einstieg: [Anleitung fuer Benutzer](docs/07-anleitung-benutzer.md) · +> [Anleitung fuer Entwickler](docs/08-anleitung-entwickler.md) · +> [Quellcode-Referenz](docs/09-quellcode.md) ## Corporate Identity diff --git a/docs/07-anleitung-benutzer.md b/docs/07-anleitung-benutzer.md new file mode 100644 index 0000000..0cb559b --- /dev/null +++ b/docs/07-anleitung-benutzer.md @@ -0,0 +1,257 @@ +# 07 Anleitung fuer Benutzer + +Diese Anleitung setzt keine Vorkenntnisse voraus. Sie beschreibt, was die +Anwendung tut und wie man sie bedient. + +## Was die Anwendung ist + +Ein CRM fuer die Projektvermittlung. Es beantwortet vier Fragen: + +| Frage | Bereich | +| --- | --- | +| Welche Geschaefte sind in Arbeit und was sind sie wert? | **Opportunities** | +| Wer ist wann wo, und wer ist beteiligt? | **Kalender** | +| Wen kennen wir, und wer kennt wen? | **Kontakte** | +| Welche Projekte auf den Boersen passen zu unseren Beratern? | **Scanner** | + +## Anmelden + +`https://pcrm.perinco.de`. Anmelden geht mit **Benutzername oder E-Mail** plus +Passwort. Die Sitzung haelt 12 Stunden, danach fragt die Anwendung erneut. + +Sagt die Meldung „Benutzername oder Passwort ist falsch", bleibt bewusst offen, +welches von beidem nicht stimmte - so kann ueber die Anmeldemaske niemand +Benutzernamen durchprobieren. + +Es gibt zwei Rollen: **Administrator** sieht zusaetzlich die Nutzerverwaltung und +die Scanner-Einstellungen. Alles andere ist fuer beide Rollen gleich. + +## Sich zurechtfinden + +``` +┌──────────────┬───────────────────────────────────────────┐ +│ Kategorien │ Titel der Ansicht [Hell/Dunkel] │ +│ Opportunities├──────────────────────────────────────────┤ +│ Kalender │ │ +│ Kontakte │ Inhalt │ +│ Scanner ▾ │ │ +│ Dashboard │ │ +│ Unpassend │ │ +│ Suchagenten│ │ +│ Boersen-… │ │ +├──────────────┤ │ +│ Einstellungen│ │ +├──────────────┤ │ +│ [Foto] Name │ │ +└──────────────┴───────────────────────────────────────────┘ +``` + +* Der Knopf mit den drei Strichen oben links klappt das Menue auf Symbolbreite + zusammen - hilfreich auf kleinen Bildschirmen. +* Rechts oben schaltet zwischen hellem und dunklem Design. Ein **Doppelklick** + auf den Schalter folgt wieder der Systemeinstellung. +* Der Menuepunkt **Scanner** klappt beim Anklicken seine Unterpunkte auf. + +## Opportunities + +Der Startbereich: eine Opportunity ist ein Geschaeft in Arbeit. Oben rechts +wechselt man die Darstellung: + +| Ansicht | wofuer | +| --- | --- | +| **Kachel** | Ueberblick mit Volumen, Tagessatz, Laufzeit, letzter Interaktion | +| **Liste** | viele Eintraege vergleichen, Spaltenkoepfe sortieren | +| **Pipeline** | Fortschritt durch die Phasen, Karten per Maus verschieben | + +**Volumen** rechnet die Anwendung selbst: Tagessatz x Personentage. Das +**gewichtete** Volumen multipliziert das noch mit der Abschlusswahrscheinlichkeit. +Beide Zahlen stehen live im Formular, auf jeder Karte, als Summe ueber der Liste, +je Pipeline-Spalte und in der Kopfzeile. + +Zu jeder Opportunity gehoeren beliebig viele **Ansprechpartner** aus den +Kontakten, jeder mit eigener Rolle („Entscheider", „Fachbereich", …). + +Aendern und loeschen darf der **Ersteller**, der **Zustaendige** und jeder +**Administrator**. Wer keines davon ist, bekommt eine Fehlermeldung - das prueft +der Server, nicht nur die Oberflaeche. + +Auf dem Tablet oder ohne Lust auf Ziehen: die Phase laesst sich auch ueber das +Auswahlfeld direkt auf der Karte aendern. + +## Kalender + +Ein **gemeinsamer** Teamkalender - alle sehen dieselben Termine, jeder darf +eintragen. + +* **Monat, Woche, Tag** oben umschalten, `Heute` und die Pfeile blaettern. +* Klick auf einen Monatstag oder eine Stundenzelle legt sofort einen Termin an. +* Klick auf einen Wochentagskopf springt in die Tagesansicht. +* **Farben** gehoeren zum Ersteller. Die eigene Farbe aendert man unter + *Einstellungen → Mein Konto*; die Legende unter dem Kalender zeigt, welche + Farbe zu wem gehoert. +* **Teilnehmer** erscheinen mit Foto oder Initiale und sind anklickbar. +* **@-Erwaehnungen**: In der Beschreibung `@` tippen, dann Kontakte *und* + Benutzer aus der Vorschlagsliste waehlen (Pfeiltasten, Enter). In der + Leseansicht sind sie klickbar - Kontakte oeffnen die Kontaktkarte, Benutzer ein + Detailfenster mit naechsten Terminen. +* Rechts stehen die **naechsten Termine**, nach Tagen gruppiert. Vergangenes + fliegt aus der Liste, bleibt im Kalender aber erhalten. + +Aendern und loeschen darf nur der **Ersteller** oder ein **Administrator**. +Anlegen darf jeder. + +## Kontakte + +Drei Ansichten, davon eine besondere: + +| Ansicht | wofuer | +| --- | --- | +| **Kachel** | Ueberblick mit Foto, Firma, Status | +| **Liste** | sortierbar, viele Eintraege | +| **Netzwerk** | wer steht mit wem in Kontakt | + +Im **Netzwerk** sind Kreise Kontakte (Farbe = Status, Groesse = Anzahl +Verbindungen) und Quadrate Benutzer, die Kontakte angelegt haben. Gruene Linien +verbinden Kontakte untereinander, graue gestrichelte den Ersteller mit seinem +Kontakt. + +Bedienung: Hintergrund ziehen verschiebt, Mausrad zoomt, einen Knoten ziehen +setzt ihn fest (gruener Punkt), Doppelklick loest ihn wieder. Ueber *Verbindung +erstellen* zwei Kontakte anklicken, um eine Verbindung anzulegen; `Esc` bricht ab. +Suche und Statusfilter wirken auch hier. + +## Scanner + +Durchsucht Projektboersen und bewertet jeden Fund mit KI gegen die Lebenslaeufe +der Berater. + +### Dashboard + +Oben vier Kennzahlen, darunter die Trefferliste. Der Knopf **Jetzt scannen** +startet sofort einen Suchlauf; laeuft schon einer, sagt die Anwendung das. + +Die Filterzeile: + +| Feld | Bedeutung | +| --- | --- | +| **Zeitraum** | wie alt darf das Projekt sein (Standard: letzte 24 Stunden) | +| **Agent** | nur Treffer eines Beraters | +| **Anzeige ab %** | schwaechere Treffer ausblenden | +| **Hinweis ab %** | ab wann Signalton und E-Mail | + +Die letzten zwei Werte werden **gespeichert** und gelten auch beim naechsten Mal +und in den Einstellungen. Rechts daneben steht `sichtbar / gesamt`. + +Ein Klick auf eine Zeile oeffnet die Details: Projektbeschreibung, die +KI-Auswertung mit Pro und Contra, dazu die Knoepfe *Gesehen*, *Beworben*, +*Zurueck auf Neu*, *Boerse oeffnen*, *Passt nicht* und *Treffer loeschen*. + +Ueber der Liste steht der Betriebszustand: *automatische Suche aktiv*, *nur +manuelle Suche*, *pausiert* oder *Suchlauf laeuft*. + +### Ton bei neuen Treffern + +Findet der Scanner einen Treffer ueber dem Wert *Hinweis ab %*, spielt das +Dashboard einen Signalton und zeigt ein Hinweisbanner. Drei Dinge muss man dazu +wissen: + +1. **Nur im Dashboard.** Wer auf Opportunities oder Kontakte wechselt, hoert + nichts - und bekommt es auch spaeter nicht nachgemeldet. +2. **In einem anderen Browsertab** klingelt es weiter, aber verzoegert: der + Browser bremst versteckte Tabs auf etwa eine Pruefung pro Minute. Ausserdem + muss man vorher einmal irgendwo auf der Seite geklickt haben, sonst laesst der + Browser keinen Ton zu. +3. **Ist der Tab geschlossen, passiert nichts.** Wer unabhaengig vom Browser + benachrichtigt werden will, schaltet die **E-Mail** in den Einstellungen ein. + +### Unpassend + +Alles, was ueber *Passt nicht* aussortiert wurde. Von hier bringt *Zurueck ins +Dashboard* einen Treffer wieder zurueck, oder er wird endgueltig geloescht. + +### Suchagenten + +Ein Suchagent ist ein Berater: + +| Feld | wofuer | +| --- | --- | +| **Name** | erscheint an jedem Treffer | +| **E-Mail** | Ziel der Benachrichtigung fuer diesen Berater | +| **Suchbegriffe** | kommagetrennt, damit wird auf den Boersen gesucht | +| **Ausschluss-Begriffe** | kommagetrennt; Projekte mit diesen Woertern werden uebersprungen | +| **Lebenslauf (PDF)** | wird als Text ausgelesen und gegen jedes Projekt bewertet | +| **aktiv** | nur aktive Agenten suchen im Hintergrund | + +Ohne Lebenslauf kann ein Agent nichts bewerten. Die Ausschluss-Begriffe sind +nicht nur Bequemlichkeit: sie greifen **vor** der KI-Bewertung und sparen damit +echte Kosten. „Arbeitnehmerueberlassung" oder „Festanstellung" sind typische +Kandidaten. + +> **Loeschen nimmt die Treffer mit.** Wird ein Suchagent geloescht, verschwinden +> alle seine bisherigen Treffer und sein Lebenslauf mit ihm - die Datenbank raeumt +> das automatisch auf. Die gefundenen **Projekte** bleiben erhalten, sie gehoeren +> nicht zu einem einzelnen Agenten. Wer einen Berater nur vorübergehend aus der +> Suche nehmen will, setzt ihn besser auf **inaktiv** statt ihn zu loeschen: dann +> sucht er nicht mehr, die Treffer bleiben aber sichtbar. + +### Boersen-Konten + +Zeigt fuer jede Boerse, ob die Verbindung steht. Zwei Arten: + +* **Oeffentliche Portale** (Hays, Etengo, …) brauchen keinen Login. +* **Geschuetzte Portale** (Freelance.de, Freelancermap.de) brauchen eine + angemeldete Sitzung. Dafuer exportiert man die Cookies mit der + Browser-Erweiterung *Cookie-Editor* als JSON und fuegt sie unter + **Cookie-Import** ein. + +*Jetzt pruefen* testet eine Verbindung sofort. Steht dort **Abgelaufen / +CAPTCHA**, hilft nur ein neuer Cookie-Import. + +Die Zugangsdaten (Benutzername, Passwort) sind optional - der Scraper versucht +damit im Notfall eine eigene Anmeldung. Passwoerter liegen verschluesselt in der +Datenbank. + +## Einstellungen + +| Reiter | Wer | Inhalt | +| --- | --- | --- | +| **Nutzerverwaltung** | Admin | Benutzer anlegen, bearbeiten, deaktivieren, loeschen | +| **Mein Konto** | alle | Foto, Kalenderfarbe, Passwort aendern | +| **Scanner** | Admin | Suchintervall, Schwellenwerte, KI-Schluessel, Ton, E-Mail | +| **Whisper** | alle | Spracherkennung fuer das Diktat einrichten | + +Im Reiter **Scanner** steht das **Suchintervall (Minuten)** - also wie oft der +Hintergrund-Scanner die Boersen durchsucht. + +Darueber der Hauptschalter **Scanner aktiviert**. Ist er aus, sucht die Anwendung +nicht mehr: kein Lauf im Hintergrund, und *Jetzt scannen* antwortet mit einem +Hinweis statt zu starten. Der Zustand steht auch oben im Scanner-Dashboard, damit +man nicht raetselt, warum keine neuen Treffer kommen. + +Foto und Kalenderfarbe werden **sofort** gespeichert, ohne Speichern-Knopf. Der +letzte aktive Administrator kann sich nicht selbst degradieren oder loeschen - +das verhindert der Server. + +## Diktat + +Ueberall, wo laengerer Text entsteht, sitzt ein Mikrofon-Knopf. Beim ersten Mal +steht dort *Diktat einrichten* und fuehrt in die Einstellungen: dort einmal +**Sprachmodell laden** klicken, rund 759 MB, danach ist es dauerhaft in diesem +Browser. + +Die Erkennung laeuft **komplett auf dem eigenen Rechner**. Aufnahmen verlassen +das Geraet nicht und werden nirgends gespeichert. Auf einem anderen Rechner oder +in einem anderen Browser faellt der Download erneut an - er gehoert zum Browser, +nicht zum Benutzerkonto. + +## Wenn etwas nicht klappt + +| Beobachtung | Ursache und Abhilfe | +| --- | --- | +| „Sitzung abgelaufen" | 12 Stunden vorbei, neu anmelden | +| Trefferliste ist leer | *Anzeige ab %* zu hoch oder *Zeitraum* zu kurz | +| Kein Ton bei Treffern | Dashboard offen? Einmal auf der Seite geklickt? Wert *Hinweis ab %* erreicht? | +| Mikrofon-Knopf fehlt | kein Mikrofon oder keine HTTPS-Verbindung | +| Boerse zeigt „Abgelaufen" | Cookies neu importieren | +| „Ein Suchlauf laeuft bereits" | kurz warten, es laeuft nur einer zur Zeit | +| Aendern nicht erlaubt | bei Opportunities darf nur Ersteller, Zustaendiger oder Admin | diff --git a/docs/08-anleitung-entwickler.md b/docs/08-anleitung-entwickler.md new file mode 100644 index 0000000..95e56dd --- /dev/null +++ b/docs/08-anleitung-entwickler.md @@ -0,0 +1,195 @@ +# 08 Anleitung fuer Entwickler + +Fuer jemanden, der das Projekt zum ersten Mal oeffnet. Was man braucht, wie man es +startet, wo man was aendert - 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` haelt 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 + uebernommen wurde. +* Alles auf Deutsch: Bezeichner, Kommentare, Meldungen. Kommentare erklaeren das + **Warum**, nicht das Was. + +## Voraussetzungen + +| | Version | +| --- | --- | +| Node | 20 oder neuer (Server laeuft 20.20) | +| MariaDB | 10.11 oder neuer, erreichbar mit Benutzer und Passwort | +| Browser | Chrome/Edge fuer WebGPU beim Diktat (sonst laeuft 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 laesst sich trotzdem +ansehen - siehe *Oberflaeche pruefen ohne Datenbank* weiter unten. + +## Pruefen + +```bash +npm run lint # = tsc --noEmit, das ist die Wahrheit +npm run build # muss ohne Fehler durchlaufen +``` + +Es gibt **keine automatischen Tests**. Wer etwas aendert, prueft mit `tsc`, einem +Build und einem Blick in die Oberflaeche. + +## 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 + routes/ ein Router je Ressource + middleware/auth.js JWT pruefen, Adminrechte + scanner/ portierter Projekt Scanner (Sequelize, Playwright, OpenAI) +docs/ diese Dokumentation +deploy/ Apache-Vhosts, systemd-Unit, Deployskript +``` + +## Wie eine Anfrage laeuft + +```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 anhaengen, 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 pruefen und normalisieren + R->>D: UPDATE ... ; SELECT (angereichert) + R-->>A: 200 JSON + A-->>K: typisiertes Objekt +``` + +Jede Route liefert den **vollstaendigen, angereicherten Datensatz** zurueck (mit +`created_by_name`, `owner_name`, Ansprechpartnern, Teilnehmern). Das Frontend muss +danach nichts nachladen. + +## Etwas hinzufuegen + +### Neues Feld an einem Kontakt + +1. `server/src/schema.sql`: Spalte mit `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` + ergaenzen - das Statement muss beim naechsten 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 Menuepunkt + +1. `src/types.ts`: Wert in `ViewState` ergaenzen. +2. `src/components/Sidebar.tsx`: Eintrag in `CATEGORY_ITEMS` (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 *Dashboard*, der auf die Ansicht `Scanner` zeigt). + +### Neuer Reiter in den Einstellungen + +1. `src/types.ts`: Wert in `SettingsTab`; nur fuer Admins zusaetzlich 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 noetig. +2. In `server/src/server.js` einhaengen - **vor** dem `/api`-404-Fallback. +3. In `src/lib/api.ts` eine typisierte Funktion ergaenzen. + +## Fallen + +Diese Punkte haben real Zeit gekostet: + +| Falle | Was passiert | +| --- | --- | +| **Reihenfolge in `server.js`** | Der 404-Fallback `app.use('/api', ...)` faengt alles, was danach registriert wird. Neue Router davor einhaengen. | +| **`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 oeffnet 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-Ausdruecke, Werte mit Leerzeichen in Anfuehrungszeichen. | +| **Zeitstempel koennen nur vorziehen** | Ein „vorgealterter" `lastRunTime` verzoegert keinen Cronlauf. Wer verzoegern will, braucht eine eigene Frist. | +| **Sperrfristen bei jedem Start neu** | Frueher schob jedes Deployment den faelligen 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 Loeschen** | Einen Suchagenten loeschen loescht alle seine Treffer mit. Inaktiv setzen ist meist gemeint. | +| **Doppelte Scanner-Laeufe** | Scheduler und *Jetzt scannen* teilen eine Sperre. Ohne sie zahlt man jede KI-Bewertung zweimal. | + +## Oberflaeche pruefen ohne Datenbank + +Fuer Bildschirmfotos und Klickwege genuegt ein Stub: ein kleiner `node:http`-Server, +der `dist/` ausliefert und auf `/api/...` erfundene Daten zurueckgibt. Token vorab +setzen, dann ist die Anmeldung uebersprungen: + +```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 erklaeren 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**, Loeschabfragen ueber `ConfirmDialog` - nie `alert()` + oder `confirm()`. +* **Rechte immer doppelt**: Oberflaeche blendet aus, Backend antwortet mit 403. + +## Deployment + +Siehe [06 Betrieb](06-betrieb.md). Kurzform: lokal bauen, `deploy/deploy.ps1` +laedt `dist/` und `server/src` hoch und startet den Dienst neu. Auf dem Server +wird **nicht** gebaut - dort sind RAM und Swap knapp. diff --git a/docs/09-quellcode.md b/docs/09-quellcode.md new file mode 100644 index 0000000..7761e42 --- /dev/null +++ b/docs/09-quellcode.md @@ -0,0 +1,225 @@ +# 09 Quellcode-Referenz + +Was in welcher Datei steckt und welche Zusagen sie einhaelt. Gedacht zum +Nachschlagen, nicht zum Durchlesen. + +## Backend + +### `server/src/server.js` + +Baut die Express-App. Die **Reihenfolge** ist bedeutsam: + +```mermaid +flowchart TD + J["express.json({ limit: '6mb' })
Avatare kommen als Data-URL"] --> C["cors (nur fuer den Dev-Server)"] + C --> H["/api/health"] + H --> R["/api/auth, /users, /leads,
/connections, /events, /opportunities"] + R --> S["/api/scanner (requireAuth + Router)"] + S --> F["/api → 404 JSON
faengt alles Unbekannte"] + F --> E["Fehlerbehandlung:
400 kaputtes JSON, 413 zu gross, 500 Rest"] +``` + +Beim Start: `initDatabase()` (CRM-Schema, Admin-Seed) → `initScannerDatabase()` +(darf fehlschlagen, dann laeuft das CRM ohne Scanner) → Scheduler, sofern +`SCANNER_SCHEDULER !== 'false'`. + +### `server/src/db.js` + +| Export | Zweck | +| --- | --- | +| `pool` | mysql2-Pool, `connectionLimit: 8`, `dateStrings: true` | +| `initDatabase()` | fuehrt `schema.sql` Statement fuer Statement aus, dann `ensureAdminUser()` | + +`dateStrings: true` ist eine bewusste Entscheidung: Termine kommen als +`YYYY-MM-DD HH:MM:SS` **ohne** Zeitzonenumrechnung an. Der Kalender rechnet mit +Wandzeit; ein `Date`-Objekt aus dem Treiber wuerde beim Serialisieren nach UTC +verschieben. + +`ensureAdminUser()` legt nur an, wenn `crm_users` **leer** ist. Ohne +`ADMIN_PASSWORD` erzeugt es ein Zufallspasswort und schreibt es einmalig ins Log - +im Quelltext steht kein Passwort. + +### `server/src/middleware/auth.js` + +| Export | Zweck | +| --- | --- | +| `signToken(user)` | JWT mit `sub` und `role`, Laufzeit aus `JWT_EXPIRES_IN` (12 h) | +| `requireAuth` | Token pruefen **und** Benutzer frisch aus der DB laden | +| `requireAdmin` | 403, wenn `req.user.role !== 'admin'` | + +Dass `requireAuth` bei jeder Anfrage die Datenbank liest, ist Absicht: ein +deaktivierter oder geloeschter Benutzer ist sofort draussen und nicht erst, wenn +sein Token ablaeuft. `secret()` wirft, wenn `JWT_SECRET` fehlt oder kuerzer als 16 +Zeichen ist - lieber ein harter Startfehler als eine schwache Signatur. + +### `server/src/userFields.js` + +`normalizeColor` und `normalizeAvatar`, benutzt von `routes/auth.js` und +`routes/users.js`. Farben nur als `#rrggbb`, Avatare nur als Bild-Data-URL oder +`http(s)`-Adresse, jeweils laengenbegrenzt. + +### Die CRM-Router + +Alle unter `router.use(requireAuth)`. + +| Router | Endpunkte | Rechte | +| --- | --- | --- | +| `auth.js` | `POST /login`, `GET /me`, `PATCH /me`, `POST /change-password` | `/login` offen, Rest angemeldet | +| `users.js` | `GET /directory`; danach `requireAdmin`: `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | `/directory` fuer alle, Rest Admin | +| `leads.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete | +| `connections.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | jeder Angemeldete | +| `events.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | anlegen jeder; aendern/loeschen nur Ersteller oder Admin | +| `opportunities.js` | `GET /`, `POST /`, `PATCH /:id`, `DELETE /:id` | aendern/loeschen nur Ersteller, Zustaendiger oder Admin | + +Wiederkehrende Muster in diesen Routern: + +* `emptyToNull()` - leere Zeichenketten werden `NULL`, damit „nicht gesetzt" und + „leer" in der Datenbank nicht zwei Zustaende sind. +* `normalizeDate()` prueft echte Kalendertage; der 31. Februar wird abgelehnt und + nicht stillschweigend verschoben. +* `normalizeNumber()` akzeptiert Komma als Dezimaltrennzeichen und begrenzt nach + oben - ein Tagessatz von 10 Millionen ist ein Tippfehler. +* `parseTags()` liest die als Text gespeicherten JSON-Arrays defensiv: kaputter + Inhalt ergibt eine leere Liste, keinen Absturz. +* `GET /login` antwortet bei unbekanntem Benutzer und falschem Passwort + **identisch**, damit sich ueber die API keine Benutzernamen aufzaehlen lassen. +* Der letzte aktive Administrator kann nicht degradiert, deaktiviert oder + geloescht werden. + +### `server/src/routes/scanner.js` + +Die portierte Scanner-API unter `/api/scanner`, vollstaendig in +[03 Projekt Scanner](03-scanner.md#endpunkte) tabelliert. Eigenheiten: + +* `multer` schreibt CV-PDFs nach `UPLOAD_DIR`, Grenze 20 MB. +* `resolveCvPath()` findet eine Datei auch dann, wenn in der Datenbank noch ein + relativer Pfad aus der alten App steht - es zaehlt der Dateiname. +* `GET /agents` laesst `cvText` weg (mehrere Hundert KB pro Agent). +* `SECRET_KEYS` (`openai_api_key`, `smtp_pass`) werden als `********` + ausgeliefert; kommt dieser Wert oder ein leeres Feld zurueck, bleibt der + gespeicherte Wert stehen. +* `normalizeCookie()` biegt Exporte von Cookie-Editor auf das zurecht, was + Playwright akzeptiert (`sameSite`, `expirationDate` → `expires`). + +### `server/src/scanner/` + +| Datei | Inhalt | +| --- | --- | +| `db/config.js` | Sequelize-Instanz, eigene Datenbank, `underscored: true` | +| `db/ensureDatabase.js` | `CREATE DATABASE IF NOT EXISTS`, Name gegen Zeichenklasse geprueft | +| `models/index.js` | `initScannerDatabase()`: verbinden, `sync()`, Portale und Standardeinstellungen seeden | +| `models/Agent.js` | `cvText`, `cvFilePath`, `keywords`, `excludeKeywords`, `isActive` | +| `models/Project.js` | Unique auf `(portal, portal_project_id)`, `updatedAt: false` | +| `models/Match.js` | Unique auf `(agent_id, project_id)`, `pros`/`cons` als JSON-Text mit Getter/Setter | +| `models/Portal.js` | Zugangsdaten, `sessionState`, `connectionStatus` | +| `models/Setting.js` | Schluessel-Wert mit `getVal`/`setVal` | +| `services/scraper.js` | Playwright je Portal; unveraendert aus der alten App | +| `services/matchingEngine.js` | OpenAI `gpt-4o-mini`, `response_format: json_object`, `temperature: 0.1` | +| `services/pipeline.js` | ein Lauf: scrapen, speichern, bewerten, benachrichtigen | +| `services/scheduler.js` | node-cron jede Minute, Zeitfenster, Sperrfrist, `last_scan_at` | +| `services/cleanup.js` | Projekte aelter als 7 Tage loeschen (Treffer kaskadieren) | +| `services/pdfParser.js` | `pdf-parse`, doppelte Leerzeilen raus | +| `utils/crypto.js` | AES ueber `crypto-js`; `decryptWith()` fuer fremde Schluessel | +| `uploadDir.js` | `UPLOAD_DIR`, `ensureUploadDir()`, `resolveCvPath()` | +| `importLegacy.js` | einmalige Uebernahme aus der alten Datenbank | + +Zusagen, auf die sich der Rest verlaesst: + +* `runMatchingPipeline()` laeuft **nie zweimal gleichzeitig** und gibt `false` + zurueck, wenn schon einer laeuft. `isPipelineRunning()` fragt den Zustand ab. +* Der Zeitpunkt landet im `finally` in `settings.last_scan_at` - auch nach einem + Abbruch, damit sich ein gescheiterter Lauf nicht sofort wiederholt. +* `cleanup.js` benutzt `sequelize.col('created_at')` statt des Attributnamens: + `Project` benennt die Spalte um, und in `where` greift die Zuordnung nicht. + +## Frontend + +### `src/lib/api.ts` + +| Export | Zweck | +| --- | --- | +| `apiRequest(path, options)` | zentraler Fetch: Token, JSON-Header, 401-Behandlung, Fehlermeldung aus dem Body | +| `getToken` / `setToken` / `clearToken` | Token im `localStorage` (`perinco_crm_token`) | +| `setUnauthorizedHandler` | wird bei 401 gerufen; `AuthProvider` haengt sich hier ein | +| `ApiError` | Fehler mit `status` | +| `authApi`, `usersApi`, `leadsApi`, `opportunitiesApi`, `eventsApi`, `connectionsApi` | typisierte Endpunkte | + +Zwei Feinheiten: Der `Content-Type` wird **nicht** gesetzt, wenn der Body +`FormData` ist. Und ein Netzwerkfehler wird zu `ApiError(status 0)` mit +„Server nicht erreichbar" - das Frontend muss `fetch`-Ausnahmen nicht kennen. + +### `src/lib/scannerApi.ts` + +Typen (`ScannerAgent`, `ScannerPortal`, `ScannerProject`, `ScannerMatch`, +`ScannerSettings`, `ScannerStatus`), das Objekt `scannerApi` mit allen Aufrufen, +`SCANNER_SETTINGS_DEFAULTS`, `SECRET_MASK` und `openAgentCv()`. + +Die Feldnamen sind absichtlich die der Scanner-Datenbank (camelCase aus +Sequelize), inklusive der Grossbuchstaben `Agent` und `Project` an den Joins - +damit bleibt der Vergleich mit der alten App moeglich. Nur `ScannerProject` +traegt beide Schreibweisen fuer `created_at`, weil das Modell die Spalte selbst +umbenennt. + +### Kontexte + +| Datei | Haelt | Eigenheit | +| --- | --- | --- | +| `auth.tsx` | Benutzer, `isAdmin`, `login`, `logout`, `updateUser` | prueft beim Start das gespeicherte Token gegen `/auth/me` | +| `theme.tsx` | hell/dunkel/System | Wahl in `localStorage`, Doppelklick loest sie wieder | +| `navigation.tsx` | `currentView`, `goTo(view, { tab })` | erspart es, Callbacks durch vier Ebenen zu reichen | +| `whisper.tsx` | Modellstatus, Worker, Fortschritt, `transcribe` | ein Worker fuer die ganze App | + +### Whisper + +| Datei | Inhalt | +| --- | --- | +| `whisper.tsx` | Konstanten (`WHISPER_MODEL`, `WHISPER_DTYPE`, `WHISPER_LANGUAGE`), Provider, Fristen, Haenger-Erkennung | +| `whisper.worker.ts` | `WorkerRequest`/`WorkerResponse`, laedt und erkennt | +| `whisperModels.ts` | `fetchModelSize()` holt die echte Groesse von Hugging Face | +| `audio.ts` | `toWhisperAudio()` - Blob zu 16-kHz-Mono-PCM | +| `dictation.tsx` | `useRecorder()` - Mikrofon, Dauer, Abbruch | + +Warum genau diese Konstanten festgelegt sind, steht in +[04 Diktat](04-diktat.md#warum-keine-auswahl). + +### Gemeinsame Bausteine + +| Datei | Inhalt | +| --- | --- | +| `components/leads/leadUi.tsx` | `inputClass`, `labelClass`, `StatusBadge`, `Avatar`, Statusfarben | +| `components/scanner/scannerUi.tsx` | `cardClass`, Buttonklassen, `MatchBadge`, `MatchStatusBadge`, `PortalStatusPill`, `formatDateTime`, `projectTimestamp` | +| `components/ui/Modal.tsx` | `Modal` (Escape schliesst) und `ConfirmDialog` | +| `components/Toast.tsx` | `useToast().addToast(text, kind)` mit `success`, `error` oder `info` | +| `components/ui/DictationField.tsx` | Mikrofon-Knopf, Hinweisbox, Textfeld mit Diktat | +| `lib/imageCompress.ts` | `compressImage()` auf 256 px, WebP → JPEG → PNG | + +### `src/types.ts` + +Neben den Interfaces stehen hier die Rechenregeln, damit Frontend und Anzeige +nicht auseinanderlaufen: + +| Export | Zweck | +| --- | --- | +| `ViewState`, `SettingsTab`, `ADMIN_SETTINGS_TABS`, `SCANNER_VIEWS` | Ansichten und Reiter | +| `USER_COLORS`, `userColor()` | Farbe gesetzt, sonst stabil aus der Benutzer-Id | +| `opportunityVolume()`, `weightedVolume()`, `formatEuro()` | Tagessatz x Personentage, gewichtet, formatiert | +| `OPPORTUNITY_STAGES`, `OPEN_STAGES`, `LEAD_STATUSES`, `CONNECTION_TYPES` | die Aufzaehlungen der Datenbank | +| `eventColor()`, `leadFullName()`, `leadInitials()`, `contactFullName()` | Anzeigelogik | + +## Datenfluss im Frontend + +```mermaid +flowchart TD + A["App.tsx: loadData()"] -->|"Promise.all"| B["5 Endpunkte parallel"] + B --> C["leads, connections, events, users, opportunities"] + C --> D["als Props in die Ansichten"] + D --> E["Dialog speichert"] + E --> F["reload() = loadData(true)"] + F -->|"still, ohne Ladeanzeige"| C + + G["Scanner-Ansichten"] -->|"eigener 30-s-Takt"| H["/api/scanner"] +``` + +Die fuenf CRM-Endpunkte laufen **parallel**, nicht nacheinander - sonst addieren +sich fuenf Rundreisen beim Anmelden. Die Scanner-Ansichten haengen bewusst nicht +an `loadData`: andere Datenbank, andere Auffrischung. diff --git a/docs/10-glossar.md b/docs/10-glossar.md new file mode 100644 index 0000000..de251be --- /dev/null +++ b/docs/10-glossar.md @@ -0,0 +1,47 @@ +# 10 Glossar + +Begriffe, die in der Oberflaeche, im Code und in den Tabellen vorkommen. Die +englischen Fachbegriffe sind bewusst nicht eingedeutscht - sie stehen so auch im +Quelltext. + +| Begriff | Bedeutung | +| --- | --- | +| **Agent** / Suchagent | Ein Beraterprofil im Scanner: Name, Lebenslauf als PDF, Suchbegriffe, Ausschluss-Begriffe. Tabelle `agents`. Nicht zu verwechseln mit einem Benutzerkonto. | +| **Ausschluss-Begriff** | Wort, das ein Projekt fuer diesen Agenten sofort aussortiert - noch **vor** der KI-Bewertung, spart also Kosten. Typisch: „Arbeitnehmerueberlassung". | +| **Benutzer** | Konto fuer die Anmeldung, Rolle `admin` oder `user`. Tabelle `crm_users`. | +| **Boerse** / Portal | Projektplattform, die der Scanner durchsucht. Oeffentlich (Hays, Etengo) oder geschuetzt (Freelance.de, Freelancermap.de). Tabelle `portals`. | +| **Cookie-Import** | Uebertragen einer angemeldeten Browsersitzung auf den Scraper, per JSON-Export der Erweiterung *Cookie-Editor*. Landet als `session_state` am Portal. | +| **Kontakt** / Lead | Person im Netzwerk: Firma, Position, Status, Verbindungen. Tabelle `crm_leads`. Im Code durchgaengig `lead`. | +| **Match** / Treffer | Die Bewertung **eines** Projekts fuer **einen** Agenten: Prozentwert, Pro-Liste, Contra-Liste, Status. Tabelle `matches`. Ein Projekt erzeugt so viele Treffer, wie es aktive Agenten gibt. | +| **Opportunity** | Geschaeft in Arbeit: Phase, Tagessatz, Laufzeit, Wahrscheinlichkeit, Ansprechpartner. Tabelle `crm_opportunities`. | +| **Phase** / Stage | Fortschritt einer Opportunity: Neu, Qualifizierung, Angebot, Verhandlung, Gewonnen, Verloren. | +| **Pipeline** | Zwei Bedeutungen, je nach Zusammenhang: die Kanban-Ansicht der Opportunities **oder** der Suchlauf des Scanners (`services/pipeline.js`). | +| **Projekt** | Ein von einer Boerse gescrapter Auftrag. Tabelle `projects`, Aufbewahrung 7 Tage. | +| **Scraper** | Der Teil, der mit einem echten Browser (Playwright/Chromium) die Suchseiten der Boersen abruft. | +| **Session** (Portal) | Gespeicherte Cookies einer angemeldeten Boersen-Sitzung, im Playwright-Format `storageState`. | +| **Sitzung** (CRM) | Die eigene Anmeldung, als JWT im `localStorage`, 12 Stunden gueltig. | +| **Suchintervall** | Abstand zwischen zwei automatischen Suchlaeufen, in Minuten. Einstellung `search_interval`. | +| **Schwellenwert Anzeige** | `display_threshold` - Treffer darunter bleiben im Dashboard ausgeblendet. | +| **Schwellenwert Benachrichtigung** | `match_threshold` - ab hier gibt es Signalton und E-Mail. | +| **Verbindung** | Kante zwischen zwei Kontakten im Netzwerkdiagramm, mit Art und Staerke. Tabelle `crm_lead_connections`. | +| **Volumen** | Tagessatz x Personentage. **Gewichtet** heisst zusaetzlich mit der Abschlusswahrscheinlichkeit multipliziert. | +| **Whisper** | Das Spracherkennungsmodell fuer das Diktat. Laeuft im Browser, nicht auf dem Server. | +| **Zustaendiger** / Owner | Benutzer, dem ein Kontakt oder eine Opportunity zugeordnet ist. Darf sie aendern, auch wenn er sie nicht angelegt hat. | + +## Statuswerte im Scanner + +| Wert in der DB | in der Oberflaeche | Bedeutung | +| --- | --- | --- | +| `new` | Neu | noch nicht angesehen | +| `reviewed` | Gesehen | geprueft, noch keine Bewerbung | +| `applied` | Beworben | Bewerbung ist raus | +| `rejected` | Abgelehnt | passt nicht; erscheint nur unter *Unpassend* | + +## Portal-Status + +| Wert | Bedeutung | +| --- | --- | +| `connected` | Verbindung steht, letzte Pruefung erfolgreich | +| `expired` | Session abgelaufen oder CAPTCHA - neuer Cookie-Import faellig | +| `error` | Portal nicht erreichbar oder Seitenaufbau geaendert | +| `disconnected` | nie verbunden; bei oeffentlichen Portalen heisst das nur „bereit" | diff --git a/docs/README.md b/docs/README.md index 926c72e..62bd710 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,6 +3,11 @@ Technische Dokumentation mit Funktionszeichnungen. Die Diagramme sind Mermaid und werden von Gitea direkt gerendert. +**Neu hier?** Wer die Anwendung *benutzen* will, faengt bei +[07 Anleitung fuer Benutzer](07-anleitung-benutzer.md) an. Wer daran *entwickeln* +will, bei [08 Anleitung fuer Entwickler](08-anleitung-entwickler.md). Beide setzen +keine Vorkenntnisse voraus. + | Kapitel | Inhalt | | --- | --- | | [01 Architektur](01-architektur.md) | Topologie, Prozesse, Ports, Anfrageweg, Rollen | @@ -11,9 +16,13 @@ werden von Gitea direkt gerendert. | [04 Diktat](04-diktat.md) | Spracherkennung im Browser, Modell-Lebenszyklus | | [05 Frontend](05-frontend.md) | Ansichten, Menue, Komponentenkarte, Zustand | | [06 Betrieb](06-betrieb.md) | Deployment, Umgebungsvariablen, Dienste, Stoerungssuche | +| [07 Anleitung fuer Benutzer](07-anleitung-benutzer.md) | Bedienung aller Bereiche, Schritt fuer Schritt | +| [08 Anleitung fuer Entwickler](08-anleitung-entwickler.md) | Einrichten, erweitern, bekannte Fallen | +| [09 Quellcode-Referenz](09-quellcode.md) | jede Datei, ihre Exporte und Zusagen | +| [10 Glossar](10-glossar.md) | Agent, Match, Lead, Opportunity, Status- und Portalwerte | -Die Anwendungsdokumentation - Bedienung, Designentscheidungen, Messwerte zur -Spracherkennung - steht im [README des Projekts](../README.md). +Die Designentscheidungen und die Messwerte zur Spracherkennung stehen im +[README des Projekts](../README.md). ## Das Ganze in einem Bild