Commit Graph
17 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 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 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 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