Jeder Suchlauf läuft nun in einem frischen Container pro Benutzer, dessen pro-Benutzer-Home als ~/.claude gemountet ist — Memories und Session-Contexte liegen damit strikt getrennt pro Benutzer. Die Such-Skills sind Shared-Code aus dem Image und werden im Container nur nach ~/.claude/skills verlinkt. Der Host-Runner startet pro Lauf `docker run --rm` und führt bis zu JOBSUCHE_MAX_PARALLEL (Default 4) Läufe parallel über verschiedene Benutzer aus (jeder hat eigenen Ollama-Key = getrennte Rate-Limits). Der alte gemeinsame ~/.claude-Pfad wird vom Runner nicht mehr beschrieben. - source/agent/: neues Agent-Image (Dockerfile + entrypoint + drei Skills) - scripts/jobsuche-runner.js: agentStarten als docker run, ensureAgentDir, runPool - scripts/jobsuche-runner.sh + bin/-Kopie: Image-Guard - package.json: docker:build-agent (lokal, ohne Registry-Push) Co-Authored-By: Claude <noreply@anthropic.com>
18 KiB
name: bewerbungs-tracker description: Read and manage job applications ("Bewerbungen") via the local Bewerbungs-Tracker REST-API. Use whenever the user wants to list, create, update or delete Bewerbungen, manage their Statusverlauf/Timeline, download generated Unterlagen (PDF attachments), read E-Mail correspondence, trigger or check KI document generation, read/write Settings, get Statistics or an Export, browse Templates (Vorlagen), trigger document generation with optional static attachments (Anlagen), or manage JobOffers (Jobangebote). Trigger phrases: "Bewerbung(en)", "Bewerbungs-Tracker", "Statusverlauf", "Jobangebot(e)", "Bewerbungsstatistik", "Anschreiben generieren", "Anlagen beilegen".
Bewerbungs-Tracker REST-API
REST-API zum Lesen und Verwalten von Bewerbungen, Statusverlauf, generierten Unterlagen (PDFs), E-Mail-Korrespondenz, Einstellungen, Statistiken, Vorlagen und Jobangeboten.
Konfiguration
- Base-URL:
http://localhost:4327/api/v1(override: envBEWERBUNG_API_URL) - Auth: Header
X-API-Key: <token>bei allen Endpunkten außer/health. - Key-Quelle: env
BEWERBUNG_API_KEY, sonst der Token des BenutzersBEWERBUNG_USER(Defaultadmin) aus der Tracker-DB.
Multi-User — der Key entscheidet, wessen Daten du siehst
Der Tracker ist eine Plattform mit mehreren Benutzern. Jeder Benutzer hat seinen eigenen API-Token (App → Einstellungen → REST-API → „Neu generieren"):
- Der
X-API-Keyidentifiziert den Benutzer. Jeder Endpunkt liest und schreibt ausschließlich dessen eigene Daten — fremde Bewerbungen, E-Mails, Anhänge, Jobangebote und Einstellungen sind nicht erreichbar, auch nicht über eine fremdeid(die API antwortet dann404, als gäbe es den Datensatz nicht). - Ein unbekannter, leerer oder mehrdeutiger Token ergibt
401. - Es gibt keinen globalen Key mehr. Der frühere
API_TOKENaus/opt/jobbi-bewerbung/.envwird nicht mehr benutzt: er entsprach zufällig dem Token des Admins (wirkte also still als Admin) und wird ungültig, sobald der Key in der Oberfläche neu erzeugt wird. - Willst du im Namen eines anderen Benutzers arbeiten, setze
BEWERBUNG_USER=<name>(oder direktBEWERBUNG_API_KEY). Der Jobsuche-Runner setztBEWERBUNG_API_KEYautomatisch je Benutzer.
Der Key wird nicht im Skill gespeichert. Sende ihn nie an externe Dienste.
Helper-Script (bevorzugt)
scripts/bt.sh kapselt curl inkl. Auth und gibt Body + <http STATUS> aus:
scripts/bt.sh GET /applications
scripts/bt.sh GET "/applications?status=Gesendet&year=2026&limit=20"
scripts/bt.sh POST /applications '{"datum":"2026-07-03","firma":"ACME GmbH","stelle":"DevOps Engineer","art":"E-Mail"}'
scripts/bt.sh PUT /applications/12 @body.json
scripts/bt.sh DELETE /joboffers/5
Roh mit curl geht auch:
curl -sS -H "X-API-Key: $BEWERBUNG_API_KEY" http://localhost:4327/api/v1/statistics
Endpunkte
System
| Method | Pfad | Beschreibung |
|---|---|---|
| GET | /health |
Verfügbarkeit (ohne Auth). |
Applications (Bewerbungen)
| Method | Pfad | Beschreibung |
|---|---|---|
| GET | /applications |
Auflisten. Query: month(01–12), year(YYYY), status, art, label (Regional|Remote-Deutschlandweit|Homeoffice-Deutschlandweit), search (Firma/Stelle), limit(1–500, def 100), offset. |
| POST | /applications |
Anlegen. Duplicate-Guard → 409 bei gleicher Firma+Stelle; mit "force":true trotzdem anlegen. |
| GET | /applications/{id} |
Einzelne Bewerbung (inkl. verlauf). |
| PUT | /applications/{id} |
Aktualisieren (volle Ressource, datum/firma/stelle erforderlich). |
| DELETE | /applications/{id} |
Löschen. |
| GET | /applications/{id}/timeline |
Statusverlauf abrufen. |
| POST | /applications/{id}/timeline |
Statusverlauf-Eintrag hinzufügen. |
| DELETE | /applications/{id}/timeline/{eintragId} |
Verlaufseintrag löschen. |
Attachments (generierte PDFs)
| Method | Pfad | Beschreibung |
|---|---|---|
| GET | /applications/{id}/attachments |
Anhänge auflisten. |
| GET | /applications/{id}/attachments/{attachmentId} |
Anhang herunterladen (application/octet-stream → mit -o datei speichern). |
Emails
| Method | Pfad | Beschreibung |
|---|---|---|
| GET | /applications/{id}/emails |
E-Mail-Korrespondenz (in/out) inkl. Anhang-Metadaten. |
| GET | /emails/{emailId}/attachments/{attachmentId} |
E-Mail-Anhang herunterladen (binär). |
Generation (KI-Unterlagen)
| Method | Pfad | Beschreibung |
|---|---|---|
| POST | /applications/{id}/generate |
Generierung anstoßen. Body optional: {"llm_notizen":"...","anlagen":[2]}. anlagen = IDs statischer Basis-Anlagen (siehe „Anlagen bei der Generierung"). |
| GET | /applications/{id}/generation-status |
Status: nicht_gestartet|ausstehend|fertig|fehler (+ anhaenge). |
Settings / Statistics / Export / Templates
| Method | Pfad | Beschreibung |
|---|---|---|
| GET | /settings |
Persönliche Angaben des eigenen Benutzers: name, adresse, kundennummer, email, telefon, ort, webseite, geburtsdatum. Noch nichts gespeichert → {} (gültiger Zustand). |
| PUT | /settings |
Persönliche Angaben speichern. Es werden nur die Felder geändert, die im Body stehen — weggelassene behalten ihren Wert. Antwort enthält den neuen Stand ({success, settings}). |
| GET | /statistics |
total, byArt[], byStatus[] — nur eigene Bewerbungen. |
| GET | /export |
Export (ohne interne_notizen). Query: month, year. |
| GET | /templates |
Basis-Unterlagen / Vorlagen des Benutzers. Enthält den Lebenslauf — daraus leitet die Stellensuche Rolle und Skills ab. |
JobOffers (Jobangebote)
| Method | Pfad | Beschreibung |
|---|---|---|
| GET | /joboffers |
Auflisten (neueste zuerst). |
| POST | /joboffers |
Upsert (per (quelle, external_id), sonst normalisierte quelle_url) → Response action: created|updated. 409 wenn geblacklistet (siehe unten). |
| GET | /joboffers/{id} |
Einzelnes Jobangebot. |
| DELETE | /joboffers/{id} |
Löschen. Setzt das Angebot standardmäßig zuvor auf die Blacklist (Typ auto), damit dieselbe Stelle nie erneut eingespielt wird. Mit ?blacklist=false hart löschen ohne Blockierung. Response {success, blacklisted}. |
Blacklist (blockierte Jobangebote)
| Method | Pfad | Beschreibung |
|---|---|---|
| GET | /joboffers/blacklist |
Blacklist-Einträge auflisten (neueste zuerst). |
| POST | /joboffers/blacklist |
Block anlegen (per URL, Domain, Firma oder Firma+Stelle) → 201 {success, entry}. |
| DELETE | /joboffers/blacklist/{id} |
Block entfernen — betroffene Angebote sind danach wieder einspielbar. |
JobOffers einspielen (Drittanbieter → Upsert)
POST /joboffers ist der Einspiel-Endpunkt für Drittanbietersoftware (CRM,
Scraper, Job-Board-Sync). Er upsertet anhand (quelle, external_id), und
falls keine external_id passt, anhand der normalisierten quelle_url:
- Treffer vorhanden → Datensatz wird aktualisiert → Response
"action":"updated", HTTP 200. - kein Treffer → neuer Datensatz →
"action":"created", HTTP 201. - Angebot steht auf der Blacklist → HTTP 409,
BlacklistConflict-Body, Angebot wird nicht aufgenommen (siehe Abschnitt „Blacklist“).
Damit ist der Import idempotent: dieselbe Quelle kann beliebig oft eingespielt werden, ohne Dubletten zu erzeugen — Voraussetzung ist eine stabile
external_idpro Quelle.
⚠️ Upsert ersetzt, er merged nicht. Beim Update wird der Datensatz mit dem gesendeten Body überschrieben; weggelassene optionale Felder werden geleert. Beim Re-Import daher immer den vollständigen Datensatz senden, nicht nur die geänderten Felder.
Felder
| Feld | Pflicht | Beschreibung |
|---|---|---|
firma |
✅ | Arbeitgeber. |
stelle |
✅ | Stellenbezeichnung. |
firma_slug |
✅ | Pflicht. Firmenname als lowercase + kebab-case (z. B. Acme GmbH → acme-gmbh). Dient der eindeutigen Identifizierung beim Blacklist-Abgleich. |
external_id |
– | ID des Angebots in der Quellsoftware. Upsert-Schlüssel — für idempotenten Import immer setzen. |
quelle |
– | Herkunftssystem/Kanal (z. B. mein-crm). Default serverseitig drittanbieter. |
ort |
– | Arbeitsort. |
adresse |
– | Anschrift des Arbeitgebers (Straße, Hausnummer, PLZ, Ort). |
ansprechpartner |
– | Name des Ansprechpartners für die Bewerbung. |
gehalt |
– | Freitext (z. B. 55.000 €). |
beschreibung |
– | Volltext der Stellenanzeige. |
quelle_url |
– | Deeplink zum Original-Inserat. |
art |
– | Bewerbungsart-Enum (siehe unten). |
status |
– | offen | uebernommen | abgelehnt. Default offen. |
anzeige_datum |
– | Veröffentlichungsdatum des Inserats (YYYY-MM-DD). Wird gespeichert. |
kontakt_email |
– | Kontakt-/Bewerbungs-E-Mail-Adresse der Stelle. |
labels |
– | Array von Arbeitsort-Labels (Enum, mehrere möglich): Regional, Remote-Deutschlandweit, Homeoffice-Deutschlandweit. Kennzeichnet den Arbeitsort-Typ der Stelle. |
created_at |
⛔ | Nicht mitsenden — Einspiel-Zeitpunkt wird serverseitig gesetzt; ein mitgesendeter Wert wird ignoriert. |
Beispiel
scripts/bt.sh POST /joboffers '{
"external_id": "job-12345",
"quelle": "mein-crm",
"firma": "Acme GmbH",
"stelle": "Softwareentwickler (m/w/d)",
"firma_slug": "acme-gmbh",
"ort": "Berlin",
"adresse": "Musterstraße 1, 10115 Berlin",
"gehalt": "55.000 €",
"beschreibung": "…",
"quelle_url": "https://acme.example/job/12345",
"art": "Online-Portal",
"anzeige_datum": "2026-07-01",
"kontakt_email": "bewerbung@acme.example",
"labels": ["Regional"]
}'
Roh mit curl:
curl -X POST "$BEWERBUNG_API_URL/joboffers" \
-H "X-API-Key: $BEWERBUNG_API_KEY" -H "Content-Type: application/json" \
-d '{"external_id":"job-12345","quelle":"mein-crm","firma":"Acme GmbH","stelle":"Softwareentwickler (m/w/d)","firma_slug":"acme-gmbh","art":"Online-Portal","anzeige_datum":"2026-07-01"}'
JSON erlaubt keine Kommentare — die
// created_at …-Zeile aus Notizen vor dem Absenden entfernen, sonst schlägt das Parsen fehl (HTTP 400).
Batch-Einspielung
Kein Bulk-Endpunkt: pro Angebot ein POST. Für viele Angebote in einer Schleife
importieren und action je Antwort auswerten:
# offers.json = JSON-Array von JobOfferCreate-Objekten
jq -c '.[]' offers.json | while read -r offer; do
scripts/bt.sh POST /joboffers "$offer" | head -1
done
Blacklist (Angebote dauerhaft blockieren)
Über /joboffers/blacklist lassen sich Angebote sperren. Ein geblacklistetes
Angebot wird beim POST /joboffers nicht aufgenommen, sondern mit 409
und BlacklistConflict-Body abgelehnt (blacklisted:true, matched_by,
blacklist_entry). Der Filter greift auf künftige Einspielungen; bereits
importierte Angebote werden nicht rückwirkend gelöscht.
Block-Typen (typ)
typ |
Body-Felder | Blockiert |
|---|---|---|
url |
wert = konkrete URL |
genau dieses Inserat (URL wird normalisiert, Tracking-Parameter entfernt). |
domain |
wert = Domain (z. B. stellenbetrug.example) |
alle Angebote dieser Domain. |
firma |
wert = Firmenname |
alle Angebote dieser Firma. |
firma_stelle |
firma + stelle (+ optional ort) |
eine bestimmte Firma-Stelle-Kombination. |
grund (Freitext) ist überall optional. Firma/Stelle/URL werden serverseitig
normalisiert, der Vergleich ist also unabhängig von Groß-/Kleinschreibung und
Tracking-Parametern. GET-Antworten enthalten die normalisierten Vergleichsfelder
zusätzlich: url_norm, domain, firma_norm, firma_slug, stelle_norm,
ort_norm (nur lesbar, serverseitig berechnet — nicht mitsenden). (Der Typ auto
in Antworten markiert automatisch erzeugte Einträge — aus einer Ablehnung oder
aus dem Löschen eines Jobangebots (DELETE /joboffers/{id}); er kann nicht
direkt angelegt werden.)
Beispiele
# Ganze Domain sperren
scripts/bt.sh POST /joboffers/blacklist '{"typ":"domain","wert":"stellenbetrug.example","grund":"Fake-Inserate"}'
# Eine Firma sperren
scripts/bt.sh POST /joboffers/blacklist '{"typ":"firma","wert":"Dubios GmbH"}'
# Konkrete Firma+Stelle sperren
scripts/bt.sh POST /joboffers/blacklist '{"typ":"firma_stelle","firma":"Acme GmbH","stelle":"Vertrieb (m/w/d)","ort":"Berlin"}'
# Auflisten und wieder entfernen
scripts/bt.sh GET /joboffers/blacklist
scripts/bt.sh DELETE /joboffers/blacklist/3
⚠️ Vor dem
POST /joboffers(v. a. Batch-Import) kann ein 409 auftreten, weil das Angebot geblacklistet ist. In Schleifen den Statuscode auswerten und solche Angebote überspringen statt abzubrechen.
Anlagen bei der Generierung (statische Basis-Anlagen)
Beim Anstoßen der KI-Generierung (POST /applications/{id}/generate) lassen sich
optional statische Basis-Anlagen (basis_anhaenge) beilegen — z. B. Zeugnisse,
Zertifikate. Feld anlagen: ein Array von Integer-IDs dieser Basis-Anlagen.
- Default: keine. Fehlt
anlagenoder ist das Array leer, werden keine statischen Anlagen beigelegt. - Die gewählten Anlagen erscheinen zusätzlich im Anschreiben unter „Anlagen".
- IDs verweisen auf die im Tracker hinterlegten Basis-Anlagen (
basis_anhaenge). Für die aktuell verfügbaren IDs/Namen im Tracker nachsehen (die Basis-Anlagen werden dort verwaltet); nur belegte IDs senden, keine raten.
# Mit einer statischen Anlage (z. B. Abschlusszeugnis = ID 2) generieren
scripts/bt.sh POST /applications/12/generate '{"anlagen":[2]}'
# Mehrere Anlagen + KI-Kontext
scripts/bt.sh POST /applications/12/generate '{"llm_notizen":"…","anlagen":[2,5]}'
# Ohne Anlagen (Default) — Feld einfach weglassen
scripts/bt.sh POST /applications/12/generate '{}'
Enums
- art:
E-Mail,Online-Portal,Indeed,StepStone,Firmenwebsite,Post,Initiativbewerbung,Arbeitsagentur,Sonstiges - status (Bewerbung/Timeline):
Entwurf,Gesendet,Eingangsbestätigung,In Bearbeitung,Interessiert,Warten auf Rückmeldung,Warten auf meine Antwort,Vorstellungsgespräch,Absage,Einstellung,Keine Rückmeldung - joboffer.status:
offen,uebernommen,abgelehnt - labels (Arbeitsort, Array, mehrere möglich):
Regional,Remote-Deutschlandweit,Homeoffice-Deutschlandweit— aufJobOfferundApplication; als?label=…-Filter aufGET /applicationsnutzbar. - blacklist.typ (anlegen):
url,domain,firma,firma_stelle(Antworten können zusätzlichautoenthalten) - email.direction:
in,out - generation.status:
nicht_gestartet,ausstehend,fertig,fehler
Feld-Referenz (wichtigste)
ApplicationCreate — erforderlich datum (YYYY-MM-DD), firma, stelle;
optional art, status, notizen, interne_notizen, ort,
stellenbeschreibung, quelle_url, llm_notizen, kommentar, force,
labels (Array: Regional|Remote-Deutschlandweit|Homeoffice-Deutschlandweit;
auch als ?label=-Filter beim Auflisten). ApplicationUpdate kennt labels ebenso.
TimelineEntryCreate — erforderlich status; optional datum, kommentar.
GenerateRequest (POST /applications/{id}/generate) — alle Felder optional:
llm_notizen (Freitext-Kontext für die KI), anlagen (Array von Integer-IDs
statischer Basis-Anlagen; Default keine, siehe Abschnitt „Anlagen bei der
Generierung“).
JobOfferCreate — erforderlich firma, stelle, firma_slug (Firmenname als
lowercase + kebab-case, z. B. acme-gmbh — eindeutige ID für den
Blacklist-Abgleich); optional external_id, quelle, ort, adresse,
ansprechpartner, gehalt, beschreibung, quelle_url, art, status,
anzeige_datum, kontakt_email, labels (Array:
Regional|Remote-Deutschlandweit|Homeoffice-Deutschlandweit, mehrere
möglich) (siehe Abschnitt „JobOffers einspielen“).
created_at wird serverseitig gesetzt und darf nicht mitgesendet werden.
BlacklistEntryCreate — erforderlich typ (url|domain|firma|
firma_stelle); bei url/domain/firma das Feld wert; bei firma_stelle
die Felder firma+stelle (optional ort); grund überall optional (siehe
Abschnitt „Blacklist“).
Hinweise / Fehlerbehandlung
- 401
{"error":"Ungültiger oder fehlender API-Key ..."}→ Key falsch, leer oder keinem Benutzer zugeordnet. Der Key liegt in der Datenbank (pro Benutzer), nicht mehr in der.env— ein Serverneustart ist nach einer Key-Änderung nicht nötig, ein neuer Key gilt sofort und macht den alten ungültig. Häufigste Ursache: Der Token wurde in der Oberfläche neu generiert, die Umgebung nutzt noch den alten. - 404 obwohl die ID existiert → Die ID gehört einem anderen Benutzer. Die API
zeigt fremde Datensätze grundsätzlich als „nicht vorhanden". Prüfe, mit wessen Key du
arbeitest (
BEWERBUNG_USER/BEWERBUNG_API_KEY). - 409 bei
POST /applications→ mögliche Dublette (matches[]); mitforce:trueerzwingen. - 409 bei
POST /joboffers→ Angebot steht auf der Blacklist (BlacklistConflict:blacklisted,matched_by,blacklist_entry); nicht erzwingbar — erst den Blacklist-Eintrag entfernen (DELETE /joboffers/blacklist/{id}). - 404 → ID existiert nicht.
- Binär-Downloads (Attachments) mit
curl ... -o dateispeichern, nicht ins Terminal. - Vor Löschungen (DELETE) beim User rückversichern.
Vollständige OpenAPI-Definition: http://localhost:4327/swagger.json.