docs: Anleitungen fuer Benutzer und Entwickler, Quellcode-Referenz, Glossar

Die bisherige Dokumentation beschrieb die Architektur, half aber niemandem, der
die Anwendung zum ersten Mal oeffnet. Dazu jetzt vier Kapitel:

- 07 Anleitung fuer Benutzer: was die Anwendung tut, jeder Bereich Schritt fuer
  Schritt, warum der Signalton nur im Dashboard klingelt, was Loeschen eines
  Suchagenten mitnimmt
- 08 Anleitung fuer Entwickler: einrichten, pruefen, Feld/Menuepunkt/Reiter/
  Endpunkt hinzufuegen, dazu eine Tabelle der Fallen, die real Zeit gekostet
  haben (Routen-Reihenfolge, FormData, Leerzeichen in Pfaden, Zeitstempel
  koennen nur vorziehen, Kaskaden beim Loeschen)
- 09 Quellcode-Referenz: jede Datei mit ihren Exporten und den Zusagen, auf die
  sich der Rest verlaesst
- 10 Glossar: Agent, Match, Lead, Opportunity und die Statuswerte der Datenbank

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sascha Wolf
2026-08-06 15:34:00 +02:00
co-authored by Claude Opus 5
parent e58b94f007
commit 4d90f14d6b
6 changed files with 739 additions and 3 deletions
+4 -1
View File
@@ -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
+257
View File
@@ -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 |
+195
View File
@@ -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 `<a href>` 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.
+225
View File
@@ -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' })<br/>Avatare kommen als Data-URL"] --> C["cors (nur fuer den Dev-Server)"]
C --> H["/api/health"]
H --> R["/api/auth, /users, /leads,<br/>/connections, /events, /opportunities"]
R --> S["/api/scanner (requireAuth + Router)"]
S --> F["/api → 404 JSON<br/>faengt alles Unbekannte"]
F --> E["Fehlerbehandlung:<br/>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<T>(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.
+47
View File
@@ -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" |
+11 -2
View File
@@ -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