Commit Graph
12 Commits
Author SHA1 Message Date
Sascha WolfandClaude Opus 5 3c9d83161e chore(scanner): alte Scanner-App zurueckgebaut
Die eigenstaendige Vorgaenger-App unter scanner.perinco.de ist auf dem Server
entfernt: Unit project-scanner-backend (Port 5005), beide Apache-Vhosts, das
Let's-Encrypt-Zertifikat samt Renewal-Config, /var/www/Projekt Scanner (486 MB)
und die Datenbank project_scanner (Dump unter /root/backups). Damit entfallen
die LEGACY_*-Variablen und das Importskript, dessen Quelle es nicht mehr gibt.

/root/.cache/ms-playwright bleibt bewusst stehen: der Cache stammt aus der
Installation der alten App, liegt aber ausserhalb beider Verzeichnisse und wird
vom CRM-Scanner benutzt - playwright ist auf 1.60.0 gepinnt, damit es genau
dieser Build (chromium-1223) bleibt. Der Hinweis steht jetzt in README und
docs/06-betrieb.md, damit ihn beim naechsten Aufraeumen niemand mitnimmt.

SCANNER_SCHEDULER_OFFSET geht auf 0: der Versatz sollte verhindern, dass beide
Instanzen mit denselben Portal-Cookies gleichzeitig losziehen. Ohne zweite
Instanz hat er keinen Zweck mehr, die Mechanik bleibt fuer kuenftige Erststarts
im Code.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 17:45:42 +02:00
Sascha WolfandClaude Opus 5 ab6b9f2372 fix(ui): Beschriftungszeile mit Diktatknopf richtet die Felder daneben aus
Im Abschnitt "Verlauf" der Opportunity begann "Was wurde besprochen?"
tiefer als "Letzte Interaktion" daneben. Ursache war die
Beschriftungszeile: rechts steht ein Diktatknopf darin, links nur Text -
die Zeile war damit 26,5 statt 16,5 Pixel hoch und schob das Eingabefeld
nach unten.

Der Knopf bekommt eine feste Hoehe (h-7, 28 px) statt sich aus
Zeilenhoehe, Innenabstand und Rahmen zu ergeben; auf 26,5 px laesst sich
keine Zeile ausrichten. Dazu labelRowClass mit min-h-7: damit ist die
Zeile in beiden Spalten gleich hoch, mit Knopf oder ohne. Gemessen sind
es jetzt 0,0 px Abweichung statt 8.

Zweiter Fund derselben Stelle: die drei Beschriftungen im Dialog waren
von Hand abgeschrieben statt labelClass zu nehmen. Damit fehlte ihnen die
Klasse "field-label", die seit den Themes die Schrift der
Feldbeschriftungen traegt - in "Pixel Art" blieben sie ohne
Pixelschrift. Die Auszeichnung steht jetzt einmal in leadUi.tsx
(labelTextClass) und wird nicht mehr kopiert; DictateTextarea benutzt
sie ebenso.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 15:30:35 +02:00
Sascha WolfandClaude Opus 5 8403f1c39d feat(ui): vierte Farbwelt "Cyberpunk" in hell und dunkel
Neon in der Nacht: fast schwarze Flaechen mit blauem Stich, violettes
Leuchten, Blaustahl statt Neutralgrau und Kanten von 2 px - scharf genug
fuer HUD-Elemente, aber nicht eckig, das ist schon Pixel Art. Schatten
sind hier kein Schatten, sondern ein Schein: farbige Kontur plus weicher
Halo in der Markenfarbe. Schrift ist Chakra Petch, die Feldbeschriftungen
laufen gesperrt.

Der Akzent ist bewusst Violett und nicht Neon-Cyan: auf brand-500 und
brand-600 steht 69 Mal weisse Schrift, und auf Cyan waere die nicht zu
lesen. Cyan sitzt deshalb nur in --color-brand-accent und in den
Statusfarben, wo dunkler Text darauf steht.

Der helle Modus ist der Konzern bei Tageslicht: kuehles Blaugrau, dasselbe
Violett nur dunkler, und aus dem Leuchten wird ein knapper kalter Schatten -
ein Halo auf hellem Grund sieht nach Fehler aus, nicht nach Neon.

Es waren vier Zeilen ausserhalb von index.css: die Whitelist in
userFields.js, die Liste in src/types.ts mit ihrer Beschriftung, die
Whitelist im Inline-Skript und die Schrift im Google-Fonts-Aufruf. Keine
Komponente wurde angefasst.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 13:47:08 +02:00
Sascha WolfandClaude Opus 5 87759a8a24 feat(ui): drei Farbwelten je hell und dunkel, gewaehlt pro Benutzer
Neben dem Design des Hauses ("perinco") gibt es "Pixel Art" und
"Outdoor", jedes in hell und dunkel. Die Wahl steht neben dem
Hell/Dunkel-Schalter im Kopf der App.

Sie gehoert dem BENUTZER und nicht dem Browser: crm_users.theme und
crm_users.theme_mode, gesetzt ueber PATCH /api/auth/me, das jeder fuer
sich selbst darf. Wer sich an einem anderen Rechner anmeldet, findet sein
Design wieder, und zwei Benutzer an derselben Maschine kommen sich nicht
in die Quere. Der localStorage bleibt als Zwischenspeicher daneben, damit
das Inline-Skript in index.html die Klassen vor dem ersten Frame setzen
kann - ohne das blitzt beim Laden das Standarddesign auf. Das hat eine
sichtbare Folge: melden sich zwei Leute an demselben Rechner an, sieht
der zweite fuer einen Augenblick das Design des ersten.

Der ThemeProvider steht deshalb in main.tsx jetzt unter dem AuthProvider
- er liest den Benutzer.

Keine Komponente wurde umgefaerbt. Im Markup steht nirgends eine feste
Farbe, sondern durchgaengig ink-*, brand-*, canvas und surface; Tailwind
v4 loest auch --radius-*, --shadow-* und die Schriften ueber Variablen
auf. Ein Theme tauscht also nur Werte, und deshalb kann Pixel Art alle
380 abgerundeten Ecken eckig machen, ohne eine Klasse anzufassen.

Zwei Dinge waren Voraussetzung:

- Der helle Block eines Themes braucht :not(.dark). html.theme-x hat
  eine Klasse mehr in der Spezifitaet als .dark und wuerde den
  Dunkelmodus sonst ueberstimmen.
- Auf brand-500/600 steht 69 Mal weisse Schrift. Jedes Theme braucht
  dort einen ausreichend dunklen Ton - ein helles Pastell waere
  unlesbar.

Die Pixelschrift (Silkscreen) sitzt nur in --font-display, also in den
Ueberschriften und den Feldbeschriftungen; labelClass tragt dafuer die
neue Klasse "field-label". Der Fliesstext bleibt Nunito Sans: die Listen
haben bis zu acht Spalten bei 11-13 px, und eine Pixelschrift waere darin
nicht mehr zu lesen. Outdoor bringt Cabin fuer beides mit.

server/test/userFields.test.mjs prueft die Werte und dass die Liste der
Farbwelten in userFields.js und src/types.ts nicht auseinanderlaeuft -
sonst zeigt die Auswahl ein Design an, dessen Speichern das Backend
abweist, und niemand weiss warum.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 13:41:17 +02:00
Sascha WolfandClaude Opus 5 62dce64739 feat(akquise): mehrere Berater je Eintrag, je Berater eine Opportunity
Ein gefundenes Projekt passt oft zu mehr als einem Berater, und fuer
jeden geht eine eigene Bewerbung heraus. Das Feld "Berater" liess bisher
nur einen Namen zu.

Gespeichert wird eine kommagetrennte Liste in crm_acquisitions.consultant
(auf VARCHAR(500) erweitert, Migration in db.js) und keine eigene
Zuordnungstabelle: die Namen sind reiner Text, die Auswahlliste in
crm_acquisition_options ist eine Pflegehilfe und kein Fremdschluessel.
Suche, Sortierung und Anzeige der Liste arbeiten damit weiter auf einem
Feld. Die Funktionen zum Zerlegen stehen in acquisitionRules.js, das
Gegenstueck fuer das Frontend in src/types.ts, geprueft von
server/test/acquisition.test.mjs.

Zerlegt wird an vier Stellen:

- Uebernahme: je Berater eine Opportunity, alle in einer Transaktion.
  Ihre Titel tragen den Namen in Klammern, weil die Opportunity kein
  eigenes Feld fuer den Berater hat und in Pipeline und Liste sonst
  identische Zeilen stehen; bei einem einzigen Berater bleibt der Titel
  unveraendert. In den Notizen steht neben dem eigenen Berater auch, mit
  wem er sich das Projekt teilt. Der Eintrag merkt sich die erste
  Opportunity - sie entscheidet nur darueber, ob er wieder im Eingang
  auftaucht.
- Verlauf (crm_acquisition_history): ein Berater je Zeile. Ein Eintrag
  mit drei Beratern sind drei Bewerbungen, "Nico, Lisa" waere sonst ein
  eigener Schluessel neben "Nico" und "Lisa".
- Dashboard: dieselbe Zaehlung. Die Summe der Balken liegt damit ueber
  der Gesamtzahl "Bewerbungen", die weiter die Eintraege zaehlt.
- Auswahlliste pflegen: Umbenennen ersetzt den Namen innerhalb der
  Liste, Loeschen zaehlt auch, wer nur einer von mehreren ist. Ein
  Gleichheitsvergleich uebersah beides.

In der Oberflaeche haengt das Auswahlfeld jeweils einen Namen an, die
Gewaehlten stehen als Plaketten darunter und lassen sich einzeln
entfernen; ein bereits gewaehlter Berater verschwindet aus der Liste.
Bewusst kein <select multiple>: Strg-Klick ist mit der Maus unauffindbar
und mit einer Beruehrung gar nicht bedienbar.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-09 10:35:43 +02:00
Sascha WolfandClaude Opus 5 29316633a6 docs: Clone-URL auf neues Gitea git.perinco.de umgestellt
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-05 14:42:24 +02:00
Sascha WolfandClaude Opus 5 f319b8207b feat(akquise): Archiv bearbeiten und wiederherstellen
Im Archiv setzte der Stift nur `editing` - das Formular stand dort nicht,
also passierte sichtbar nichts. Es wird jetzt auch im Archiv gezeigt,
aber nur beim Bearbeiten: eingetragen wird weiterhin im Eingang.

Dazu ein Weg zurueck. `POST /api/acquisitions/:id/restore` setzt
`found_at` auf heute und leert `converted_at` samt `opportunity_id` -
ohne Letzteres haelt die Uebernahme einen Rueckfaeller im Archiv fest,
gleich welches Datum er traegt. Eine Spalte "wiederhergestellt" gibt es
bewusst nicht; die Tabelle fuehrt keinen Zustand.

Der Pfeil erscheint nur im Archiv. Ein nie uebernommener Eintrag wird
ohne Rueckfrage zurueckgeholt, ein Rueckfaeller mit - dabei faellt der
Verweis auf die geloeschte Opportunity weg. Fuer diese Rueckfrage kennt
`ConfirmDialog` jetzt `tone="neutral"`: ein roter Knopf an einer
harmlosen Stelle nimmt der Farbe ihre Bedeutung.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 09:35:15 +02:00
Sascha WolfandClaude Opus 5 8741a9ae0c feat: Interviews, Mail-Papierkorb, Kontaktart
Drei Aenderungen aus dem Alltagsgebrauch:

- Der Kalender heisst jetzt "Interviews" - dafuer wird er benutzt. Der Wert in
  ViewState wandert mit, damit Menue und Kopfzeile nicht auseinanderlaufen.
  Sichtbare Reste ("Kalenderfarbe") heissen jetzt Terminfarbe.

- Mails haben einen Papierkorb, der unter dem Menuepunkt aufklappt. Loeschen
  heisst dabei ausschliesslich: im CRM. Im Postfach crm@perinco.de bleibt die
  Mail unberuehrt - Schreibrechte darauf hat die Anwendung nicht. Damit der
  Delta-Abgleich geloeschte Mails nicht wieder einliest, bleibt nach dem Leeren
  eine leere Zeile als Merker stehen (crm_purged); runMailSync() ueberspringt
  alles, was im CRM geloescht ist. Zurueckholen und Leeren darf jeder
  Angemeldete, Leeren nach einer Warnung.

- Der Kontaktstatus (Neu .. Verloren) wird zur Kontaktart: Recruiter, Partner,
  Endkunde. Die Spalte heisst jetzt kind, weil "Status" das Feld falsch
  benannte. migrateLeadKind() stellt bestehende Datenbanken um und setzt alle
  vorhandenen Kontakte auf Recruiter - welche Art jemand ist, sagt der alte
  Status nicht (so mit dem Nutzer entschieden).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 11:12:49 +02:00
Sascha WolfandClaude Opus 5 59080019d2 docs: Umlaute auch im erklaerenden Text der Codebloecke
Der Dateibaum im README und in 05/08, die Kommentare hinter # in den
Beispielbefehlen und die Beschriftungen in Mermaid-Sequenzdiagrammen sind
Lesetext, stehen aber in Codeblöcken - die erste Runde hat sie deshalb
übersprungen.

Die Befehle selbst und alle Bezeichner bleiben unverändert: ersetzt wurde
nur hinter "# ", hinter den zwei Leerzeichen des Dateibaums und hinter
": " in Mermaid-Nachrichten. ER-Labels wie bewertet_fuer sind Bezeichner
und bleiben, "systemctl daemon-reload" ebenfalls.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 09:48:11 +02:00
Sascha WolfandClaude Opus 5 7973dc07b1 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>
2026-08-11 09:45:58 +02:00
Sascha WolfandClaude Opus 5 6fd713497d feat(postfach): crm@perinco.de lesend anbinden, Kontakte aus Mails
Spiegelt das Vertriebspostfach ueber Microsoft Graph ins CRM. Aus jeder Mail
entstehen Kontakt-Vorschlaege, und jede Mail laesst sich an Opportunities und
Kontakte haengen.

Uebernommen ist die Idee aus dem Altsystem KeepTrack, nicht die Umsetzung:

- Delta Query statt "$filter=isRead eq false" mit anschliessendem PATCH. Damit
  genuegt die Anwendungsberechtigung Mail.Read, es braucht keinen Schreibzugriff
  auf das Postfach, und eine vorher in Outlook geoeffnete Mail geht nicht
  verloren. Der Fortschritt liegt als delta_link in der Datenbank und ueberlebt
  Neustarts; ein Lauf ist gedeckelt und fortsetzbar.
- Tenant- und Client-ID kommen aus der .env, nicht aus dem Browser. Im
  Altsystem konnte jeder Angemeldete das Server-Secret gegen einen fremden
  Tenant verwenden.
- Kontaktdaten werden aus den Graph-Feldern und der Signatur gelesen, ohne
  Sprachmodell. Mailinhalte verlassen den Server nicht.
- Ein Vorschlag ohne E-Mail-Adresse wird nicht mit einer erfundenen Adresse
  angelegt, sondern bleibt zur Durchsicht liegen. Ist die Adresse schon als
  Kontakt vorhanden, werden nur leere Felder ergaenzt.
- Mails haengen als eigene Verknuepfung an Opportunity und Kontakt, nicht als
  Text im Notizfeld - loesbar und mehrfach moeglich.
- Fremdes Mail-HTML wird serverseitig entschaerft und in einem iframe mit
  sandbox="" gezeigt. Externe Bilder sind per CSP gesperrt, damit das Oeffnen
  dem Absender nichts meldet.
- Der Verbindungstest prueft Anmeldung und tatsaechlichen Postfachzugriff und
  benennt den wahrscheinlichen Grund. Der alte Test holte nur ein Token.

Fehlen die MS_*-Werte, startet das CRM normal; die Einstellungen nennen die
fehlende Variable. Neue Tabellen: crm_mail_messages, crm_mail_links,
crm_mail_contact_candidates, crm_mail_sync_state, crm_settings.

Uebernommene Kontakte tragen source = 'Mail'; die Kontaktliste hat dafuer einen
Herkunftsfilter.

Nicht enthalten: Senden, Antworten, Weiterleiten, Anhaenge und die automatische
Zuordnung zu Opportunities.

npm test im Verzeichnis server prueft Kontaktextraktion und Graph-Client
(47 Pruefungen, ohne Framework und ohne Datenbank).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 00:06:01 +02:00
Sascha WolfandClaude Opus 5 4d90f14d6b 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>
2026-08-06 15:34:00 +02:00