Files
jobbi-bewerbung/agent/skills/bewerbungs-tracker/SKILL.md
T
thomasandClaude ce844412e5 Jobsuche: Agent pro Benutzer in isoliertem Docker-Container
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>
2026-07-14 15:41:22 +02:00

18 KiB
Raw Blame History


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: env BEWERBUNG_API_URL)
  • Auth: Header X-API-Key: <token> bei allen Endpunkten außer /health.
  • Key-Quelle: env BEWERBUNG_API_KEY, sonst der Token des Benutzers BEWERBUNG_USER (Default admin) 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-Key identifiziert 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 fremde id (die API antwortet dann 404, 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_TOKEN aus /opt/jobbi-bewerbung/.env wird 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 direkt BEWERBUNG_API_KEY). Der Jobsuche-Runner setzt BEWERBUNG_API_KEY automatisch 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(0112), year(YYYY), status, art, label (Regional|Remote-Deutschlandweit|Homeoffice-Deutschlandweit), search (Firma/Stelle), limit(1500, 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_id pro 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 GmbHacme-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 anlagen oder 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 — auf JobOffer und Application; als ?label=…-Filter auf GET /applications nutzbar.
  • blacklist.typ (anlegen): url, domain, firma, firma_stelle (Antworten können zusätzlich auto enthalten)
  • 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[]); mit force:true erzwingen.
  • 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 datei speichern, nicht ins Terminal.
  • Vor Löschungen (DELETE) beim User rückversichern.

Vollständige OpenAPI-Definition: http://localhost:4327/swagger.json.