Commit Graph
100 Commits
Author SHA1 Message Date
thomasandClaude 69e6a29d87 PDF-Export: Autotable-Plugin zuverlaessig laden vor der Generierung
loadPdfLibraries() hat im "bereits ladend"-Zweig aufgeloest, sobald jsPDF
vorhanden war — das Autotable-Plugin (zweites Script) war dann aber noch
nicht angehaengt, sodass doc.autoTable undefined war und die Generierung
mit "doc.autoTable is not a function" fehlschlug. Die Polling-Bedingung
prueft jetzt auch jsPDF.API.autoTable, und generatePDF wartet zusaetzlich
auf loadPdfLibraries(), bevor das Dokument gebaut wird.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 02:35:37 +02:00
thomasandClaude 167960b91a Benutzerverwaltung: übersichtliches Layout mit klaren Aktionen
- Header mit Icon + Statistikleiste (Benutzer / Admins / Bewerbungen)
- Karten-Layout (rund, Shadow), Sektionen mit Icons
- Aktionen als einheitliche Icon+Label Buttons (Jobsuche, Anmelden als,
  Passwort, Löschen) statt gequetschter Textlinks, je eigene Farbe
- Avatar-Initial + Admin-Badge mit Icon in der Tabelle
- Passwort-Reset als eingeblendete Karte mit Abbrechen + Hinweis
- Relative Zeitangaben (Erstellt, Audit) mit Tooltips
- Audit-Sektion als eigene Karte

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 02:21:32 +02:00
thomasandClaude e0733666ae Admin kann Nutzer-Konto übernehmen (Impersonation) mit Rückwechsel
- 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>
2026-07-14 02:17:50 +02:00
thomasandClaude fd1db3970f Sicherheitscheck: Schwachstellen behoben
- Pfad-Traversal: safeFilename/containedPath-Helper, storeAnhang sanitizes
  filename, alle Download/Delete/Mail-Send-Routen pruefen Containment
- Stored XSS: serveInline entscheidet Viewable-Typ nur nach Extension,
  nicht nach client/seitigem MIME; nicht viewbare Typen werden als Download
  erzwungen. Upload fileFilter (Basis/Interne) + Extension-Validierung
  (Signatur/Foto leiten Ext aus MIME, blockieren .html)
- URL-Scheme-Allowlist (safeUrl) fuer quelle_url-hrefs gegen javascript:-XSS
- E-Mail-Iframe: Sandbox auf allow-same-only (kein allow-popups-to-escape)
- Sicherheits-Header: CSP, X-Content-Type-Options, X-Frame-Options,
  Referrer-Policy, COOP; x-powered-by aus; jsPDF self-hosted unter /vendor
- Session: Secure-Flag bei TLS, serverseitige absoluteexpiry, Scrypt async
  + Dummy-Verify gegen Timing/Enumerate + Login-Rate-Limit
- Open Redirect: /email/fetch nur same-origin Redirects
- SSRF: Validierung von OLLAMA_HOST/CALDAV_URL/MAIL_HOST gegen
  Metadata/Link-Local-BLock (localhost/LAN bleibt erlaubt)
- Globaler Error-Handler ohne Interna-Leak, env=production

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 02:09:17 +02:00
thomasandClaude da0b497972 Wortmarke in der Top-Navi einfarbig
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 01:43:49 +02:00
thomasandClaude bf789af62a Rebranding zu NextJobs (nextjobs.cc)
- Markenname 'Bewerbungs-Tracker' -> 'NextJobs' im gesamten UI,
  Footer (incl. nextjobs.cc Copyright), Login, Wortmarke in der
  Top-Navi (Next + Jobs-Gradient), Favicon, Swagger/OpenAPI-Titel,
  CalDAV-PRODID, Browser-Erweiterung (manifest, content, background),
  Runner-Log und Doku.
- Funktionale Begriffe (Bewerbungsunterlagen, -datum etc.) bleiben.
- Docker-Registry (git.hackner.dev) als Infrastruktur unberuehrt.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 01:40:26 +02:00
thomasandClaude cc2df90653 Favicon und moderner Schriftzug fuer die Top-Navi
- Passendes SVG-Favicon (Gradient-Mark wie in der Top-Navi) +
  theme-color, registriert im <head>
- 'Bewerbungs-Tracker' als zweifarbiger Schriftzug: Bewerbungs in
  Slate, Tracker im Sky->Blue-Gradient

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 01:33:26 +02:00
thomasandClaude fc28bee55f Footer dezenter, an Top-Navi angelehnt
Keine eigene Oberflaeche mehr (nur Seitenhintergrund + Haarlinie),
kleinere und stummere Schrift — gleiche ruhige Sprache wie die
Top-Navigation.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 01:27:46 +02:00
thomasandClaude 17d438d102 Jobsuche-Runner: nvm laden, damit Cron node findet
Node laeuft unter nvm, dessen bin-Verzeichnis nicht im Cron-PATH
lag. Jeder Cron-Tick schlug mit `node: command not found` fehl,
weshalb manuell angeforderte Suchlaeufe ewig im Status 'angefordert'
(wartet) haengen blieben. nvm.sh wird jetzt vor dem Aufruf gesourced.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 01:04:59 +02:00
thomasandClaude 9f000e2719 Filterbereich entfernt, Suche + Aktionen als Toolbar zusammengefasst
- Monat/Jahr-Filter geloescht (Server-Filter bleibt via URL nutzbar)
- Suchfeld und die Buttons 'PDF Export' und 'Bewerbung hinzufuegen'
  in einer gemeinsamen Toolbar zusammengefasst, als Pill-Buttons
- Tote filterForm-CSS und -JS entfernt

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 00:56:24 +02:00
thomasandClaude 98a644524b Statistik auf eigene Seite ausgelagert, umfangreich und modern
- 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>
2026-07-14 00:48:08 +02:00
thomasandClaude 4fd843318f Header/Navigation-Styling ueberarbeitet
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-14 00:34:49 +02:00
thomasandClaude Opus 4.8 1aa902bdce Top-Navigation: neu strukturiert, mit Aktiv-Zustand und Dropdowns
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>
2026-07-13 23:55:58 +02:00
thomasandClaude Opus 4.8 9418750061 Jobsuche als Feature der App: Suchprofil pro Benutzer statt Cron-Prompt
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>
2026-07-13 23:28:56 +02:00
thomasandClaude Opus 4.8 1e14738258 API: Zugriff strikt auf den Besitzer des API-Keys, Key generieren/anzeigen
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>
2026-07-13 22:56:21 +02:00
thomasandClaude Opus 4.8 b6cb94fdb5 Einstellungen strikt pro Benutzer: kein process.env-Fallback mehr
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>
2026-07-13 22:31:08 +02:00
thomasandClaude Opus 4.8 b2e884d9b3 Persoenliche Angaben: fehlende settings-Zeile ist ein gueltiger Zustand
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>
2026-07-13 22:16:45 +02:00
thomasandClaude 0371aa85a5 Multi-User-Plattform: jeder Benutzer hat eigene, isolierte Daten
- 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>
2026-07-13 21:57:50 +02:00
thomasandClaude 97b48f9841 Einstellungen-Seite: .env-Werte in DB (app_state), keine .env mehr
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>
2026-07-13 20:57:31 +02:00
thomasandClaude 41887dd56c Status "Absage von meiner Seite" ergaenzt
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-13 12:51:37 +02:00
thomasandClaude 0c866805d3 Dark Mode folgt Browser-Einstellung (prefers-color-scheme), Toggle entfernt
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-13 12:34:56 +02:00
thomasandClaude e97d1b3687 Persoenliche Angaben unter Vorlagen statt hardcoded/Settings-Modal
- settings-Tabelle um email/telefon/ort/webseite/geburtsdatum erweitert
  (CREATE + ALTER-Migration + Default-INSERT)
- Vorlagen-Seite: neuer Bereich "Persoenliche Angaben" (8 Felder),
  POST /vorlagen/persoenlich; Settings-Modal + Zahnrad-Button entfernt
- Generierung: Kontakt kommt verbindlich aus den Settings (buildHeader
  laesst gespeicherte Werte ueber die KI-Extraktion gewinnen), bewerber-
  Fakten + Prompt angepasst; Design-Vorschau zeigt echte Kontaktdaten
- main.js: Settings-Modal-Handling entfernt; /api/settings bleibt bestehen

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-13 11:41:32 +02:00
thomasandClaude Opus 4.8 d1116d522d Vor dem Generieren waehlen: Anschreiben, Lebenslauf oder beides
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>
2026-07-13 02:18:02 +02:00
thomasandClaude Opus 4.8 66366659d2 Social-Design: Herz-Abstaende korrigiert + Icons fuer die Kontaktdaten
Die Abschnittsueberschriften wurden mit align:'center' zentriert. jsPDF misst
dafuer mit getTextWidth(), das den charSpace ignoriert, rendert den Text aber
gesperrt - er lief nach rechts, waehrend die Herzen symmetrisch sassen. Ergebnis:
links sichtbar mehr Luft als rechts. Die Ueberschrift wird jetzt selbst zentriert,
gemessen an der tatsaechlichen Tinte (Sperrung zaehlt nur zwischen den Glyphen,
nicht nach dem letzten), und die Herzen haengen an den echten Textkanten.

Kontaktdaten bekommen passende Icons: Umschlag, Telefon, Map-Pin, Globus,
Kalender, Ausweis. Als Vektoren gezeichnet, weil kein Icon-Glyph in der Schrift
vorausgesetzt werden kann; sie skalieren mit der Seitenskalierung mit.
buildContactLines liefert dafuer jetzt {typ, text} - die klassische Sidebar nutzt
weiterhin nur den Text und bleibt unveraendert schlicht.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 01:30:56 +02:00
thomasandClaude Opus 4.8 0e1d9b3322 Zweites Lebenslauf-Design "Social Media" + Anschreiben folgt dem Layout
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>
2026-07-13 01:15:02 +02:00
thomasandClaude Opus 4.8 9f7349af4e Design der Unterlagen unter Vorlagen einstellbar
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>
2026-07-13 00:51:36 +02:00
thomasandClaude Opus 4.8 65a5943993 KI-Prompts unter Vorlagen editierbar statt hardcoded
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>
2026-07-13 00:09:03 +02:00
thomasandClaude 0a0b0b4724 E-Mail: KI-Absage generieren (andere Stelle angenommen)
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>
2026-07-10 20:51:37 +02:00
thomasandClaude 0c7502cfd7 KI-Chat: Bewerbungsdaten per Tool-Calling nachschlagen statt in Prompt stopfen
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>
2026-07-09 05:41:14 +02:00
thomasandClaude 306026b0a1 Interne Notizen: Anhang-Upload repariert + schöneres Design
Der Upload- und Lösch-Formular war versehentlich inside des
Bewerbungsdaten-Formulars verschachtelt – HTML lässt verschachtelte
Formulare nicht zu, weshalb der Browser das multipart-Formular
ignorierte und die Datei nie gesendet wurde. Formulare jetzt outside
des Speichern-Formulars. Dazu ansprechendere Karte-Optik mit
Öffnen-/Download-/Löschen-Aktionen.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-09 04:46:33 +02:00
thomasandClaude 0b44f04456 Interne Notizen: eigene Anhänge (PDFs/Dateien) nur für dich
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>
2026-07-09 04:37:52 +02:00
thomasandClaude 40b4de12e1 KI-Chat: Verlaufs-Titel live aktualisiert + sauberes Floating-Icon
- 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>
2026-07-07 14:57:35 +00:00
thomasandClaude 26a61aa302 KI-Chat: Verlaufs-Titel inline umbenennbar
Pro Verlauf ein Stift-Button (neben dem Löschen-Button). Klick macht den
Titel inline editierbar; Enter/Blur speichert via PATCH /chat/api/threads/:id,
Escape bricht ab. Auch für client-seitig neu angelegte Threads.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 14:55:06 +00:00
thomasandClaude d2545c4faf KI-Chat: Warte-Indikator als gestaffelte Bounce-Animation
Drei Punkte mit gestaffeltem Animation-Delay (klassische Tipp-Animation),
inkl. prefers-reduced-motion-Fallback (statisch).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 14:48:04 +00:00
thomasandClaude 61b67e0843 KI-Chat: Verlauf wird beim Laden angezeigt + einzelne Chats löschbar
- initialMessages-JSON wurde mit <%= %> HTML-escaped, wodurch JSON.parse
  fehlschlug und die Nachrichtenfläche beim Öffnen eines Verlaufs leer blieb.
  Jetzt <%- %> mit < -> < escapet, JSON bleibt parsebar.
- Pro Verlauf ein Trash-Button in der Sidebar (DELETE /chat/api/threads/:id).
  Löscht den Verlauf, entfernt die Zeile, setzt bei aktivem Verlauf auf
  neuen Chat zurück. Auch für client-seitig neu angelegte Threads.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 14:42:29 +00:00
thomasandClaude cf6cf9ee33 KI-Chat: reichhaltigerer Kontext (Stellenbeschreibung, Korrespondenz, Profil)
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>
2026-07-07 14:36:45 +00:00
thomasandClaude cc397e8f71 KI-Chat: Markdown-Rendering, Left-Alignment, kompaktere UI, "Neuer Chat" fix
- Markdown-Renderer (Headings/Listen/**bold*/*italic*/`code`/Links) für
  Assistenten-Antworten; HTML wird vorher escaped (keine Injektion).
- Antworten explizit linksbündig, Inhalt getrimmt (keine Leerzeilen oben/unten).
- Initiale Nachrichten als JSON-Blob, einheitlich via chat.js gerendert.
- Chat-Container kompakter (Höhe/Spacing reduziert).
- "Neuer Chat" setzt clientseitig auf leeren Zustand zurück (vorher lud der
  Button nur den letzten Thread neu).

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 14:28:54 +00:00
thomasandClaude c389d3d5ec KI-Chat: Kontext-Injection repariert (SQL from-Alias) + Datum im Prompt
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>
2026-07-07 14:21:13 +00:00
thomasandClaude 8656e92f3c KI-Chat: Eingabefeld bei neuem/leerem Chat nicht mehr disabled
Das Textfeld war ohne vorhandenen Thread deaktiviert, sodass die erste
Frage (die den Thread erst anlegt) nicht eintippbar war. disabled entfernt;
chat.js legt den Thread weiterhin beim ersten Absenden an.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-07 14:14:00 +00:00
thomasandClaude ead70efa49 KI-Chat: interaktiver Bewerbungs-Assistent (Ollama-Streaming)
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>
2026-07-07 14:07:38 +00:00
thomasandClaude Opus 4.8 17df46cb6c Reply-Mail: Zeilenumbrüche als <br> statt white-space:pre-wrap
Ausgehende Antwort-Mails (multipart/alternative) rendern den HTML-Teil
bisher über white-space:pre-wrap auf rohen Zeilenumbrüchen ohne <br>.
Beim ersten Empfang korrekt, aber sobald der Empfänger-Client (Outlook)
die Mail zitiert, wird der pre-wrap-Stil verworfen und die nackten
Newlines zu Leerzeichen kollabiert -> unser formatiertes Anschreiben wird
im Zitat zur zusammenhanglosen Textwand. Explizite <br> überleben diesen
Round-Trip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 09:18:44 +00:00
thomasandClaude Opus 4.8 4e1043897d Blacklist: Firmen schreibweisen-robust matchen (eine Firma nur einmal)
sameCompany() vergleicht Firmen-Slugs jetzt per Gleichheit ODER führendem
Bindestrich-Präfix (>=2 Tokens), sodass Kurzname und voller Firmenname
derselben Firma als gleich gelten (z. B. "it-problemloeser" =
"it-problemloeser-verwaltungs-und-handels", "stadtwerke-essen" =
"stadtwerke-essen-netz"). firmaMatch() nutzt das für die Blacklist-Typen
firma/firma_stelle/auto. Distinkte Firmen mit nur gleichem ersten Wort
("meyer-it" vs "meyer-logistik") bleiben getrennt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 07:25:04 +00:00
thomasandClaude Opus 4.8 7e0c27341a CalDAV: HTML-Entities in Terminfeldern dekodieren
Manche Kalender speichern SUMMARY/LOCATION/DESCRIPTION mit wörtlichen
HTML-Entities (z. B. "Vorstellungsgespr&#228;ch &#8211; dot.haus"). Diese
wurden roh angezeigt. Beim Parsen werden sie jetzt in echte Zeichen
dekodiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 22:58:07 +02:00
thomasandClaude Opus 4.8 22f967c753 Carry firma_slug on job offers, supplied by the ingesting client
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>
2026-07-06 22:42:41 +02:00
thomasandClaude Opus 4.8 f935e00b53 Add robust company slug for job-offer blacklist matching
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>
2026-07-06 22:25:59 +02:00
thomasandClaude Opus 4.8 4e5933890f Footer: Copyright hackner.dev ergänzen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:50:04 +02:00
thomasandClaude Opus 4.8 3b694edac6 Dark-Mode vor erstem Paint setzen (kein Light-Mode-Flash mehr)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:46:49 +02:00
thomasandClaude Opus 4.8 30f83d433a Gesendete Anhänge in Korrespondenz anzeigen (email_anhaenge verknüpfen)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:47:07 +02:00
thomasandClaude Opus 4.8 4003279453 Anhänge (Unterlagen + Mailanhänge): PDF im neuen Tab öffnen statt Download
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:42:12 +02:00
thomasandClaude Opus 4.8 d99c8885fc Antwort-Betreff: kein doppeltes Re:/AW: voranstellen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 13:55:43 +02:00
thomasandClaude Opus 4.8 7dddbe18b1 Antwort-Mails: hinterlegte Anlagen mitschicken (Standard: keine)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 11:48:39 +02:00
thomasandClaude Opus 4.8 c2c51f9fca Neue Bewerbungs-Status: In Bearbeitung, Interessiert, Warten auf Rückmeldung/meine Antwort
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>
2026-07-06 10:06:56 +02:00
thomasandClaude Opus 4.8 03760d293f Labels für Stellen (Bewerbungen + Jobangebote), inkl. API & Swagger
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>
2026-07-05 15:23:44 +02:00
thomasandClaude Opus 4.8 d1c6743d2f PDF-Export: keine zerrissenen Zeilen mehr + HTML-Entities dekodieren
rowPageBreak 'avoid' verhindert das Splitten einer Zeile am Seitenumbruch,
das bisher Datum/Art/Status auf der Folgeseite leer liess. Firma/Stelle/Art
werden HTML-dekodiert (z.B. "&amp;" -> "&"), leere Werte zeigen "—".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 14:20:27 +02:00
thomasandClaude Opus 4.8 78b1ea78d0 PDF-Export: kompakte Tabelle, Filter nach letztem Status-Datum
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>
2026-07-05 14:12:24 +02:00
thomasandClaude Opus 4.8 b269dcaecc Footer immer am Seitenende (Sticky) und Text auf "Bewerbungssystem"
Body als Flex-Column, main mit flex-1 drueckt den Footer nach unten.
Footer-Jahr serverseitig gerendert (immer aktuell).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:40:55 +02:00
thomasandClaude Opus 4.8 82431490a9 Startseite: Bewerbungsliste als klappbare Sektionen (eine Zeile pro Bewerbung)
Tabelle auf Grid-Layout umgebaut. Status-Gruppen und der Detailbereich
(Verlauf/Notizen) je Bewerbung sind animiert zuklappbar, Default zugeklappt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 13:36:29 +02:00
thomasandClaude Opus 4.8 7cc527b013 Glocke im Header ganz nach rechts verschieben
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:48:36 +02:00
thomasandClaude Opus 4.8 a44b319e7e Benachrichtigungsglocke fuer ungelesene E-Mails
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>
2026-07-04 23:39:05 +02:00
thomasandClaude Opus 4.8 b1531663bb E-Mails als HTML anzeigen; Antworten zitieren Vornachricht (auch KI)
- HTML-Mails werden in einem sandboxed iframe (ohne allow-scripts)
  gerendert statt als Quelltext angezeigt; Höhe an Inhalt angepasst,
  Skripte/Tracking laufen nicht, Layout bleibt isoliert. Reine
  Text-Mails weiterhin als pre-wrap. Gilt für Postfach und Bewerbung.
- Antwortformular ist wie im Mailclient mit der zitierten Vornachricht
  vorbelegt (Attribution + "> "-Zeilen), Cursor darüber.
- KI-Antwort hängt das Zitat unter den generierten Text und erhält den
  bereinigten Klartext (statt HTML) als Eingabe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:06:59 +02:00
thomasandClaude 7ceff39364 Ignore local Indeed test fixture (indeed_example.txt)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 20:10:27 +02:00
thomasandClaude Opus 4.8 95b5b8510d Add a two-way application calendar (CalDAV / SOGo)
New lib/caldav.js speaks CalDAV over Basic auth (shared mail account):
reads events in a window, and creates/updates/deletes iCalendar VEVENTs
with a reminder alarm. DST-correct Europe/Berlin <-> UTC handling.

Appointments (Vorstellungsgespräch / general) are managed per application
in a new "Termine" section and mirrored to the SOGo calendar; recording a
"Vorstellungsgespräch" status suggests a prefilled calendar entry
(confirm + click). A dashboard widget lists upcoming appointments. A
background poller reconciles remote edits/deletions via the collection
ctag. Config: CALDAV_URL (+ CALDAV_ALARM_MIN, CALDAV_POLL_MS); disabled
gracefully when unset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 20:06:38 +02:00
thomasandClaude Opus 4.8 89c8c6db97 Delete e-mails from the Postfach; searchable assignment picker
Each Postfach e-mail gets a Delete button (POST /postfach/:id/delete),
which also removes stored attachment files. The plain assignment
dropdown is replaced by a searchable combobox that filters applications
by company, position and city, with keyboard navigation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 18:55:23 +02:00
thomasandClaude Opus 4.8 26872ece08 Choose enclosed attachments before generating; drop instant-generate
The generate form now lets the user pick which extra attachments
(basis_anhaenge) to enclose — none selected by default. Only the chosen
ones are attached, and their names are passed to the LLM so the cover
letter's "Anlagen" list and wording reflect exactly what is enclosed.

Job offers keep only "Als Bewerbung übernehmen" (draft first); the
"Übernehmen & KI-Unterlagen" button and its route are removed.

REST API: POST /applications/:id/generate accepts an optional `anlagen`
array of attachment IDs (Swagger updated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 17:36:14 +02:00
thomasandClaude Opus 4.8 360e04d677 Confirm before sending; add address + contact to job offers
"Bewerbung senden" now asks for an extra confirmation (with the
recipient) before dispatching.

Job offers gain adresse and ansprechpartner fields — shown, editable,
and carried through the REST API upsert (Swagger updated). Taking an
offer over now writes the employer address (street, number, city) and
the contact person into the application's AI notes (llm_notizen), so the
LLM can use them for the letter's Anschriftfeld and salutation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 17:10:13 +02:00
thomasandClaude Opus 4.8 6a5bdfa6f8 Split job offers into Open and Taken-over pages
/jobangebote now lists only open offers; taken-over offers move to a
dedicated /jobangebote/uebernommen page showing which applications were
created (with link, status and date). A tab bar switches between both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 14:43:12 +02:00
thomasandClaude Opus 4.8 bc112e2627 Let job offers carry a full description into AI generation
Job offers can now be edited in the UI — a large description textarea
(any length) plus the other fields — so a full job posting can be pasted
before turning it into an application. Taking an offer over copies its
complete description into the application's stellenbeschreibung, which is
exactly the text handed to the LLM.

New primary action "Übernehmen & KI-Unterlagen" creates the application
and immediately starts generation; "Nur als Entwurf" keeps the plain
draft path. New routes: POST /jobangebote/:id/bearbeiten and
/jobangebote/:id/uebernehmen-generieren.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 14:07:30 +02:00
thomasandClaude Opus 4.8 dae077486f Add a job-offer blacklist with URL-based de-duplication
Offers are now de-duplicated by normalized URL (tracking params stripped)
in addition to (quelle, external_id), so the same posting never lands
twice — even re-scraped under a new id. Deleting an offer (web or API)
auto-blacklists it, so it can never reappear.

lib/blacklist.js provides shared normalization + matching. Manual entries
can block a URL, a whole domain, a company, or a company+title posting
(gender-marker tolerant). New /blacklist page lists and manages entries.

REST API: GET/POST /joboffers/blacklist, DELETE /joboffers/blacklist/{id};
POST /joboffers returns 409 when blacklisted; DELETE /joboffers/{id}
auto-blacklists (opt out with ?blacklist=false). Swagger updated with the
new paths and schemas.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 13:33:12 +02:00
thomasandClaude Opus 4.8 1655427b4c Drop the underline rules under the CV sidebar headings
The sidebar section labels no longer carry a short accent underline;
tracking and weight provide the structure, keeping the left column clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 12:50:50 +02:00
thomasandClaude Opus 4.8 fa25307791 Flatten a multi-line address in the cover-letter footer
doc.text breaks on newlines, which stacked the street above the city in
the footer contact strip. Split the address on newlines and commas and
rejoin as one line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 12:39:36 +02:00
thomasandClaude Opus 4.8 8281128adc Restyle the cover letter to match the CV; refine the résumé head
Cover letter: a big confident name + role in tracked caps, the shared
hairline-with-accent motif, and a centred contact footer strip (moved
out of the header) so it reads as one set with the monochrome résumé.

Résumé: drop the divider rule above the Profil heading — the section
headings carry the structure on their own.

Prompt: forbid the headline from using a protected professional title
the applicant has not verifiably earned (e.g. Fachinformatiker,
Ingenieur, Meister); fall back to a neutral activity label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 12:33:32 +02:00
thomasandClaude Opus 4.8 390f9a643b Redesign the Lebenslauf as a monochrome two-column layout
Replace the single-column résumé with a two-column layout: a tinted
grey sidebar (photo, contact, Kernkompetenzen, Sprachen, Interessen)
and a wide main column with a large name header, Profil, a timeline
Berufserfahrung, and education. Strictly black-and-white — the former
navy accent becomes near-black across both the résumé and the cover
letter so the set stays consistent.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 12:12:48 +02:00
thomasandClaude 35b60bc2ca Stop long company names from pushing the actions column off-screen
The overview table used auto layout with whitespace-nowrap on Firma/Stelle,
so a long name expanded the columns beyond w-full and the overflow-hidden
container clipped the right edge — hiding Bearbeiten/Löschen. Switch to
table-fixed with a colgroup for stable column widths, allow Firma/Stelle to
wrap (break-words, align-top), and make the wrapper overflow-x-auto so the
table scrolls horizontally on narrow viewports instead of clipping.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 00:44:45 +02:00
thomasandClaude 89991d5c06 Show job offer anzeige_datum prominently on the card
The posting date was only a small gray span at the bottom and easy to
miss. Promote it to a labeled "Anzeige:" line right under firma/stelle/ort,
and add "Bewerbung vom: <datum>" in the footer row for offers that have
been turned into an application.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 23:29:07 +02:00
thomasandClaude 955e975ace Show job offer kontakt_email prominently on the card
Move the mailto: link up next to firma/stelle/ort as a labeled
"Kontakt:" line so the contact email is directly visible on each job
offer, instead of a small link below the description.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 22:29:55 +02:00
thomasandClaude cb1115ef95 Add kontakt_email to job offers
- New kontakt_email TEXT column on jobangebote (+ ALTER TABLE migration)
- Accept kontakt_email in POST /api/v1/joboffers (create + upsert) and
  document it in the OpenAPI spec
- Render a mailto: link on the /jobangebote page and include it in the
  curl example

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 22:16:55 +02:00
thomasandClaude faa3a45815 Clarify the two job-offer dates in the page docs
The curl example only showed anzeige_datum, which read as if there's a
single date. Add an explicit list distinguishing Anzeige (anzeige_datum,
sender-provided) from Einspielung (created_at, server-set automatically),
and a comment in the curl body that created_at is not to be sent.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 21:58:24 +02:00
thomasandClaude 5d6fed7041 Show job offer posting date (anzeige_datum) alongside ingestion date
- Add anzeige_datum DATE column to jobangebote (with ALTER TABLE migration
  for pre-existing tables) — the original posting date supplied by the
  third party
- Accept anzeige_datum in POST /api/v1/joboffers (create + upsert) and
  document it in the OpenAPI spec
- Render both on /jobangebote: "Anzeige: <anzeige_datum>" and
  "Einspielung: <created_at>"

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 21:46:12 +02:00
thomasandClaude 674dba40fa Fix empty Swagger UI: drop invalid presets/layout config
SwaggerUIBundle.presets.apisAndSaver does not exist (undefined), which broke
initialization and rendered nothing. Use the minimal canonical config (url,
dom_id, deepLinking, persistAuthorization) and drop the now-unused standalone
preset script.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 21:37:00 +02:00
thomasandClaude 88875dbc33 Add third-party REST API (/api/v1) + Swagger UI and Jobangebote page
- REST API under /api/v1 with X-API-Key auth (API_TOKEN), documented with
  OpenAPI 3.0; Swagger UI at /swagger, spec at /swagger.json
- Endpoints: applications CRUD + timeline, attachments/emails download,
  generation trigger/status, settings, statistics, export, templates, joboffers
- Jobangebote page (/jobangebote) listing offers ingested via the REST API,
  with "Als Bewerbung übernehmen" and delete actions; header nav + badge
- jobangebote table with (quelle, external_id) upsert for third-party ingestion

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 21:30:33 +02:00
thomasandClaude Opus 4.8 0af731695a Size CV heading accent bar to the word width
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 20:26:28 +02:00
thomasandClaude 482d8d86fa Shorten cover email + strip non-Latin script from AI text
The generated Begleit-E-Mail was too long for an HR reader and sometimes
contained stray non-Latin characters (e.g. CJK "包括"). Make the email a
minimal accompanying note (no repetition of the cover letter, max ~3
sentences), add a German-only/Latin-script instruction to both the
document and reply prompts, and add a backstop sanitizer in str() that
strips any non-Latin script characters the model still emits.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 19:28:34 +02:00
thomasandClaude fd11facadd Add centered live company search to the overview
A modern search bar at the top center of the index page filters the
applications table live by firma/stelle. Status group headers hide when
their group has no visible rows, per-group counts update with the
filtered result, and an empty-state row shows when nothing matches.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 19:17:54 +02:00
thomasandClaude 106b184fda Let postfach e-mails expand to full body
The unlinked-e-mail cards clipped the body to a preview height. Add a
toggle to show the complete body inline and collapse it back.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 19:12:24 +02:00
thomasandClaude 4418c15553 Add Postfach page to manually assign unlinked e-mails
Incoming e-mails that IMAP matching could not link to an application
(bewerbung_id IS NULL) were invisible. Add a dedicated page listing
them with a dropdown of existing applications to assign each one by
hand. A header badge on every page shows the count of unlinked e-mails
via a small /api/emails/unassigned-count endpoint.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 19:07:54 +02:00
thomasandClaude 931bb85c6a Don't name current employer in cover letter
The LLM was naming the current employer (e.g. "IT-Problemlöser GmbH")
in the cover letter text. Add a prompt rule to keep the company name out
of the Anschreiben; the CV still lists employers as before.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 18:50:59 +02:00
thomasandClaude 57eb19632d Always include languages (esp. English) in CV
Languages were dropped when not explicitly required by the job posting.
Sprachkenntnisse are standard CV content in Germany, so always include
them from the base documents regardless of job relevance.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 18:43:55 +02:00
thomasandClaude Opus 4.8 b1c92dd77b Guard against duplicate applications + capture jobs on any website
Duplicate safeguard: before creating an application (manual add and
browser import) the server checks for an existing one for the same job —
matched on a normalised source URL (job-id params like Indeed's jk pin
the posting across paths/tracking) or an identical company + role
(case/umlaut/whitespace-insensitive). On a match it returns 409 with the
matches; the web form and the extension show the existing entry and
re-submit with force=true only if the user confirms. Not a hard block, so
legitimate re-applications stay possible.

Universal capture: the extension popup becomes an editable capture form
that works on any site. It extracts the active page on demand (schema.org
JobPosting JSON-LD -> OpenGraph/meta -> h1/title/selection -> canonical
URL), lets the user review/correct, and sends. The import route is now
source-agnostic and derives the application source (art) from the URL
instead of hardcoding Indeed; the Indeed on-page button remains as a fast
path. Adds scripting/activeTab permissions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:32:17 +02:00
thomasandClaude Opus 4.8 d8b552e57d Ban hollow filler and self-labels in generated prose
The model produced awkward AI-tells like "Als Muttersprachler kommuniziere
ich serviceorientiert und klar - sowohl mit Anwendern als auch im Team".
Add an explicit style rule (system prompt, shared prose guidance, and the
AI-reply prompt) that forbids empty buzzword pairs and self-labels such as
"Als Muttersprachler ...", quoting the offending sentence as a negative
example. Language proficiency belongs only in the CV's Sprachen section;
every trait must come out of a concrete task or result.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:11:31 +02:00
thomasandClaude Opus 4.8 4a896e72e3 Restyle cover letter to match the CV as one application set
Bring the Anschreiben into the résumé's visual language: navy name,
tracked role line, the shared hairline-plus-accent-bar rule under the
letterhead, and a navy bold subject line as the letter's headline. Keep
strict DIN-5008 structure (recipient block, right-aligned date, subject,
salutation/closing, signature space, enclosures) and near-black body for
readability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 18:04:52 +02:00
thomasandClaude Opus 4.8 ddcf5edbc3 Restyle CV: navy accent system + competency-first structure
Rework the résumé as an HR-first document. Move Kernkompetenzen (renamed
from Fähigkeiten) directly under the Profil so the role-match is visible
in the first seconds, ahead of the reverse-chronological experience.

Introduce one restrained deep navy-slate accent used only for structure:
the name, section labels, a short accent bar starting each section rule,
and small square bullet markers — a coherent, precise motif. Body text
stays near-black for a crisp, ATS-safe read. Verified on the with-photo
and no-photo layouts.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 17:58:22 +02:00
thomasandClaude Opus 4.8 05357acddb Fix monthly PDF export to include only the selected month
The month dropdown carried only the month (e.g. "07") while the year was
a separate select; the export required month AND year, so a month-only
selection fell through to exporting every application. Encode the year in
the month option value ("YYYY-MM"), parse it client-side to always send
the exact month+year, and harden /api/export so a month can never fall
through to "export all".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 16:39:46 +02:00
thomasandClaude Opus 4.8 0b44f61410 Add e-mail system: send applications, receive & reply to responses
Send the complete application (body + generated attachments) to a
user-entered address via authenticated SMTP submission through the
account's own server, so it applies DKIM and uses its reputable IP/PTR —
required for deliverability here (domain publishes SPF -all, DMARC
p=reject). From/Return-Path stay aligned on the sending domain.

Reply e-mails are polled over IMAP, parsed, stored and matched to the
right application (via In-Reply-To/References, then sender address);
their attachments are saved and downloadable. The detail page gains a
correspondence thread with compose, threaded reply, and an AI-drafted
reply the user can edit before sending.

New: lib/mailer.js (nodemailer + imapflow + mailparser), generateEmailReply
in lib/documents.js, emails/email_anhaenge/app_state tables, background
poller + manual fetch. Credentials come from MAIL_* env vars (.env, not
committed / not in the image).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:59:32 +02:00
thomasandClaude Opus 4.8 3c553cadf5 Generate an editable norm-compliant cover email per application
Alongside the tailored PDFs, the LLM now produces a short, DIN-style
Begleit-E-Mail (subject + body) grounded in the same data: same
salutation as the cover letter, the target role, a line or two of core
motivation, a pointer to the attached documents, and a friendly close.

Persisted in new bewerbungen columns (email_betreff, email_anschreiben)
and surfaced in an editable subject field + textarea on the detail page,
so the applicant can tweak it before sending; a copy button puts subject
and body on the clipboard. Values are stored raw and HTML-escaped on
render to avoid double-escaping.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:20:56 +02:00
thomasandClaude Opus 4.8 0bfa5683fb CV: tuck Profil up and flow it around the photo
Drop the dead gap between the contact block and the first section: the
Profil heading and its opening lines now sit beside the photo's lower
flank (hairline stopping short of the photo) and spill to full width
once past the photo's bottom edge. No-photo layout unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 15:10:25 +02:00
thomasandClaude 5090dbbeb6 Anrede: keep full surname incl. particles (Al/El/van/von/de)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 14:52:31 +02:00
thomasandClaude 7d2640f5d5 CV: enlarge photo 20%, drop header rule above Profil
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 14:45:12 +02:00
thomasandClaude 990a663cdd Optional applicant photo in CV header
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 14:39:13 +02:00
thomasandClaude e914dee855 Anrede: last name only, strip first name (keep titles/particles)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 14:18:02 +02:00
thomasandClaude 86f94ce642 Embed Lato typeface for CV and cover letter (ATS-safe, German glyphs)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 13:50:39 +02:00