- Startseite zeigt alle anstehenden Termine (vorher auf 6 begrenzt)
- Termine lassen sich inline bearbeiten (CalDAV-Update, Neu-Anlage bei 404/412)
- Neuer Bewerbungsstatus Telefonat (Listen, Farben, API, PDF, Statistik)
Co-Authored-By: Claude <noreply@anthropic.com>
Zwei neue, pro Benutzer gespeicherte Konfigurationswerte:
GEN_DOKUMENTE_DEFAULT (Standard: Anschreiben + Lebenslauf) und
GEN_ANLAGEN_DEFAULT (Standard: keine). Sie belegen die Häkchen auf der
Bewerbungsseite vor; eine bereits getroffene Dokumentenauswahl der
Bewerbung gewinnt weiterhin. Die REST-API nutzt die Vorauswahl, wenn
dokumente/anlagen im Body fehlen.
Die Einstellungsseite kennt dafür jetzt Checkbox-Gruppen, deren Optionen
auch erst zur Renderzeit feststehen dürfen (die eigenen Anlagen).
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>
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>
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>
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>
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>
"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>
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>
- 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>
- 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>
- 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>