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:
co-authored by
Claude Opus 5
parent
e58b94f007
commit
4d90f14d6b
@@ -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 |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user