Eine zu große oder nicht erlaubte Datei wurde von multer abgewiesen, der Fehler
nur auf die Server-Konsole geloggt und der Browser kommentarlos auf /vorlagen
zurückgeleitet. Für den Benutzer sah das aus, als sei der Upload durchgelaufen,
die Anlage tauchte nur nirgends auf ("Upload error: File too large" im Log, die
Teildatei räumt multer selbst wieder weg). Betroffen waren alle drei Uploads der
Vorlagen-Seite: Anlagen, Unterschrift und Bewerbungsfoto.
Die Routen leiten jetzt mit ?fehler=… um – dasselbe Muster wie auf der Jobsuche –
und die Seite zeigt den Grund als Banner: zu groß (mit Limit), Dateityp nicht
erlaubt, oder keine Datei gewählt. Das Limit steht zusätzlich am Datei-Feld,
bevor jemand eine zu große Datei auswählt.
Limit für Anlagen von 15 auf 30 MB angehoben; gescannte Zeugnisse liegen
regelmäßig darüber. Die Größen sind Konstanten (ANHANG_MAX_MB / BILD_MAX_MB),
aus denen sowohl multer als auch die Meldung und der Hinweis im Formular lesen –
eine Meldung, die ein anderes Limit nennt als das durchgesetzte, wäre schlimmer
als gar keine. Der Bild-Filter für Unterschrift/Foto lehnt jetzt ebenfalls mit
einem Fehler ab statt die Datei still zu verwerfen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In multer/busboy-Upload-Callbacks ist der per-request AsyncLocalStorage-
Kontext nicht zuverlässig propagiert, sodass currentUserId() dort null
liefern kann und basis_anhaenge-/interne_anhaenge-Zeilen mit user_id=NULL
verwaisten (für den User unsichtbar). Die INSERTs nutzen jetzt uidFromReq
(= req.user.id mit uid()-Fallback) wie bereits userStorageDirForReq für
Signatur/Foto. Zwei bestehende verwaiste Zeilen wurden repariert.
Co-Authored-By: Claude <noreply@anthropic.com>
Der Aktivitäten-Feed (Startseite, /aktivitaeten, /statistik) führt Status-
änderungen und E-Mails per UNION zusammen, sortiert nach Erfassungszeit.
E-Mail-Einträge bekommen einen eigenen Farbpunkt (blau = erhalten, sky =
gesendet); ohne verknüpfte Bewerbung wird Absender bzw. Empfänger als
Kopfzeile gezeigt. Das UNION steckt in einem Subselect, weil SQLite ORDER BY
an einem Compound-SELECT keine Ergebnisspalten nach Namen aufloest. Die
Paginierung von /aktivitaeten zaehlt jetzt Statusaenderungen + E-Mails.
Co-Authored-By: Claude <noreply@anthropic.com>
Stern zum Merken einzelner Bewerbungen auf der Startseiten-Liste, der
Bewerbungsdetailseite und einer neuen 'Gemerkte Bewerbungen'-Sektion ganz
oben — unabhaengig vom Status. Die bisherigen Jobangebot-Favoriten werden
durchgaengig entfernt (Endpoint, Migration, UI, Startseiten-Sektion), da
nur Bewerbungen gemerkt werden sollen.
Co-Authored-By: Claude <noreply@anthropic.com>
Stellen lassen sich per Stern merken — unabhängig vom Status (offen /
uebernommen / abgelehnt). Neue Spalte jobangebote.favorit (PRAGMA-Migration),
JSON-Toggle-Endpunkt /jobangebote/:id/favorit, Stern-Button auf den Karten
sowie eine "Gemerkte Stellen"-Sektion ganz oben auf der Startseite. Toggle
laeuft per fetch mit optimistischem UI; beim Entfernen auf der Startseite
verschwindet die Zeile sofort.
Co-Authored-By: Claude <noreply@anthropic.com>
Der Assistent deckt nun alle Bereiche aus /einstellungen ab. Neue Schritte
fuer E-Mail (SMTP/IMAP) & CalDAV sowie REST-API; Indikator auf 7 Schritte.
Secrets (Passwort, Token) werden nie vorausgefuellt und beim Speeren leerer
Felder nicht ueberschrieben. Neuer Endpunkt /api/einrichtung/config mit
SSRF-Guard (Mail-Host, CalDAV-URL, Ollama-Host) und API-Token-Kollisionspruefung.
Abschluss-Checkliste trennt Pflicht- von optionalen Verbindungen.
Co-Authored-By: Claude <noreply@anthropic.com>
Nutzer ohne Bewerbungen sehen einen mehrstufigen Wizard, der Persoenliche
Angaben, Basis-Unterlagen und Ollama-KI per fetch speichert. Dashboard bleibt
im DOM (hidden), damit main.js und "Bewerbung manuell anlegen" funktionieren.
Neue Endpunkte /api/einrichtung/{persoenlich,basis,ollama,verstecken} mit
SSRF-Guard; Ollama gezielt via setForUser, ohne Mail/Kalender/Token zu leeren.
Co-Authored-By: Claude <noreply@anthropic.com>
express.static setzte kein Cache-Control, also legte der Browser die
Lebensdauer selbst fest (heuristisch, aus dem Alter der Datei). Folge: nach
einem Deploy lief die Oberflaeche im offenen Tab weiter mit dem alten
chat.js - die frisch ausgelieferten Tabellen wurden dort weiter als
Pipe-Wueste gerendert, obwohl der Server die neue Datei schon hatte.
no-cache heisst nicht "nicht cachen": der Browser behaelt die Datei, fragt
aber per ETag nach, ob sie noch gilt (304, kein erneuter Download).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Feinschliff schlug per KI Technologien vor ("Proxmox", "Rechenzentrum")
und verbot Rollenbezeichnungen ausdruecklich - die sollte die Suche selbst
aus dem Lebenslauf ableiten. Das war schief: Welche Anzeigen ueberhaupt
gefunden werden, entscheidet die Suchanfrage, also der Stellentitel.
Technologien gewichten nur, was schon da ist. Damit schlug die KI genau das
vor, was der Such-Agent ohnehin selbst ableitet, waehrend die eine Sache,
die der Nutzer weder sieht noch steuern kann - unter welchen Titeln gesucht
wird -, bei jedem Lauf neu improvisiert wurde. Faellt "Fachinformatiker
Systemintegration" weg, merkt das niemand; es kommt nur wenig zurueck.
- Neues Profilfeld berufsbezeichnungen: die KI schlaegt es vor, der Nutzer
editiert es, der Such-Agent fragt jeden Titel einzeln ab.
- zusatz_begriffe bleibt, aber als Kontext (Branche, Umfeld, Arbeitszeit),
nicht als Stellentitel; ausschluesse unveraendert.
- Leeres Feld = altes Verhalten (Titel aus dem Lebenslauf), niemandes
Profil aendert sich durch das Update.
- Spalte wird fuer bestehende Installationen nachgezogen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Titel in der Seitenleiste waren bisher die ersten 60 Zeichen der
ersten Nachricht - abgeschnitten mitten im Wort ("Ich habe mich bei der
Hetzner Online GmbH bewor..."). Jetzt formuliert das Modell daraus einen
kurzen Titel, wie man es aus ChatGPT kennt.
- Der Aufruf laeuft NACH dem done-Event: die Antwort wartet nicht auf die
Titelgenerierung (1-3 s), der Titel kommt als eigenes Event nach und die
Seitenleiste zieht live nach.
- Der abgeschnittene Titel bleibt als Platzhalter, solange die Antwort
streamt - die Seitenleiste ist also nie leer, und faellt die
Titelgenerierung aus, bleibt es schlicht dabei.
- Nur beim ersten Schlagabtausch und nur, wenn der Thread noch keinen
Titel hatte: ein selbst vergebener Name wird nicht ueberschrieben.
- Eigener Mini-Systemprompt statt des Chat-Prompts, der will beraten,
nicht betiteln. Anfuehrungszeichen/"Titel:"-Praefixe werden abgeraeumt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Assistent kannte bisher nur die Datenbank. Fragen wie "Was muss ich
ueber die Firma wissen?" konnte er damit nicht beantworten - Groesse,
Produkte, News, Kultur stehen nirgends in der Bewerbung.
Zwei neue Werkzeuge (web_suche, web_seite_lesen) ueber die Web-Search-API
von ollama.com. Sie authentifizieren sich mit demselben OLLAMA_API_KEY,
den der Benutzer fuer Chat und Dokumente ohnehin hinterlegt hat: keine
zusaetzliche Konfiguration. Anders als /api/chat haengt die Websuche
nicht an OLLAMA_HOST - wer rein lokal chattet, hat schlicht keine
Websuche, der Rest laeuft unveraendert weiter.
- Tool-Runden 4 -> 8: Bewerbung holen, suchen, Seiten lesen, antworten
lief vorher ins Limit, statt zu antworten.
- Antworten der Websuche hart gedeckelt, sie wandern sonst in jeder
weiteren Runde erneut ins Kontextfenster.
- Status-Label zeigt, wonach gesucht wird - sonst sieht der Nutzer nicht,
ob die richtige Firma erwischt wurde.
- Nackte URLs im Chat sind klickbar: Belege nennt das Modell meist als
reine URL, nicht als Markdown-Link.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Link unter "Letzte Aktivitaeten" heisst jetzt "Alle" und fuehrt auf
/aktivitaeten - den vollstaendigen Verlauf ueber alle Bewerbungen, 25 Eintraege
je Seite. Die Blaetterleiste sind echte Links (kein JS), damit Zurueck-Taste,
Lesezeichen und Neuer-Tab funktionieren; eine Seitenzahl ausserhalb des Bereichs
wird auf den gueltigen Rand geklemmt statt eine leere Seite zu zeigen.
Dabei ein Fehler von mir aus dem letzten Commit behoben: Ich hatte den Tag-Namen
samt href per <%= %> zusammengesetzt, was die Anfuehrungszeichen escaped -
heraus kam href=""/bewerbung/121"" und damit die URL
/%22/bewerbung/121%22. Jetzt zwei ausgeschriebene Zweige (Link bzw. Block).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Unter der Liste stehen jetzt die acht juengsten Statusaenderungen quer ueber
alle Bewerbungen - mit Firma, Stelle, Status, Datum und Kommentar, verlinkt auf
die Bewerbung. Sortiert nach Erfassungszeit statt nach fachlichem Datum, damit
ein nachgetragener alter Eintrag nicht oben steht.
Der Kopf der Listen-Karte nennt die Gesamtzahl. Er sitzt bewusst ausserhalb des
md:min-w-Rahmens, damit er auf dem Handy nicht horizontal mitscrollt.
Beides verschwindet waehrend einer Suche, wie Liste und Termine auch. Die
Status-Farbkarte ist eine Ebene hoeher gewandert, weil Liste und Aktivitaeten
sie jetzt beide brauchen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neue Station zwischen Vorstellungsgespraech und Einstellung - in der
Statusliste, der Gruppierung auf der Startseite, den Farbskalen (Lime,
zwischen dem Gelb des Gespraechs und dem Gruen der Einstellung), im
PDF-Export, in der Statistik und in der REST-API/OpenAPI.
Der Statistik-Funnel zaehlte bisher ever('Vorstellungsgespraech') +
ever('Einstellung') und damit eine Bewerbung, die beide Stationen
durchlaufen hat, doppelt. Mit einer weiteren Stufe waere das schlimmer
geworden, deshalb zaehlt der Funnel jetzt pro Bewerbung statt pro
Statuszeile: jede Stufe zaehlt eine Bewerbung genau einmal, ein spaeterer
Status impliziert die frueheren.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bisher bekam jeder Login pauschal ein 30-Tage-Cookie. Die Checkbox ist per
Default gesetzt (unveraendertes Verhalten); wird sie abgewaehlt, gilt ein
Session-Cookie ohne Ablaufdatum (weg beim Schliessen des Browsers) und
serverseitig eine Frist von 12 Stunden Inaktivitaet - die serverseitige Frist
ist die verbindliche, ein Client kann sein Cookie manipulieren.
Bestehende Sessions behalten per Spalten-Default die 30 Tage, niemand wird
durch das Update ausgeloggt.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drei Erweiterungen, die sich server.js und lib/documents.js teilen und
deshalb zusammen liegen:
Jobsuche: Der Feinschliff (Zusatzbegriffe/Ausschluesse) laesst sich per KI
aus den eigenen Basis-Unterlagen vorschlagen. Der Rollen-Prompt ist wie die
uebrigen unter Vorlagen editierbar; gespeichert wird erst auf Klick.
Suche: Startseite und KI-Chat durchsuchen jetzt per SQLite-FTS5 auch die
E-Mail-Korrespondenz (Absender, Betreff, Text), Notizen und
Stellenbeschreibung - ein Name aus einer Mail findet die Bewerbung. Ranking
per bm25, Fundstellen mit Snippet, Feldfilter (firma:, von:), Phrasen,
Ausschluesse, Umlaut-Varianten (mueller/muller/mueller). Der Index wird von
SQL-Triggern gepflegt, weil Bewerbungen und Mails aus UI, REST-API, IMAP und
KI-Import geschrieben werden.
E-Mail: Vor dem Generieren laesst sich vorgeben, was in der Antwort stehen
soll; die Vorgabe geht als verbindliche Anweisung in den Prompt. Steht schon
ein Entwurf im Feld, ueberarbeitet die KI ihn, statt neu anzufangen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die gefilterten Export-Zweige bauten `SELECT * FROM (<subquery>) AND
strftime(...)` — ohne WHERE nach der abgeleiteten Tabelle. SQLite
quittierte das mit "near \"AND\": syntax error" => HTTP 500, der Client
erhielt {error:'Serverfehler'} statt des Bewerbungs-Arrays und
generatePdfDocument crashte mit "applications.map is not a function".
Fix: `AND` => `WHERE` in den drei gefilterten Zweigen (month+year,
month, year). Die Per-User-Isolation bleibt unberuehrt (user_id-Filter
steht weiterhin in der Subquery). Zusaetzlich prueft generatePDF jetzt
Array.isArray(applications) und zeigt eine klare Fehlermeldung statt
des .map-Crashs.
Co-Authored-By: Claude <noreply@anthropic.com>
- sessions.impersonator_id: Admin-Session behält Token, schaltet user_id
aufs Ziel, Admin bleibt in impersonator_id gespeichert (Stack, keine
Verschachtelung). uid()/Config/Dateien laufen als Ziel-Nutzer.
- Admin sieht in /admin pro Nutzer "Anmelden als"; Bestätigungsdialog.
- Dauerhaftes amber Banner im Header mit "Zurück zum Admin" (POST, kein JS
nötig) erscheint auf jeder Seite während Impersonation.
- requireAdmin verweigert während Impersonation -> keine Admin-Aktionen als
fremder Nutzer; Stop-Route prüft impersonator_id (kein Escalation-Pfad für
Normalnutzer). Selbst-Imitation blockiert.
- audit_log-Tabelle protokolliert Start/Stop persistent; Admin-Seite zeigt
Audit-Liste (/admin/audit/impersonations).
- Migration: idempotentes ALTER ADD COLUMN impersonator_id fuer Bestand.
Co-Authored-By: Claude <noreply@anthropic.com>
- Eigene Route /statistik mit umfassender Auswertung pro Benutzer
- KPI-Karten, Monats-Trend (SVG), Verteilung nach Status/Art/Ort,
Bewerbungs-Funnel und letzte Aktivitaet als Timeline
- Statistik-Eintrag in der Top-Navigation (Desktop + Mobil)
- Kurzsstatistik vom Dashboard entfernt
Co-Authored-By: Claude <noreply@anthropic.com>
Die Leiste war eine flache Reihe aus sieben gleichrangigen Pillen. Zwei
Probleme: sie zeigte nie an, wo man gerade ist (es gab schlicht keinen
Aktiv-Zustand), und auf dem Handy fielen nur die Beschriftungen weg - uebrig
blieben sieben unbeschriftete Icons.
Struktur folgt jetzt dem tatsaechlichen Ablauf (finden -> bewerben ->
antworten) statt der Reihenfolge, in der die Seiten entstanden sind:
Bewerbungen | Jobangebote | Stellensuche v | Postfach | Vorlagen
"Stellensuche" buendelt als Dropdown, was Angebote erzeugt und filtert:
Suchprofil & Laeufe, Uebernommene Angebote, Blacklist - jeweils mit einer Zeile,
die erklaert, wozu der Punkt gut ist. Einstellungen, Benutzerverwaltung und
Abmelden wandern ins Konto-Menue rechts, wo Systemkram hingehoert. Die
taeglichen Wege (Bewerbungen, Jobangebote, Postfach) bleiben ein Klick weit weg.
Gestaltung: Die Leiste behaelt in beiden Themes ihr dunkles Navy - sie ist der
Rahmen des Werkzeugs und die einzige Flaeche, die sich beim Navigieren nie
aendert. Alles darin ist bewusst still (Slate auf Navy); der einzige Akzent ist
der aktive Punkt, markiert durch eine schmale leuchtende Schiene an der
Unterkante. Beim Hover deutet sich dieselbe Schiene gedaempft an.
Zaehler (ungelesene Post, offene Angebote) haengen jetzt an Klassen statt IDs,
weil sie in Leiste UND Mobilmenue erscheinen. Ein Controller bedient alle
Dropdowns: Klick oeffnet, andere schliessen, Escape und Klick daneben schliessen,
aria-expanded bleibt ehrlich. Fokus ist sichtbar, prefers-reduced-motion wird
respektiert, das Mobilmenue zeigt alle Punkte ausgeschrieben und gruppiert.
Der aktive Pfad kommt aus res.locals.pfad (Auth-Middleware). Die Navigation liegt
weiterhin in genau einem Partial - sie ist damit auf allen Seiten identisch;
nur die Login-Seite hat bewusst keine.
Geprueft mit echtem Browser (Screenshots): hell/dunkel, Dropdowns, Mobilmenue,
und der Aktiv-Zustand je Seite - Unterseiten wie /blacklist oder /jobsuche heben
korrekt "Stellensuche" hervor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Die Jobsuche lag in zwei Host-Skripten (jobsuche-cron.sh, jobsuche-remote-cron.sh)
aus der Ein-Benutzer-Zeit: Die Suchkriterien (sieben Staedte, Rollen, Buzzwords)
standen fest im Prompt, und importiert wurde mit EINEM globalen API-Token aus der
.env - der zufaellig dem Admin gehoerte. Auf der Multi-User-Plattform ist beides
hinfaellig.
Jetzt legt jeder Benutzer sein Suchprofil selbst fest (/jobsuche):
- Modus: regional / 100 % Remote / beides
- Staedte (die erste gilt als Wohnort und wird hoechstpriorisiert, max. 12)
- Feinschliff: zusaetzliche Begriffe, Ausschluesse
- Zeitplan: Wochentage + Uhrzeit, plus Button "Jetzt suchen"
Rollen und Technologien bleiben abgeleitet - aus dem Lebenslauf des Benutzers
(basis_dokumente), nicht aus einer gepflegten Liste. Admins koennen Profil und
Zeitplan eines Benutzers ueber /jobsuche?user=<id> mitpflegen (Link im Admin-Panel).
Aufteilung App/Host: Der Container hat weder claude noch ollama. Die App reiht
Laeufe daher nur in die Warteschlange ein (Tabelle suchlaeufe); der neue Runner
auf dem Host (scripts/jobsuche-runner.js, Cron alle 5 Min) arbeitet sie ab, baut
den Prompt je Benutzer aus dessen Profil + Lebenslauf und laeuft mit DESSEN
Zugangsdaten:
- BEWERBUNG_API_KEY = eigener API-Token des Benutzers (wird beim ersten Lauf
automatisch erzeugt), damit Treffer im richtigen Konto landen und durch dieselbe
Dedup-/Blacklist-Logik gehen,
- ANTHROPIC_BASE_URL/AUTH_TOKEN = eigener Ollama-Key des Benutzers ueber die
Anthropic-kompatible Schnittstelle von Ollama Cloud (https://ollama.com/v1/messages,
verifiziert: gueltiger Key -> 200, ungueltiger -> 401). Damit zahlt jeder seine
eigene Suche, statt alles ueber die Host-Subscription zu buchen
(JOBSUCHE_KI_AUTH=host stellt das alte Verhalten wieder her).
Ohne eigenen Ollama-Key oder ohne Lebenslauf bricht der Lauf mit klarer Meldung ab
statt still nichts zu tun; die Oberflaeche warnt vorab. Kein Stapeln: solange ein
Lauf offen ist, erzeugt ein weiterer Klick keinen zweiten. Verwaiste Laeufe
(Prozess weg) werden nach Zeitlimit als Fehler freigegeben.
Verifiziert mit zwei Benutzern: Zugriffsschutz (fremdes Profil -> 403), Speichern,
Warteschlange, automatische Key-Erzeugung, Prompt-Aufbau aus Profil + CV,
Ergebnis-Ruecklauf in die Oberflaeche, Fehlerpfade und die Faelligkeitslogik des
Zeitplans (Tag/Uhrzeit/bereits gelaufen).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der X-API-Key identifiziert den Benutzer; alle Endpunkte waren bereits auf
dessen user_id gescoped (27 Operationen geprueft). Zwei Luecken blieben:
- Die Aufloesung Token -> Benutzer nahm bei mehrdeutigem Token per LIMIT 1
einfach den ersten Treffer. Haetten zwei Benutzer denselben Token, saehe
der eine die Daten des anderen. Jetzt: fail closed (401 + Logeintrag),
und /einstellungen weist einen bereits vergebenen Token mit 409 ab.
- Ein leerer Token galt als Wert: wer seine Einstellungen einmal gespeichert
hatte, besass eine API_TOKEN-Zeile mit ''. Leere Werte matchen jetzt nie.
Neu in den Einstellungen (Abschnitt REST-API):
- "Neu generieren" erzeugt einen zufaelligen Token (32 Byte, crypto.get-
RandomValues); er wird nur ins Feld gefuellt und erst beim Speichern
aktiv, ein Fehlklick laesst sich also verwerfen.
- "Kopieren" legt den Token in die Zwischenablage; das Auge blendet ihn ein
(bestand bereits fuer Secret-Felder).
API aktualisiert:
- PUT /settings kannte nur name/adresse/kundennummer, GET lieferte aber alle
acht Felder. Jetzt schreibt PUT alle (email, telefon, ort, webseite,
geburtsdatum) und aendert nur die im Body uebergebenen Felder; die Antwort
enthaelt den neuen Stand.
Swagger:
- Beschreibung sagte "konfiguriert via Umgebungsvariable API_TOKEN" - das
gilt seit der Multi-User-Umstellung nicht mehr. Jetzt dokumentiert:
Token pro Benutzer aus den Einstellungen, Zugriff nur auf eigene Daten,
fremde id -> 404, unbekannter/leerer/mehrdeutiger Token -> 401.
- Settings-Schema um die fehlenden fuenf Felder ergaenzt.
Verifiziert mit zwei Benutzern und je eigenem Key: Lesen, Aendern, Loeschen,
Timeline, Anhaenge, E-Mails, Generierung, Jobangebote und Blacklist des
jeweils anderen liefern durchgaengig 404; Listen, Export und Statistik
zeigen nur eigene Daten; kollidierender Token -> 409; mehrdeutiger Token in
der DB -> 401 fuer beide.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
config.get() ist bei einem nicht gesetzten Schluessel auf process.env
zurueckgefallen. Da die Env-Variablen die Konfiguration des Admins
enthalten (Docker-Env: MAIL_*, CALDAV_URL, OLLAMA_API_KEY, API_TOKEN),
hat damit JEDER neu angelegte Benutzer ohne eigene Einstellungen
stillschweigend die Zugangsdaten des Admins geerbt:
- /einstellungen zeigte ihm die Zugangsdaten des Admins an.
- mailer/caldav isConfigured() war true -> der IMAP-Poller hat fuer den
neuen Benutzer das Postfach des Admins abgerufen und dessen E-Mails in
sein Konto einsortiert; CalDAV synchronisierte den Kalender des Admins.
- Der bezahlte Ollama-Key des Admins wurde mitbenutzt.
Jetzt:
- config.get() loest ausschliesslich die Zeilen des aktuellen Benutzers auf,
sonst den eingebauten Standard (nicht-geheime Werte wie Modell, Host,
Ports, Intervalle). Alle Credentials sind bei neuen Benutzern leer,
d. h. Ollama/E-Mail/CalDAV/API sind fuer sie aus, bis sie sich selbst
etwas eintragen.
- Noch per Env gesetzte Konfiguration wird einmalig in die Zeilen des
ADMIN uebernommen (importEnvIntoAdmin, Aufruf beim Boot nachdem
app_state existiert - in runMigration war das bei Neuinstallationen ein
No-op, weil die Tabelle dort noch nicht angelegt ist).
- config.ensureLoaded(user.id) beim Aufloesen der Session bzw. des
X-API-Key. config.get() ist synchron und liest den Per-User-Cache; ohne
Warmladen las ein Web-Request die Werte als "nicht konfiguriert". Das
hat bisher der env-Fallback verdeckt (er hielt zufaellig die Werte des
Admins) - ohne ihn muss die Config pro Request wirklich geladen werden.
Verifiziert gegen eine Kopie der Produktions-DB mit Sentinel-Env-Werten:
Admin behaelt seine kompletten Einstellungen, der zweite Benutzer sieht
ueberall leere Credentials, Mail/CalDAV sind fuer ihn inaktiv, und der
Env-API-Token wird nicht mehr als gueltiger X-API-Key akzeptiert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neu angelegte Benutzer hatten keine settings-Zeile - die legte bisher nur
die Migration von Hand fuer den Admin an, POST /admin/users dagegen nicht.
Der Code ging aber davon aus, dass die Zeile immer existiert:
- /vorlagen lieferte fuer neue Benutzer 500: die View greift auf
settings.name zu, bekam aber undefined.
- PUT /api/v1/settings verwarf Schreibzugriffe stillschweigend: das blanke
UPDATE traf null Zeilen und meldete trotzdem success.
Statt Platzhalter-Zeilen zu provisionieren ist "keine Zeile" jetzt ueberall
ein gueltiger Zustand - genau wie bei prompts, design und app_state:
- loadSettings() (neben loadPrompts()/loadDesign()) liefert {} statt
undefined; alle sechs Lesestellen gehen darueber.
- PUT /api/v1/settings ist ein Upsert, GET liefert {} statt leerem Body.
- Chat-Tool bewerbung_detail: JOIN auf jobangebote zusaetzlich ueber
j.user_id = b.user_id, wie derselbe JOIN an anderer Stelle.
Repariert auch bereits angelegte Benutzer ohne Backfill-Migration.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Auth via Session-Cookie + Login-Seite (scrypt, lib/password.js, sessions-Tabelle)
- AsyncLocalStorage (lib/context.js) propagiert aktuellen Benutzer durch alle Libs
- user_id auf allen Datentabellen (FK->users ON DELETE CASCADE), per-user PK/UNIQUE
(app_state, settings, prompts, design, jobangebote) und per-user Dateispeicher
(data/<dir>/<userId>/)
- Alle Queries in server.js + lib/api.js nach user_id scope-iert
- Pro-Benutzer-Konfiguration (Ollama/Mail/CalDAV/API-Token) in app_state,
Live gelesen via config.get(); Hintergrund-Loops (IMAP/CalDAV) iterieren alle Benutzer
- REST-API /api/v1: X-API-Key loest den Token zu einem Benutzer auf, Anfragen
operieren nur auf dessen Daten
- Admin-Panel /admin: Benutzer anlegen, Passwort zuruecksetzen, loeschen (mit Daten)
- Idempotente Migration (lib/migrate-multiuser.js + scripts/migrate-to-multiuser.js):
bestehende Daten werden dem Benutzer admin:admin zugeordnet
Co-Authored-By: Claude <noreply@anthropic.com>
Neue /einstellungen-Seite (Zahnrad im Header) mit allen bisherigen .env-Werten
(Ollama, E-Mail, CalDAV, REST-API), gespeichert in SQLite (app_state, cfg:-Prefix).
Libs lesen per lib/config.js zur Laufzeit statt beim Start -> Aenderungen
wirken sofort, kein Neustart. Bestehende .env wird beim ersten Start einmalig
migriert.
Co-Authored-By: Claude <noreply@anthropic.com>
Auf der Bewerbungsseite laesst sich jetzt auswaehlen, welche Unterlagen die KI
erzeugt. Standard bleibt beides; die letzte Auswahl wird gemerkt und ist beim
naechsten Generieren vorbelegt. Ohne Haken bricht das Formular ab statt still
beides zu erzeugen.
Nicht gewaehlte Dokumente werden gar nicht erst angefragt: JSON-Schema und
Skelett im Prompt werden auf die gewuenschten Teile reduziert, das spart einen
Gutteil der Generierungszeit. Gerendert wird ebenfalls nur das Gewaehlte - auch
wenn das Modell sich nicht an das Schema haelt und trotzdem alles liefert.
Fuer den E-Mail-Versand mitgedacht:
- Wird kein Lebenslauf erzeugt, fuehrt ihn das Anschreiben nicht mehr unter
"Anlagen" auf. Sonst kuendigt der Brief eine Anlage an, die nie mitgeht.
- Der Prompt bekommt zusaetzlich die Liste der Dateien, die der Bewerbungsmail
tatsaechlich anhaengen (inkl. Anschreiben selbst), damit der Begleittext keine
Unterlagen nennt, die nicht dabei sind.
- Die Anhang-Checkboxen im Mailformular ziehen sich aus den erzeugten Anhaengen,
greifen also automatisch.
Die REST-API kennt das Feld ebenfalls (POST .../generate, "dokumente"), inkl.
OpenAPI-Doku.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Der Lebenslauf kann jetzt zwischen zwei Layouts waehlen, und das Anschreiben
uebernimmt immer das Layout des Lebenslaufs - beide Dokumente kommen als ein Set
beim Unternehmen an.
"Social Media" ist die verspielte Variante nach der Media-Kit-Vorlage: getoente
Seite, rundes Portrait mit Blob, Schreibschrift-Zeile ueber einem grossen Namen,
Herzchen als Abschnittsmarker und Bullets, abgerundete Karten je Station,
Pill-Chips fuer Kompetenzen und Sprachen, Glitzer-Sterne als Deko. Bewusst laut -
gedacht fuer kreative und Social-Media-Stellen.
Die gesamte Farbwelt (Seite, Karten, Pills, Deko) wird aus der Akzentfarbe
abgeleitet, das Layout funktioniert also auch in Violett oder Tiefblau. Neue
Akzente Pink und Violett; das Layout bringt eigene Defaults mit (pink, rundes
Foto), sodass ein Wechsel sofort stimmig aussieht.
Neue Schriften (SIL OFL): Poppins fuer die geometrische Sans, Pacifico fuer die
Schreibschrift. Es werden nur die Fonts eingebettet, die das gewaehlte Layout
nutzt - ein klassischer Lebenslauf traegt Poppins/Pacifico nicht mit.
Herz und Sparkle sind als Bezier-Pfade gezeichnet, weil sich auf ein Herz-Glyph
in der Schrift nicht verlassen werden kann. Das neue Layout nutzt dieselbe
Mess- und Skalier-Mechanik wie das klassische, erbt also die Ein-Seiten-Garantie.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Akzentfarbe, Sidebar-Hintergrund, Foto (an/aus, eckig/rund) und Schriftgroesse
lagen als Konstanten im Renderer. Sie kommen jetzt aus lib/design.js und sind
unter Vorlagen waehlbar, inklusive Vorschau-PDF mit Musterinhalten - die zeigt
das Ergebnis, ohne dass die KI laufen muss.
Bewusst geschlossen gehalten: die Palette ist eine kurze Liste gedeckter Toene
(kein freier Color-Picker), Grauwerte und Grundlayout bleiben fest. Die
Schriftgroesse ist nur der Startwert der Seitenskalierung - der Lebenslauf passt
weiterhin garantiert auf eine Seite.
Das Theme wird durch die Render-Funktionen gereicht statt als Modulzustand
gesetzt, damit spaeter mehrere Bewerber parallel generieren koennen, ohne sich
gegenseitig das Design umzustellen.
Rundes Foto: das Bild fuellt den Kreis formatfuellend und zentriert (Clip statt
Skalieren), sonst wuerde ein Hochformat-Foto im Quadrat gestaucht.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rolle, Tonfall und Regeln der KI (Unterlagen, Chat, E-Mail-Antwort,
E-Mail-Absage, gemeinsame Stilregeln) lagen fest im Code. Sie liegen jetzt
als Defaults in lib/prompts.js und lassen sich auf der Vorlagen-Seite je
Prompt anpassen und wieder zuruecksetzen.
Nur die System-Prompts sind editierbar. Die User-Prompts tragen das
JSON-Skeleton, gegen das die Antwort geparst wird - ein Tippfehler dort
wuerde die Generierung lahmlegen, also bleiben sie im Code.
Gespeichert wird nur, was abweicht: ein Override ist eine Zeile in der neuen
Tabelle `prompts`, "Zuruecksetzen" loescht sie. Damit bleiben die Defaults im
Code die Wahrheit und wandern bei Updates automatisch mit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Im Antwort-Formular gibt es neben dem KI-Button jetzt ein Dropdown Antwort/Absage.
Für Absage formuliert die KI eine kurze, höfliche E-Mail: Bewerber hat eine andere
Stelle angenommen, bedankt sich, zieht sich aus dem Prozess zurück.
Co-Authored-By: Claude <noreply@anthropic.com>
Der Systemprompt enthielt früher nur die 12 jüngsten Bewerbungen (LIMIT 12),
daher fand die KI ältere Firmen wie QBS KLIMTAX nicht. Alle Bewerbungen in den
Prompt zu laden machte den Chat sehr langsam, da der Prompt bei jedem Turn
voll neu verarbeitet wird.
Jetzt nutzt der Assistent Ollama-Tool-Calling, um Datenbankdaten on demand
nachzuschlagen. Der Systemprompt bleibt klein und konstant (nur Profil +
Datum), unabhängig von der Anzahl der Bewerbungen.
- lib/chat.js: streamChat mit tools + runChat-Tool-Schleife (max 4 Runden)
- server.js: Tools suche_bewerbungen/list_bewerbungen/bewerbung_detail/
kommende_termine mit SQL-Queries; gatherChatContext auf Kern reduziert
- public/js/chat.js: Tool-Indikator ("durchsucht Bewerbungen…") im UI
Co-Authored-By: Claude <noreply@anthropic.com>
Unter den internen Notizen lassen sich nun private Anhänge hochladen.
PDFs öffnen per Klick direkt im Browser; Anhänge werden weder in den
PDF-Export noch in den Bewerbungsversand einbezogen.
Co-Authored-By: Claude <noreply@anthropic.com>
- Der automatisch generierte Verlaufs-Titel (aus der ersten Nachricht) erscheint
jetzt sofort in der Sidebar, nicht erst nach Reload. Der titel wird im
done-Event mitgeschickt und clientseitig gesetzt.
- Floating-Button-Icon durch eine saubere Chat-Bubble ersetzt.
Co-Authored-By: Claude <noreply@anthropic.com>
Der Assistent kannte nur Firma/Stelle/Status/Notizen und konnte daher keine
firmenspezifischen Fragen beantworten. gatherChatContext lädt jetzt pro
Bewerbung: Ort, interne Notizen, Stellenbeschreibung (bzw. verknüpftes
Jobangebot), Kontakt/Ansprechpartner und die E-Mail-Korrespondenz (Betreffe).
Zusätzlich wird der Lebenslauf des Bewerbers injiziert, damit Antworten auf
"was für mich wichtig" zugeschnitten werden können.
Relevante Bewerbungen = Termin-Bewerbungen + 12 jüngste (gebunden im Token-Budget).
Co-Authored-By: Claude <noreply@anthropic.com>
gatherChatContext() brach mit SQLITE_ERROR ab, weil "e.from_addr AS from"
das SQL-Schlüsselwort from als Alias nutzte. Der Fehler wurde im Route-Handler
geschluckt (context={}), sodass der Assistent keinen Zugriff auf Bewerbungen/
Termine/E-Mails hatte. Alias entfernt.
Zusätzlich erhält der System-Prompt jetzt das heutige Datum, damit der
Assistent "morgen"/"heute" aus den UTC-Termin-Zeitstempeln ableiten kann.
Co-Authored-By: Claude <noreply@anthropic.com>
Eigenes Chat-Interface mit SSE-Streaming gegen das hinterlegte Ollama-Modell,
gegroundet in den Bewerbungs-/E-Mail-/Termindaten des Nutzers.
- lib/chat.js: streamChat (Ollama stream:true, NDJSON-Token) + buildContextPrompt
- chat_threads/chat_messages Tabellen (CASCADE, Index)
- Routen: GET /chat, Thread-CRUD, POST /messages (SSE, AbortController)
- views/chat.ejs + public/js/chat.js + Floating-Button im Footer
- hasApiKey-Gating (503 ohne OLLAMA_API_KEY)
Co-Authored-By: Claude <noreply@anthropic.com>
The blacklist company slug now lives on the offers themselves. POST /joboffers
accepts firma_slug and stores it as supplied (deriving it from firma only when
omitted); it is persisted on jobangebote (schema + ALTER/backfill + index) and
returned in the Joboffer response. Blacklist matching prefers the client-supplied
slug over one derived from firma, so the indexing client controls the identity
used to block an offer. OpenAPI request/response schemas updated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Company matching relied on normText(), which kept legal-form suffixes,
spacing and umlaut spelling — so "Bosch GmbH", "Bosch AG" and "bosch gmbh"
did not match the same entry, making the blacklist unreliable.
Introduce firmaSlug(): transliterate German umlauts, strip diacritics and
trailing legal-form tokens (GmbH/AG/SE/KG/…), then kebab-join. Add a
firma_slug field to jobangebote_blacklist (schema + ALTER/backfill migration)
and match on it for typ firma/firma_stelle/auto, falling back to firma_norm
for legacy rows. POST /joboffers rejects blacklisted offers via matchBlacklist
automatically since offerSignature now carries the slug.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Vier mittlere Pipeline-Status ergänzt (nach Eingangsbestätigung, vor
Vorstellungsgespräch), inkl. Farben/Reihenfolge in Übersicht, Detailseite
und PDF-Export sowie REST-API-Validierung und Swagger-Enum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mehrere Labels pro Stelle (Regional, Remote-Deutschlandweit,
Homeoffice-Deutschlandweit), gespeichert als JSON-Array in einer neuen
labels-Spalte beider Tabellen (Migration). Geteiltes lib/labels.js mit
parse/serialize; wiederverwendbare Partials fuer Chips + Mehrfachauswahl.
- Web: setzen im Hinzufuegen-Modal, auf der Bearbeiten-Seite und im
Jobangebot-Bearbeiten-Formular; Anzeige als Chips in den Listen.
- Uebernahme eines Angebots traegt dessen Labels in die neue Bewerbung.
- REST-API: labels[] in /applications und /joboffers (GET/POST/PUT),
Filter ?label=…; OpenAPI/Swagger-Schemas + Enums erweitert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verbose Blöcke (Notizen + kompletter Verlauf) durch eine autoTable ersetzt:
eine Zeile pro Bewerbung mit Datum, Firma, Stelle, Art und letztem Status
(farbiges Badge). Export filtert/datiert nun nach dem effektiven Datum, also
der letzten Statusaenderung – eine im Juni gesendete, im Juli zum Gespraech
gewordene Bewerbung erscheint dadurch im Juli-Export. Modal-Auswahl nutzt
dieselben effektiven Monate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neue Antworten werden meist automatisch einer Bewerbung zugeordnet und
tauchten daher nie im Postfach auf - man bemerkte sie nicht. Eine Glocke
im Header zeigt jetzt auf jeder Seite die Zahl ungelesener empfangener
E-Mails (inkl. zugeordneter Antworten) und listet sie in einem Dropdown
mit Absender, Betreff, Auszug und Link zur Bewerbung bzw. zum Postfach.
- GET /api/notifications: Anzahl + neueste ungelesene Nachrichten.
- POST /api/emails/mark-all-read: alle als gelesen markieren.
- Postfach-Ansicht markiert unverknuepfte Mails als gelesen (leert die
Glocke), zugeordnete Antworten werden beim Oeffnen der Bewerbung gelesen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>