docs: echte Umlaute in der Dokumentation
893 Stellen in Fliesstext, Tabellen und Mermaid-Beschriftungen. Dazu die sichtbaren ss-Fälle (ausschließlich, groß, außen, schließt, heißt, draußen, weiße, anstoßen, Straße). Nicht angetastet, damit die Doku weiter beschreibt, was wirklich in der Datenbank steht: - Codeblöcke und Code-Spans - dort stehen Bezeichner, SQL, Enum-Werte und Dateinamen: `mail_pool`, `'uebernommen'`, `extract.js` - in Mermaid nur der Text in Anführungszeichen; Knotennamen, Pfeile und Werte wie source = 'Mail' bleiben, wie sie sind - `empfaenger` im ER-Diagramm von crm_mail_contact_candidates: die Aufzählung dort nennt die Datenbankwerte, nicht die Anzeige Die Menü-Skizze in der Benutzeranleitung zeigt jetzt die aufgeklappten Kontakte mit "Aus Mails" samt Zähler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8b7ce448a6
commit
7973dc07b1
@@ -1,14 +1,14 @@
|
||||
# perinco CRM
|
||||
|
||||
CRM im perinco Corporate Design. Vite/React-Frontend, Express-Backend, MariaDB.
|
||||
Enthaelt den portierten *Projekt Scanner* und die lesende Anbindung des
|
||||
Vertriebspostfachs `crm@perinco.de` ueber Microsoft Graph.
|
||||
Enthält den portierten *Projekt Scanner* und die lesende Anbindung des
|
||||
Vertriebspostfachs `crm@perinco.de` über Microsoft Graph.
|
||||
|
||||
> **Dokumentation mit Funktionszeichnungen:** [`docs/`](docs/README.md)
|
||||
> — Architektur, Datenmodell, Scanner-Ablauf, Diktat, Frontend, Betrieb,
|
||||
> [Postfachanbindung](docs/11-postfach.md).
|
||||
> Zum Einstieg: [Anleitung fuer Benutzer](docs/07-anleitung-benutzer.md) ·
|
||||
> [Anleitung fuer Entwickler](docs/08-anleitung-entwickler.md) ·
|
||||
> Zum Einstieg: [Anleitung für Benutzer](docs/07-anleitung-benutzer.md) ·
|
||||
> [Anleitung für Entwickler](docs/08-anleitung-entwickler.md) ·
|
||||
> [Quellcode-Referenz](docs/09-quellcode.md)
|
||||
|
||||
## Corporate Identity
|
||||
@@ -17,12 +17,12 @@ Direkt aus dem perinco Logo gesampelt:
|
||||
|
||||
| Rolle | Wert |
|
||||
| --- | --- |
|
||||
| Primaerfarbe (Gruen) | `#6BA04A`, Akzent `#68A33F` |
|
||||
| Sekundaerfarbe (Grau) | `#6E6E6E` |
|
||||
| Primärfarbe (Grün) | `#6BA04A`, Akzent `#68A33F` |
|
||||
| Sekundärfarbe (Grau) | `#6E6E6E` |
|
||||
| Schrift | Avenir LT (wie perinco.de); Fallback Nunito Sans |
|
||||
|
||||
Die Farbskalen liegen als Tailwind-Theme in `src/index.css` (`brand-*`, `ink-*`)
|
||||
sowie die Flaechen `canvas` (Seite) und `surface` (Karten, Leisten, Dialoge).
|
||||
sowie die Flächen `canvas` (Seite) und `surface` (Karten, Leisten, Dialoge).
|
||||
|
||||
## Dark Mode
|
||||
|
||||
@@ -32,21 +32,21 @@ Doppelklick auf den Umschalter stellt das wieder her.
|
||||
|
||||
Umgesetzt ist der Dark Mode **ohne `dark:`-Varianten im Markup**: die Klasse
|
||||
`dark` auf `<html>` tauscht in `src/index.css` nur die Werte der Variablen
|
||||
`canvas`, `surface`, `ink-*`, `brand-*` und der getoenten Statusfarben. Die
|
||||
Komponenten benutzen durchgaengig diese Skalen und folgen damit automatisch.
|
||||
`canvas`, `surface`, `ink-*`, `brand-*` und der getönten Statusfarben. Die
|
||||
Komponenten benutzen durchgängig diese Skalen und folgen damit automatisch.
|
||||
|
||||
Drei Dinge bleiben in beiden Themes gleich und sind deshalb bewusst so gewaehlt:
|
||||
Drei Dinge bleiben in beiden Themes gleich und sind deshalb bewusst so gewählt:
|
||||
|
||||
| Was | Warum |
|
||||
| --- | --- |
|
||||
| `ink-950` | liegt nur unter Dialog-Overlays, muss dunkel bleiben |
|
||||
| `text-white` | steht immer auf farbigen Flaechen (Buttons, Termin-Chips) |
|
||||
| `brand-500` | das CI-Gruen der Buttons; nur die Tints darum kippen |
|
||||
| `text-white` | steht immer auf farbigen Flächen (Buttons, Termin-Chips) |
|
||||
| `brand-500` | das CI-Grün der Buttons; nur die Tints darum kippen |
|
||||
|
||||
Das Netzwerkdiagramm kann im SVG keine Tailwind-Klassen stapeln und benutzt
|
||||
deshalb `var(--color-*)` direkt. Das Logo ist fuer helle Hintergruende gemacht -
|
||||
deshalb `var(--color-*)` direkt. Das Logo ist für helle Hintergründe gemacht -
|
||||
die Utility `.logo-adaptive` dreht im Dark Mode die Helligkeit und holt per
|
||||
`hue-rotate` das Gruen zurueck.
|
||||
`hue-rotate` das Grün zurück.
|
||||
|
||||
Ein Inline-Skript in `index.html` setzt die Klasse vor dem ersten Frame, sonst
|
||||
blitzt beim Laden die helle Variante auf.
|
||||
@@ -62,11 +62,11 @@ Browser ──► Apache (:443/:80, pcrm.perinco.de)
|
||||
└──► Microsoft Graph (crm@perinco.de, nur lesend)
|
||||
```
|
||||
|
||||
Der Scanner haengt unter `/api/scanner`, die Postfachanbindung unter `/api/mail` -
|
||||
Der Scanner hängt unter `/api/scanner`, die Postfachanbindung unter `/api/mail` -
|
||||
beide brauchen damit **keine** eigene Apache-Regel.
|
||||
|
||||
Der Browser spricht **nie** direkt mit MariaDB - deshalb das Express-Backend.
|
||||
Das Backend bindet ausschliesslich auf `127.0.0.1` und ist von aussen nur ueber
|
||||
Das Backend bindet ausschließlich auf `127.0.0.1` und ist von außen nur über
|
||||
den Apache-Proxy erreichbar.
|
||||
|
||||
### Datenmodell
|
||||
@@ -74,25 +74,25 @@ den Apache-Proxy erreichbar.
|
||||
| Tabelle | Inhalt |
|
||||
| --- | --- |
|
||||
| `crm_users` | Benutzer, Rolle `admin` oder `user`, bcrypt-Hash, `color` (Kalenderfarbe), `avatar` (Foto) |
|
||||
| `crm_leads` | Kontakte inkl. `created_by` (Ersteller) und `owner_id` (zustaendig); `mail_pool = 1` ist der Vorbereich „Aus Mails" aus dem Postfachabgleich |
|
||||
| `crm_leads` | Kontakte inkl. `created_by` (Ersteller) und `owner_id` (zuständig); `mail_pool = 1` ist der Vorbereich „Aus Mails" aus dem Postfachabgleich |
|
||||
| `crm_opportunities` | Vertriebschancen: Phase, Tagessatz, Laufzeit, letzte Interaktion, Wahrscheinlichkeit |
|
||||
| `crm_opportunity_contacts` | Ansprechpartner je Opportunity (n:m auf `crm_leads`) mit Rolle |
|
||||
| `crm_lead_connections` | Wer steht mit wem in Kontakt; Paar aufsteigend gespeichert, Unique-Key verhindert Duplikate |
|
||||
| `crm_events` | Termine des gemeinsamen Teamkalenders; `description` kann Mention-Tokens enthalten |
|
||||
| `crm_event_participants` | Teilnehmer je Termin (n:m), Primaerschluessel verhindert Doppeleintraege |
|
||||
| `crm_event_participants` | Teilnehmer je Termin (n:m), Primärschlüssel verhindert Doppeleinträge |
|
||||
|
||||
### Rollen
|
||||
|
||||
* `admin` - sieht den Menuepunkt Einstellungen inkl. Nutzerverwaltung
|
||||
* `user` - sieht Einstellungen nicht; die API weist Zugriffe zusaetzlich mit 403 ab
|
||||
* `admin` - sieht den Menüpunkt Einstellungen inkl. Nutzerverwaltung
|
||||
* `user` - sieht Einstellungen nicht; die API weist Zugriffe zusätzlich mit 403 ab
|
||||
|
||||
Ein Sicherheitsnetz im Backend verhindert, dass der letzte aktive Administrator
|
||||
degradiert, deaktiviert oder geloescht wird.
|
||||
degradiert, deaktiviert oder gelöscht wird.
|
||||
|
||||
Ausnahme von der Admin-Regel ist `GET /api/users/directory`: die Benutzerliste
|
||||
brauchen alle Rollen fuer Netzwerkdiagramm, Zustaendigkeiten, @-Mentions,
|
||||
brauchen alle Rollen für Netzwerkdiagramm, Zuständigkeiten, @-Mentions,
|
||||
Teilnehmerauswahl und Farben. Passwort-Hashes liefert der Endpunkt nie.
|
||||
Termine darf jeder anlegen; aendern und loeschen nur der Ersteller oder ein
|
||||
Termine darf jeder anlegen; ändern und löschen nur der Ersteller oder ein
|
||||
Administrator.
|
||||
|
||||
## Struktur
|
||||
@@ -173,40 +173,40 @@ deploy/
|
||||
|
||||
## Opportunities
|
||||
|
||||
Erster Reiter im Menue und Startansicht. Drei Ansichten wie bei den Kontakten,
|
||||
Erster Reiter im Menü und Startansicht. Drei Ansichten wie bei den Kontakten,
|
||||
umgeschaltet oben rechts:
|
||||
|
||||
| Ansicht | Inhalt |
|
||||
| --- | --- |
|
||||
| **Kachel** | Karten mit Volumen, Tagessatz, Laufzeit, Zeitraum, letzter Interaktion und naechstem Schritt |
|
||||
| **Kachel** | Karten mit Volumen, Tagessatz, Laufzeit, Zeitraum, letzter Interaktion und nächstem Schritt |
|
||||
| **Liste** | sortierbare Tabelle, Spaltenkopf klicken kehrt die Richtung um |
|
||||
| **Pipeline** | Kanban nach Phase, Karten per Drag & Drop verschiebbar |
|
||||
|
||||
Die dritte Ansicht ist bewusst eine Pipeline statt eines Netzwerks: bei einer
|
||||
Vertriebschance interessiert der Fortschritt durch die Phasen. Wer nicht ziehen
|
||||
mag oder auf dem Tablet arbeitet, aendert die Phase ueber das Auswahlfeld auf der
|
||||
mag oder auf dem Tablet arbeitet, ändert die Phase über das Auswahlfeld auf der
|
||||
Karte.
|
||||
|
||||
**Felder:** Titel, Firma, Projektbeschreibung (in Kachel und Liste
|
||||
aufklappbar), Phase, Abschlusswahrscheinlichkeit, Tagessatz, Laufzeit in
|
||||
Personentagen (halbe Tage moeglich), Projektstart und -ende, letzte Interaktion
|
||||
mit Notiz, naechster Schritt, Tags, interne Notizen, Zustaendigkeit sowie
|
||||
Personentagen (halbe Tage möglich), Projektstart und -ende, letzte Interaktion
|
||||
mit Notiz, nächster Schritt, Tags, interne Notizen, Zuständigkeit sowie
|
||||
beliebig viele Ansprechpartner aus den Kontakten - jeder mit eigener Rolle.
|
||||
|
||||
**Volumen** rechnet die App aus Tagessatz x Personentagen, **gewichtet** zusaetzlich
|
||||
**Volumen** rechnet die App aus Tagessatz x Personentagen, **gewichtet** zusätzlich
|
||||
mit der Wahrscheinlichkeit. Beides steht live im Formular, pro Karte, als Summe
|
||||
ueber der Liste, je Pipeline-Spalte und in der Kopfzeile.
|
||||
über der Liste, je Pipeline-Spalte und in der Kopfzeile.
|
||||
|
||||
**Sortieren** geht doppelt: ueber das Auswahlfeld in der Steuerleiste (wirkt in
|
||||
allen Ansichten) und in der Liste ueber die Spaltenkoepfe. Geld- und Datumsfelder
|
||||
**Sortieren** geht doppelt: über das Auswahlfeld in der Steuerleiste (wirkt in
|
||||
allen Ansichten) und in der Liste über die Spaltenköpfe. Geld- und Datumsfelder
|
||||
starten absteigend, Text aufsteigend; leere Werte landen immer hinten.
|
||||
|
||||
Aendern und loeschen darf der Ersteller, der Zustaendige und jeder Administrator.
|
||||
Ändern und löschen darf der Ersteller, der Zuständige und jeder Administrator.
|
||||
|
||||
## Projekt Scanner
|
||||
|
||||
Der eigenstaendige *Projekt Scanner* ist vollstaendig hier eingezogen und soll
|
||||
die alte App ersetzen. Die laeuft weiter unter `scanner.perinco.de`:
|
||||
Der eigenständige *Projekt Scanner* ist vollständig hier eingezogen und soll
|
||||
die alte App ersetzen. Die läuft weiter unter `scanner.perinco.de`:
|
||||
|
||||
```
|
||||
/var/www/Projekt Scanner/backend Express auf 0.0.0.0:5005
|
||||
@@ -215,24 +215,24 @@ systemd-Unit: project-scanner-backend (nicht "project-scanner")
|
||||
```
|
||||
|
||||
Bis zur Ablosung laufen **beide parallel und beide voll funktional**: eigene
|
||||
Datenbank, eigener Verschluesselungsschluessel, eigenes Upload-Verzeichnis,
|
||||
eigener Scheduler. Die alte App bleibt unveraendert - weder ihre Dateien noch
|
||||
Datenbank, eigener Verschlüsselungsschlüssel, eigenes Upload-Verzeichnis,
|
||||
eigener Scheduler. Die alte App bleibt unverändert - weder ihre Dateien noch
|
||||
ihre Datenbank werden angefasst.
|
||||
|
||||
Menuepunkt **Scanner** links; ein Klick klappt die Reiter der alten App darunter
|
||||
Menüpunkt **Scanner** links; ein Klick klappt die Reiter der alten App darunter
|
||||
auf:
|
||||
|
||||
| Reiter | Inhalt |
|
||||
| --- | --- |
|
||||
| **Dashboard** | Kennzahlen, Filter (Zeitraum, Agent, Schwellenwerte), sortierbare Match-Tabelle, *Jetzt scannen* |
|
||||
| **Unpassend** | als „passt nicht" markierte Treffer; zurueckholen oder loeschen |
|
||||
| **Unpassend** | als „passt nicht" markierte Treffer; zurückholen oder löschen |
|
||||
| **Suchagenten** | Beraterprofile: Name, E-Mail, Keywords, Ausschlussbegriffe, CV-PDF, aktiv/inaktiv |
|
||||
| **Boersen-Konten** | Verbindungsstatus je Portal, Zugangsdaten, Cookie-Import, Verbindungstest |
|
||||
| **Börsen-Konten** | Verbindungsstatus je Portal, Zugangsdaten, Cookie-Import, Verbindungstest |
|
||||
|
||||
Die **Einstellungen** des Scanners stehen nicht im Untermenue, sondern als Reiter
|
||||
*Scanner* unter *Einstellungen* - also nur fuer Rolle `admin`. Die Reiter selbst
|
||||
Die **Einstellungen** des Scanners stehen nicht im Untermenü, sondern als Reiter
|
||||
*Scanner* unter *Einstellungen* - also nur für Rolle `admin`. Die Reiter selbst
|
||||
sieht jede Rolle. Anzeige- und Benachrichtigungs-Schwellenwert lassen sich an
|
||||
beiden Stellen aendern; es ist derselbe Wert in der Datenbank.
|
||||
beiden Stellen ändern; es ist derselbe Wert in der Datenbank.
|
||||
|
||||
### Was portiert wurde
|
||||
|
||||
@@ -241,60 +241,60 @@ beiden Stellen aendern; es ist derselbe Wert in der Datenbank.
|
||||
| Sequelize-Modelle (`agents`, `portals`, `projects`, `matches`, `settings`) | `server/src/scanner/models/` |
|
||||
| Scraper (Playwright), Matching (OpenAI `gpt-4o-mini`), Pipeline, Scheduler, Cleanup, PDF-Parser | `server/src/scanner/services/` |
|
||||
| REST-API | `server/src/routes/scanner.js`, gemountet als `/api/scanner` |
|
||||
| Oberflaeche | `src/components/scanner/`, in Tailwind und CI-Farben neu gebaut |
|
||||
| Uebernahme aus der alten DB | `server/src/scanner/importLegacy.js` (`npm run scanner:import`) |
|
||||
| Oberfläche | `src/components/scanner/`, in Tailwind und CI-Farben neu gebaut |
|
||||
| Übernahme aus der alten DB | `server/src/scanner/importLegacy.js` (`npm run scanner:import`) |
|
||||
|
||||
Drei bewusste Unterschiede zum Original:
|
||||
|
||||
1. **Anmeldung** Die HTTP-Basic-Auth des Scanners entfaellt - alle Endpunkte
|
||||
1. **Anmeldung** Die HTTP-Basic-Auth des Scanners entfällt - alle Endpunkte
|
||||
liegen hinter `requireAuth`, also der CRM-Sitzung.
|
||||
2. **CV-PDFs** kommen ueber `GET /api/scanner/agents/:id/cv` statt aus einem
|
||||
2. **CV-PDFs** kommen über `GET /api/scanner/agents/:id/cv` statt aus einem
|
||||
statischen Verzeichnis: ein Link im neuen Tab kann kein Bearer-Token
|
||||
mitschicken, deshalb holt das Frontend die Datei und oeffnet sie als Blob.
|
||||
3. **Meldungen** laufen als Toast statt `alert()`, Loeschabfragen als Dialog.
|
||||
mitschicken, deshalb holt das Frontend die Datei und öffnet sie als Blob.
|
||||
3. **Meldungen** laufen als Toast statt `alert()`, Löschabfragen als Dialog.
|
||||
|
||||
### Parallelbetrieb - alles doppelt
|
||||
|
||||
Beide Scanner laufen **gleichzeitig und funktional**, ohne sich zu beruehren.
|
||||
Dafuer ist hier jede geteilte Ressource verdoppelt:
|
||||
Beide Scanner laufen **gleichzeitig und funktional**, ohne sich zu berühren.
|
||||
Dafür ist hier jede geteilte Ressource verdoppelt:
|
||||
|
||||
| | alte App | diese Instanz |
|
||||
| --- | --- | --- |
|
||||
| Datenbank | `project_scanner` | `perinco_scanner` (eigene Tabellen, gleiche Namen) |
|
||||
| Verschluesselung | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` - eigener Wert |
|
||||
| Verschlüsselung | `ENCRYPTION_KEY` | `SCANNER_ENCRYPTION_KEY` - eigener Wert |
|
||||
| CV-PDFs | `/var/www/Projekt Scanner/backend/uploads` | `SCANNER_UPLOAD_DIR` = `/var/www/perinco_CRM/server/uploads` |
|
||||
| Scheduler | eigener Cron im alten Dienst | `SCANNER_SCHEDULER`, Standard `true` |
|
||||
| Aufraeumen | 03:00 | 03:30 |
|
||||
| Aufräumen | 03:00 | 03:30 |
|
||||
|
||||
Die Scanner-Datenbank legt das Backend beim Start **selbst an**
|
||||
(`CREATE DATABASE IF NOT EXISTS`), die Tabellen kommen aus `sequelize.sync()`,
|
||||
Portale und Standardeinstellungen werden geseedet. Es genuegt also, das Backend
|
||||
Portale und Standardeinstellungen werden geseedet. Es genügt also, das Backend
|
||||
zu starten.
|
||||
|
||||
| Variable | Bedeutung |
|
||||
| --- | --- |
|
||||
| `SCANNER_DB_NAME` | eigene Datenbank, Standard `perinco_scanner` - **nicht** auf `project_scanner` zeigen lassen |
|
||||
| `SCANNER_DB_HOST/_PORT/_USER/_PASSWORD` | leer = CRM-Zugangsdaten |
|
||||
| `SCANNER_ENCRYPTION_KEY` | eigener Schluessel fuer die Portal-Passwoerter; ohne ihn wird ein bekannter Ersatzschluessel benutzt und beim Start gewarnt |
|
||||
| `SCANNER_ENCRYPTION_KEY` | eigener Schlüssel für die Portal-Passwörter; ohne ihn wird ein bekannter Ersatzschlüssel benutzt und beim Start gewarnt |
|
||||
| `SCANNER_UPLOAD_DIR` | eigenes CV-Verzeichnis; leer = `<server>/uploads` |
|
||||
| `SCANNER_SCHEDULER` | Hintergrund-Scraper; Standard an, nur `false` schaltet ab |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz fuer den **allerersten** Lauf, Standard `30`. Danach traegt `settings.last_scan_at` den Zeitplan ueber Neustarts hinweg |
|
||||
| `SCANNER_SCHEDULER_OFFSET` | Minuten Versatz für den **allerersten** Lauf, Standard `30`. Danach trägt `settings.last_scan_at` den Zeitplan über Neustarts hinweg |
|
||||
|
||||
> **Kosten:** Beide Instanzen bewerten dieselben Projekte unabhaengig
|
||||
> **Kosten:** Beide Instanzen bewerten dieselben Projekte unabhängig
|
||||
> voneinander - die OpenAI-Kosten fallen also doppelt an, solange die alte App
|
||||
> laeuft. Das ist der Preis des Parallelbetriebs, kein Fehler. Wer nur zuschauen
|
||||
> läuft. Das ist der Preis des Parallelbetriebs, kein Fehler. Wer nur zuschauen
|
||||
> will, setzt `SCANNER_SCHEDULER=false`; der Knopf *Jetzt scannen* funktioniert
|
||||
> dann weiter.
|
||||
|
||||
Der **Zeitversatz** ist nicht Kosmetik: beide Instanzen benutzen dieselben
|
||||
Portal-Sessions (aus derselben Cookie-Quelle) und wuerden beim ersten Start
|
||||
Portal-Sessions (aus derselben Cookie-Quelle) und würden beim ersten Start
|
||||
gleichzeitig losziehen - das provoziert Rate-Limits und CAPTCHAs bei
|
||||
Freelancermap und Freelance.de. `SCANNER_SCHEDULER_OFFSET=30` verschiebt den
|
||||
ersten Lauf um 30 Minuten, danach laufen beide im eigenen Intervall verschraenkt.
|
||||
ersten Lauf um 30 Minuten, danach laufen beide im eigenen Intervall verschränkt.
|
||||
|
||||
Ab dem zweiten Start traegt `settings.last_scan_at` den Zeitplan - die Frist
|
||||
Ab dem zweiten Start trägt `settings.last_scan_at` den Zeitplan - die Frist
|
||||
greift dann nicht mehr. Sonst begann sie bei jedem Deployment von vorn und ein
|
||||
faelliger Lauf konnte beliebig lange ausfallen (siehe
|
||||
fälliger Lauf konnte beliebig lange ausfallen (siehe
|
||||
[docs/03-scanner.md](docs/03-scanner.md#wann-laeuft-er)).
|
||||
|
||||
Innerhalb dieser Instanz kann **nur ein Lauf** gleichzeitig arbeiten: Scheduler
|
||||
@@ -303,11 +303,11 @@ Aufruf bekommt 409 und den Hinweis *„Ein Suchlauf läuft bereits"*. Das Dashbo
|
||||
zeigt oben links den Zustand - automatische Suche aktiv, nur manuelle Suche,
|
||||
pausiert, Suchlauf läuft - samt Namen der Datenbank.
|
||||
|
||||
### Daten aus der alten App uebernehmen
|
||||
### Daten aus der alten App übernehmen
|
||||
|
||||
Damit die neue Instanz nicht bei Null anfaengt, holt ein Skript Agenten, Portale
|
||||
und Einstellungen herueber. Es liest die alte Datenbank nur, geschrieben wird
|
||||
ausschliesslich in `perinco_scanner`:
|
||||
Damit die neue Instanz nicht bei Null anfängt, holt ein Skript Agenten, Portale
|
||||
und Einstellungen herüber. Es liest die alte Datenbank nur, geschrieben wird
|
||||
ausschließlich in `perinco_scanner`:
|
||||
|
||||
```bash
|
||||
cd /var/www/perinco_CRM/server
|
||||
@@ -319,42 +319,42 @@ npm run scanner:import -- --force # vorhandene Zieldaten vorher loesche
|
||||
|
||||
Quelle kommt aus `LEGACY_*` in `server/.env`:
|
||||
|
||||
| Variable | Wofuer |
|
||||
| Variable | Wofür |
|
||||
| --- | --- |
|
||||
| `LEGACY_DB_NAME` | Standard `project_scanner` |
|
||||
| `LEGACY_DB_HOST/_PORT/_USER/_PASSWORD` | leer = CRM-Zugangsdaten |
|
||||
| `LEGACY_UPLOAD_DIR` | Verzeichnis der alten CV-PDFs (`/var/www/Projekt Scanner/backend/uploads`); die Dateien werden **kopiert**. Wegen des Leerzeichens im Pfad in der `.env` in Anfuehrungszeichen setzen - systemd liest die Datei als `EnvironmentFile` und trennt sonst am Leerzeichen |
|
||||
| `LEGACY_ENCRYPTION_KEY` | nur damit lassen sich die Portal-Passwoerter uebernehmen: entschluesseln mit dem alten, neu verschluesseln mit dem eigenen Schluessel |
|
||||
| `LEGACY_UPLOAD_DIR` | Verzeichnis der alten CV-PDFs (`/var/www/Projekt Scanner/backend/uploads`); die Dateien werden **kopiert**. Wegen des Leerzeichens im Pfad in der `.env` in Anführungszeichen setzen - systemd liest die Datei als `EnvironmentFile` und trennt sonst am Leerzeichen |
|
||||
| `LEGACY_ENCRYPTION_KEY` | nur damit lassen sich die Portal-Passwörter übernehmen: entschlüsseln mit dem alten, neu verschlüsseln mit dem eigenen Schlüssel |
|
||||
|
||||
Uebernommen werden auch die Session-Cookies der Portale (Klartext-JSON in der
|
||||
Übernommen werden auch die Session-Cookies der Portale (Klartext-JSON in der
|
||||
DB) - der neue Scanner ist damit sofort scharf.
|
||||
|
||||
Agenten, Projekte und Treffer behalten ihre Ids, damit uebernommene Treffer
|
||||
Agenten, Projekte und Treffer behalten ihre Ids, damit übernommene Treffer
|
||||
weiter auf die richtigen Agenten und Projekte zeigen. Deshalb bricht das Skript
|
||||
ab, wenn im Ziel schon Agenten stehen - `--force` raeumt vorher auf.
|
||||
ab, wenn im Ziel schon Agenten stehen - `--force` räumt vorher auf.
|
||||
|
||||
> **Beim Rollout am 06.08.2026 festgestellt:** Die in der alten Datenbank
|
||||
> gespeicherten Portal-Passwoerter lassen sich mit dem dortigen
|
||||
> `ENCRYPTION_KEY` **nicht** mehr entschluesseln - der Schluessel wurde nach dem
|
||||
> gespeicherten Portal-Passwörter lassen sich mit dem dortigen
|
||||
> `ENCRYPTION_KEY` **nicht** mehr entschlüsseln - der Schlüssel wurde nach dem
|
||||
> Speichern getauscht. Die alte App kann sie damit ebenso wenig lesen; es ging
|
||||
> also nichts verloren. Wer den Notfall-Login des Scrapers will, gibt die
|
||||
> Passwoerter unter *Boersen-Konten* neu ein. Die Sessions (Cookies) sind davon
|
||||
> unberuehrt und wurden uebernommen.
|
||||
> Passwörter unter *Börsen-Konten* neu ein. Die Sessions (Cookies) sind davon
|
||||
> unberührt und wurden übernommen.
|
||||
|
||||
### Playwright
|
||||
|
||||
`playwright` ist auf **exakt 1.60.0** gepinnt - dieselbe Version wie die laufende
|
||||
Scanner-App. Deren Chromium-Build (`chromium-1223`) liegt auf dem Server bereits
|
||||
in `/root/.cache/ms-playwright`, also faellt kein zweiter Browser-Download an und
|
||||
in `/root/.cache/ms-playwright`, also fällt kein zweiter Browser-Download an und
|
||||
gescrapt wird mit der Version, die dort produktiv funktioniert. Ein `^` in der
|
||||
Version wuerde beides aufgeben, deshalb steht in `server/package.json` ein
|
||||
`comments`-Feld mit dieser Begruendung.
|
||||
Version würde beides aufgeben, deshalb steht in `server/package.json` ein
|
||||
`comments`-Feld mit dieser Begründung.
|
||||
|
||||
## Diktat (Whisper im Browser)
|
||||
|
||||
Ueberall, wo laengerer Text entsteht - Projektbeschreibung, Interaktionsnotiz,
|
||||
naechster Schritt, interne Notizen - sitzt ein Mikrofon-Button. Die Erkennung
|
||||
laeuft **vollstaendig im Browser des Benutzers**: Whisper als ONNX-Modell ueber
|
||||
Überall, wo längerer Text entsteht - Projektbeschreibung, Interaktionsnotiz,
|
||||
nächster Schritt, interne Notizen - sitzt ein Mikrofon-Button. Die Erkennung
|
||||
läuft **vollständig im Browser des Benutzers**: Whisper als ONNX-Modell über
|
||||
`@huggingface/transformers`, gerechnet auf der Grafikkarte (WebGPU) oder
|
||||
ersatzweise auf der CPU (WASM).
|
||||
|
||||
@@ -366,8 +366,8 @@ Das Backend ist daran **nicht beteiligt** und sieht keine Audiodaten - es gibt
|
||||
keinen API-Key, keine Kosten pro Minute und keinen Datenabfluss. Aufnahmen
|
||||
werden nirgends gespeichert.
|
||||
|
||||
* Die Erkennung laeuft in einem **Web Worker** (`src/lib/whisper.worker.ts`),
|
||||
damit die Oberflaeche waehrenddessen nicht einfriert.
|
||||
* Die Erkennung läuft in einem **Web Worker** (`src/lib/whisper.worker.ts`),
|
||||
damit die Oberfläche währenddessen nicht einfriert.
|
||||
* Die **ONNX-Laufzeit** wird mit ausgeliefert (Vite bundelt die `.wasm`), die
|
||||
**Modelldateien** kommen beim ersten Laden vom Hugging-Face-CDN und bleiben
|
||||
danach im Browser-Cache. Wer auch das vermeiden will, kann die Modelle auf den
|
||||
@@ -375,42 +375,42 @@ werden nirgends gespeichert.
|
||||
|
||||
### Reiter "Whisper"
|
||||
|
||||
Unten im Menue, ueber den Einstellungen, fuer **alle Rollen** sichtbar. Es gibt
|
||||
Unten im Menü, über den Einstellungen, für **alle Rollen** sichtbar. Es gibt
|
||||
dort **nichts zu konfigurieren** - genau ein Knopf:
|
||||
|
||||
1. Reiter oeffnen, dort steht *"Einmalig werden 299 MB Sprachmodell geladen."*
|
||||
1. Reiter öffnen, dort steht *"Einmalig werden 299 MB Sprachmodell geladen."*
|
||||
2. **Sprachmodell laden (299 MB)** klicken, Fortschrittsbalken abwarten
|
||||
3. Fertig - ab jetzt sind die Mikrofon-Buttons in den Formularen aktiv, auch
|
||||
nach jeder weiteren Anmeldung
|
||||
|
||||
Dazu gibt es eine Probeaufnahme und **Sprachmodell loeschen**, falls man den
|
||||
Platz zurueckhaben will. Sonst nichts: keine Modellauswahl, keine Genauigkeit,
|
||||
kein Rechenweg, keine Geraetetabelle.
|
||||
Dazu gibt es eine Probeaufnahme und **Sprachmodell löschen**, falls man den
|
||||
Platz zurückhaben will. Sonst nichts: keine Modellauswahl, keine Genauigkeit,
|
||||
kein Rechenweg, keine Gerätetabelle.
|
||||
|
||||
Das ist Absicht. Modell, Genauigkeit, Rechenweg und Sprache stehen als
|
||||
Konstanten in `src/lib/whisper.tsx` - `small`, `q4`, Grafikkarte wenn vorhanden,
|
||||
Deutsch. Jede andere Kombination war in den Messungen entweder schlechter oder
|
||||
kaputt (siehe unten); eine Auswahl haette also nur Gelegenheit geboten, etwas
|
||||
kaputt (siehe unten); eine Auswahl hätte also nur Gelegenheit geboten, etwas
|
||||
falsch zu machen.
|
||||
|
||||
Beim Anmelden wird das Modell automatisch bereitgestellt, **wenn** es schon in
|
||||
diesem Browser liegt - ein Download von 759 MB startet nie von allein. Der
|
||||
Automatismus greift bewusst nur einmal pro Seitenaufruf: wer das Modell loescht,
|
||||
Automatismus greift bewusst nur einmal pro Seitenaufruf: wer das Modell löscht,
|
||||
bekommt es nicht sofort wieder heruntergeladen.
|
||||
|
||||
### Wo das Modell liegt und wie lange
|
||||
|
||||
Nicht in Cookies - dort passen nur wenige Kilobyte. Die Dateien liegen im
|
||||
**Cache-Speicher der Seite** (`caches`, Name `transformers-cache`), der
|
||||
Sitzungen und Browserneustarts ueberlebt. Beim Laden bittet die App zusaetzlich
|
||||
Sitzungen und Browserneustarts überlebt. Beim Laden bittet die App zusätzlich
|
||||
per `navigator.storage.persist()` um dauerhaften Speicher; damit darf der Browser
|
||||
die Dateien bei Platznot nicht mehr selbst wegraeumen. Chrome entscheidet darueber
|
||||
nach eigenen Kriterien und lehnt haeufig ab - dann funktioniert alles genauso, nur
|
||||
die Dateien bei Platznot nicht mehr selbst wegräumen. Chrome entscheidet darüber
|
||||
nach eigenen Kriterien und lehnt häufig ab - dann funktioniert alles genauso, nur
|
||||
ohne diese Zusicherung.
|
||||
|
||||
Geraet oder Browserprofil gewechselt? Dann faellt der Download erneut an; der
|
||||
Speicher gehoert zum Profil, nicht zum Benutzerkonto. Findet die App das Modell
|
||||
nach dem Laden nicht im Cache, sagt sie es ausdruecklich.
|
||||
Gerät oder Browserprofil gewechselt? Dann fällt der Download erneut an; der
|
||||
Speicher gehört zum Profil, nicht zum Benutzerkonto. Findet die App das Modell
|
||||
nach dem Laden nicht im Cache, sagt sie es ausdrücklich.
|
||||
|
||||
### Warum small und q4 - die Messungen
|
||||
|
||||
@@ -424,17 +424,17 @@ eine echte Stimme) auf einem Rechner mit NVIDIA-GPU, jeweils in q4:
|
||||
| small | "auf Esfihana", "Der Tagessatz liegt bei 1100 Euro" | 299 MB | 10 s |
|
||||
| **large-v3-turbo** | **"auf S4 HANA"**, "Der Tagessatz liegt bei 1100 Euro" | 759 MB | 13 s |
|
||||
|
||||
Alle drei erkennen etwa 4x schneller als Echtzeit; auf Geraeten ohne WebGPU
|
||||
Alle drei erkennen etwa 4x schneller als Echtzeit; auf Geräten ohne WebGPU
|
||||
verschiebt sich das Tempo erheblich.
|
||||
|
||||
Festgelegt ist **large-v3-turbo**: nur dieses Modell bekommt Fachbegriffe wie
|
||||
"S4 HANA" richtig, und genau daran scheitern die kleineren im Alltag. Die 759 MB
|
||||
fallen einmal pro Browser an - danach liegt das Modell im Cache-Speicher der
|
||||
Seite und ueberlebt Browserneustarts (gemessen: 762 MB, zweite Sitzung ohne
|
||||
Seite und überlebt Browserneustarts (gemessen: 762 MB, zweite Sitzung ohne
|
||||
jeden Download bereit).
|
||||
|
||||
`small` ist der Kompromiss: turbo erkennt besser, wird vom Browser aber nicht
|
||||
zwischengespeichert (siehe Groessengrenzen unten) und laedt darum bei jedem Start
|
||||
zwischengespeichert (siehe Größengrenzen unten) und lädt darum bei jedem Start
|
||||
erneut 759 MB.
|
||||
|
||||
### Welche Genauigkeit auf welchem Rechenweg
|
||||
@@ -443,41 +443,41 @@ Deshalb ist `q4` festgelegt:
|
||||
|
||||
| | q4 | q8 | fp16 | fp32 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **WebGPU** | tiny 1,4 s · small 1,9 s · turbo 3,8 s | Kauderwelsch oder keine Antwort | tiny laeuft, small blockiert den Tab | Einzeldatei zu gross |
|
||||
| **WASM** | laeuft | small 10,3 s | ungetestet | Einzeldatei zu gross |
|
||||
| **WebGPU** | tiny 1,4 s · small 1,9 s · turbo 3,8 s | Kauderwelsch oder keine Antwort | tiny läuft, small blockiert den Tab | Einzeldatei zu groß |
|
||||
| **WASM** | läuft | small 10,3 s | ungetestet | Einzeldatei zu groß |
|
||||
|
||||
Der wichtigste Grund fuer diese Beschraenkung: bei `small` in fp16 auf WebGPU
|
||||
Der wichtigste Grund für diese Beschränkung: bei `small` in fp16 auf WebGPU
|
||||
reagiert der ganze Browser-Tab nicht mehr - **kein Timeout in der Seite kann das
|
||||
abfangen**, weil das Skript selbst nicht mehr laeuft.
|
||||
abfangen**, weil das Skript selbst nicht mehr läuft.
|
||||
|
||||
Genau deshalb ist die Auswahl verschwunden: `q4` steht fest, und damit kann diese
|
||||
Klasse von Fehlern nicht mehr auftreten. Als Netz bleibt trotzdem eine Frist pro
|
||||
Erkennung und eine Haenger-Erkennung beim Laden - falls ein Geraet trotzdem etwas
|
||||
Erkennung und eine Hänger-Erkennung beim Laden - falls ein Gerät trotzdem etwas
|
||||
Unerwartetes tut.
|
||||
|
||||
Zusaetzlich hat jede Erkennung eine Frist (15x Aufnahmedauer, mindestens 45 s,
|
||||
hoechstens 5 min). Laeuft sie ab, wird der Worker verworfen und der Fehler
|
||||
Zusätzlich hat jede Erkennung eine Frist (15x Aufnahmedauer, mindestens 45 s,
|
||||
höchstens 5 min). Läuft sie ab, wird der Worker verworfen und der Fehler
|
||||
gemeldet, statt dauerhaft "Erkenne..." zu zeigen.
|
||||
|
||||
### Die Groessengrenze des Browsers
|
||||
### Die Größengrenze des Browsers
|
||||
|
||||
Nicht die Gesamtgroesse entscheidet, sondern die **groesste Einzeldatei** - der
|
||||
Browser muss sie in einem zusammenhaengenden Speicherblock halten. Ueber etwa
|
||||
Nicht die Gesamtgröße entscheidet, sondern die **größte Einzeldatei** - der
|
||||
Browser muss sie in einem zusammenhängenden Speicherblock halten. Über etwa
|
||||
500 MB bricht das Laden mit "Array buffer allocation failed" ab. Belege: 425 MB
|
||||
(turbo in q4) laden und laufen, 688 MB (turbo in fp32) scheitern.
|
||||
|
||||
Genau daran scheitern fuer turbo alle Genauigkeiten ausser q4 - ein weiterer
|
||||
Genau daran scheitern für turbo alle Genauigkeiten ausser q4 - ein weiterer
|
||||
Grund, warum es keine Auswahl gibt.
|
||||
|
||||
Die Downloadgroesse im Reiter kommt live von Hugging Face
|
||||
Die Downloadgröße im Reiter kommt live von Hugging Face
|
||||
(`src/lib/whisperModels.ts`), damit dort eine echte Zahl steht.
|
||||
|
||||
> **Korrektur:** In einer frueheren Fassung stand hier eine zweite Grenze von
|
||||
> **Korrektur:** In einer früheren Fassung stand hier eine zweite Grenze von
|
||||
> etwa 300 MB pro Cache-Eintrag, oberhalb derer das Modell angeblich nicht
|
||||
> zwischengespeichert wird. Das war falsch - ein Artefakt der Testumgebung.
|
||||
> Von Playwright gestartete Chrome-Instanzen schreiben den Cache-Speicher nicht,
|
||||
> ein normal gestartetes Chrome dagegen sehr wohl: turbo/q4 liegt dort mit
|
||||
> 762 MB im Cache und ueberlebt einen Browserneustart. Deshalb prueft
|
||||
> 762 MB im Cache und überlebt einen Browserneustart. Deshalb prüft
|
||||
> `scratchpad/realchrome.mjs` die Persistenz gegen ein normal gestartetes
|
||||
> Chrome und nicht gegen den Automatisierungskontext.
|
||||
|
||||
@@ -490,59 +490,59 @@ Die Downloadgroesse im Reiter kommt live von Hugging Face
|
||||
|
||||
Solange nichts geladen ist, erscheint im Opportunity-Formular ein Hinweis mit
|
||||
Knopf **Zu Whisper**; die einzelnen Mikrofon-Buttons werden zu *Diktat
|
||||
einrichten*. Beides schliesst den Dialog und springt in den Reiter. Auf Geraeten
|
||||
einrichten*. Beides schließt den Dialog und springt in den Reiter. Auf Geräten
|
||||
ohne Mikrofon oder ohne HTTPS erscheint gar kein Diktat.
|
||||
|
||||
## Kalender
|
||||
|
||||
Ein gemeinsamer Teamkalender: alle Benutzer sehen dieselben Termine, jeder darf
|
||||
Eintraege anlegen.
|
||||
Einträge anlegen.
|
||||
|
||||
* **Ansichten** Monat, Woche, Tag; `Heute` und die Pfeile blaettern. Klick auf
|
||||
* **Ansichten** Monat, Woche, Tag; `Heute` und die Pfeile blättern. Klick auf
|
||||
einen Monatstag oder eine Stundenzelle legt direkt einen Termin an, Klick auf
|
||||
einen Wochentagskopf springt in die Tagesansicht.
|
||||
* **Farben** Jeder Termin hat die Farbe seines Erstellers. Die Farbe kommt aus
|
||||
`crm_users.color`; ist sie leer, wird sie stabil aus der Benutzer-ID
|
||||
abgeleitet (Palette in `src/types.ts`). Aenderbar unter *Einstellungen → Mein
|
||||
Konto* und - fuer Admins fuer alle - in der Nutzerverwaltung. Die Legende
|
||||
unter dem Kalender zeigt, welche Farbe zu wem gehoert.
|
||||
abgeleitet (Palette in `src/types.ts`). Änderbar unter *Einstellungen → Mein
|
||||
Konto* und - für Admins für alle - in der Nutzerverwaltung. Die Legende
|
||||
unter dem Kalender zeigt, welche Farbe zu wem gehört.
|
||||
* **Teilnehmer** Ein Termin kann beliebig viele Benutzer haben; sie erscheinen
|
||||
mit Foto - oder, wenn keines hinterlegt ist, als Initiale auf ihrer
|
||||
Kalenderfarbe - und sind anklickbar.
|
||||
* **@-Mentions** In der Beschreibung `@` tippen, um Kontakte *und* Benutzer zu
|
||||
markieren (Pfeiltasten, Enter/Tab zum Uebernehmen). Gespeichert wird ein Token
|
||||
markieren (Pfeiltasten, Enter/Tab zum Übernehmen). Gespeichert wird ein Token
|
||||
`@[Anzeigename](lead:12)` bzw. `@[Anzeigename](user:3)`. In der Leseansicht
|
||||
sind Mentions klickbar: Kontakte oeffnen das Kontaktfenster, Benutzer ein
|
||||
Detailfenster mit Farbe, naechsten Terminen und zugeordneten Kontakten.
|
||||
sind Mentions klickbar: Kontakte öffnen das Kontaktfenster, Benutzer ein
|
||||
Detailfenster mit Farbe, nächsten Terminen und zugeordneten Kontakten.
|
||||
Existiert die Person nicht mehr, bleibt der Name als grauer Text stehen.
|
||||
* **Naechste Termine** Rechts neben dem Kalender, nach Tagen gruppiert und
|
||||
zeitlich sortiert - der naechste anstehende Termin steht oben. Vergangene
|
||||
* **Nächste Termine** Rechts neben dem Kalender, nach Tagen gruppiert und
|
||||
zeitlich sortiert - der nächste anstehende Termin steht oben. Vergangene
|
||||
Termine fallen aus der Liste, bleiben im Kalender aber erhalten.
|
||||
|
||||
## Fotos
|
||||
|
||||
Fotos von Kontakten und Benutzern liegen als Data-URL in der Datenbank
|
||||
(`crm_leads.avatar`, `crm_users.avatar`). Damit das die Nutzlast nicht aufblaeht,
|
||||
(`crm_leads.avatar`, `crm_users.avatar`). Damit das die Nutzlast nicht aufbläht,
|
||||
verkleinert der Browser jedes Bild **vor** dem Upload
|
||||
(`src/lib/imageCompress.ts`):
|
||||
|
||||
* mittiger Ausschnitt auf ein Quadrat von 256 px - mehr braucht keine Ansicht,
|
||||
die groesste ist 80 px
|
||||
* WebP mit Qualitaet 0,82, Fallback JPEG, dann PNG
|
||||
die größte ist 80 px
|
||||
* WebP mit Qualität 0,82, Fallback JPEG, dann PNG
|
||||
* aus einem 2,9-MB-Foto werden so einige Kilobyte
|
||||
|
||||
Das Backend akzeptiert nur Bild-Data-URLs und http(s)-Adressen und begrenzt die
|
||||
Laenge; die Quelldatei darf bis 25 MB gross sein, weil sie ohnehin
|
||||
Länge; die Quelldatei darf bis 25 MB groß sein, weil sie ohnehin
|
||||
komprimiert wird.
|
||||
|
||||
Benutzerfotos pflegt jeder fuer sich unter *Einstellungen → Mein
|
||||
Konto*, Admins fuer alle in der Nutzerverwaltung. Angezeigt werden sie in der
|
||||
Benutzerfotos pflegt jeder für sich unter *Einstellungen → Mein
|
||||
Konto*, Admins für alle in der Nutzerverwaltung. Angezeigt werden sie in der
|
||||
Sidebar, bei Terminteilnehmern und Erstellern, im Benutzer-Detailfenster, in den
|
||||
@-Vorschlaegen, in der Nutzerverwaltung und als Benutzerknoten im
|
||||
Netzwerkdiagramm. Ohne Foto erscheint ueberall die Initiale auf der
|
||||
@-Vorschlägen, in der Nutzerverwaltung und als Benutzerknoten im
|
||||
Netzwerkdiagramm. Ohne Foto erscheint überall die Initiale auf der
|
||||
Kalenderfarbe (`UserAvatar`).
|
||||
|
||||
Ueberlappende Termine teilen sich im Zeitraster die Spaltenbreite, mehrtaegige
|
||||
Überlappende Termine teilen sich im Zeitraster die Spaltenbreite, mehrtägige
|
||||
Termine erscheinen an jedem betroffenen Tag, und eine rote Linie markiert die
|
||||
aktuelle Uhrzeit.
|
||||
|
||||
@@ -550,14 +550,14 @@ aktuelle Uhrzeit.
|
||||
|
||||
Kraftgerichtetes Layout (`d3-force`), gerendert als SVG.
|
||||
|
||||
* **Kreise** = Kontakte, Farbe nach Status, Groesse nach Anzahl Verbindungen
|
||||
* **Kreise** = Kontakte, Farbe nach Status, Größe nach Anzahl Verbindungen
|
||||
* **Quadrate** = Benutzer, die Kontakte angelegt haben
|
||||
* **Gruene Linie** = Kontakt ↔ Kontakt (Dicke nach Verbindungsstaerke)
|
||||
* **Grüne Linie** = Kontakt ↔ Kontakt (Dicke nach Verbindungsstärke)
|
||||
* **Graue gestrichelte Linie** = Ersteller → Kontakt
|
||||
|
||||
Bedienung: Hintergrund ziehen verschiebt, Mausrad zoomt auf den Cursor,
|
||||
Knoten ziehen fixiert ihn (gruener Punkt), Doppelklick loest die Fixierung.
|
||||
Ueber *Verbindung erstellen* zwei Kontakte anklicken, um eine Kante anzulegen;
|
||||
Knoten ziehen fixiert ihn (grüner Punkt), Doppelklick löst die Fixierung.
|
||||
Über *Verbindung erstellen* zwei Kontakte anklicken, um eine Kante anzulegen;
|
||||
Esc bricht ab. Die Ansicht respektiert Suche und Statusfilter.
|
||||
|
||||
## Lokale Entwicklung
|
||||
@@ -569,7 +569,7 @@ npm start # Backend :5006
|
||||
cd .. && npm run dev # Frontend :3100, /api wird geproxied
|
||||
```
|
||||
|
||||
Fuer lokale Entwicklung braucht es eine erreichbare MariaDB mit der Datenbank
|
||||
Für lokale Entwicklung braucht es eine erreichbare MariaDB mit der Datenbank
|
||||
`perinco_CRM`; das Schema legt das Backend beim Start selbst an. Die
|
||||
Scanner-Datenbank `perinco_scanner` legt es komplett selbst an, inklusive
|
||||
Tabellen, Portalen und Standardeinstellungen.
|
||||
@@ -611,19 +611,19 @@ Abschnitt *Playwright* oben.
|
||||
|
||||
> **Reihenfolge beachten:** Der Import schreibt `search_interval` aus der alten
|
||||
> App (15 Minuten) in die Einstellungen. Der laufende Scheduler bemerkt die
|
||||
> Aenderung binnen einer Minute und plant sich neu - beim Rollout hat er deshalb
|
||||
> Änderung binnen einer Minute und plant sich neu - beim Rollout hat er deshalb
|
||||
> direkt nach dem Import einen Scan gestartet, den der folgende Restart mitten im
|
||||
> Scrapen abgeschossen hat (`Target page ... has been closed` im Log, harmlos).
|
||||
> Wer das vermeiden will, macht Schritt 3 vor Schritt 2.
|
||||
|
||||
Das hoehere `MemoryMax` (1536M statt 512M) ist kein Luxus: ein Scan-Lauf startet
|
||||
Chromium als Kindprozess in derselben cgroup, mit 512M wuerde der OOM-Killer das
|
||||
Backend mitten im Scrapen beenden. `deploy.ps1` laedt die Unit nicht mit hoch -
|
||||
Das höhere `MemoryMax` (1536M statt 512M) ist kein Luxus: ein Scan-Lauf startet
|
||||
Chromium als Kindprozess in derselben cgroup, mit 512M würde der OOM-Killer das
|
||||
Backend mitten im Scrapen beenden. `deploy.ps1` lädt die Unit nicht mit hoch -
|
||||
daher der `cp` oben.
|
||||
|
||||
> **RAM im Parallelbetrieb:** Im schlechtesten Fall scrapen beide Scanner
|
||||
> gleichzeitig, also laufen zwei Chromium-Instanzen. `SCANNER_SCHEDULER_OFFSET`
|
||||
> verschiebt die Laeufe gegeneinander, garantiert aber nichts - der alte Dienst
|
||||
> verschiebt die Läufe gegeneinander, garantiert aber nichts - der alte Dienst
|
||||
> hat seinen eigenen Zeitplan. Auf einem Server mit knappem RAM und vollem Swap
|
||||
> ist das der Punkt, den man nach dem ersten Tag im Journal nachsieht.
|
||||
|
||||
@@ -665,4 +665,4 @@ in `deploy/pcrm.perinco.de.conf` und den certbot-Aufruf anpassen.
|
||||
|
||||
Beim ersten Backend-Start wird ein Administrator aus den `ADMIN_*`-Variablen in
|
||||
`server/.env` angelegt - aber nur, wenn `crm_users` leer ist. Das Passwort ist
|
||||
danach ausschliesslich ueber die Nutzerverwaltung bzw. *Mein Konto* aenderbar.
|
||||
danach ausschließlich über die Nutzerverwaltung bzw. *Mein Konto* änderbar.
|
||||
|
||||
Reference in New Issue
Block a user