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>
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
---
|
||||
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:
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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_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 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
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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.
|
||||
|
||||
```bash
|
||||
# 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`.
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# Helper for the Bewerbungs-Tracker REST-API.
|
||||
# Usage:
|
||||
# bt.sh GET /applications
|
||||
# bt.sh GET "/applications?status=Gesendet&limit=20"
|
||||
# bt.sh POST /applications '{"datum":"2026-07-03","firma":"ACME","stelle":"DevOps"}'
|
||||
# bt.sh PUT /applications/12 @/path/to/body.json
|
||||
# bt.sh DELETE /joboffers/5
|
||||
#
|
||||
# Config via env (with sensible defaults):
|
||||
# BEWERBUNG_API_URL base URL (default http://localhost:4327/api/v1)
|
||||
# BEWERBUNG_API_KEY API key (X-API-Key header)
|
||||
# BEWERBUNG_USER username whose key to use (default: admin)
|
||||
#
|
||||
# MULTI-USER: the tracker has one API key PER USER; the key decides whose data you
|
||||
# see and change. There is no global key any more. The old fallback (API_TOKEN from
|
||||
# /opt/jobbi-bewerbung/.env) is gone on purpose: it happened to hold the admin's
|
||||
# token, so it silently acted as admin — and it goes stale the moment a key is
|
||||
# rotated in the UI (Einstellungen -> "Neu generieren").
|
||||
#
|
||||
# Key resolution:
|
||||
# 1. $BEWERBUNG_API_KEY if set (this is what the Jobsuche-Runner passes per user)
|
||||
# 2. otherwise: look up the key of $BEWERBUNG_USER (default admin) in the tracker DB
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE="${BEWERBUNG_API_URL:-http://localhost:4327/api/v1}"
|
||||
DB="${BEWERBUNG_DB:-/opt/jobbi-bewerbung/data/bewerbungen.db}"
|
||||
USER_NAME="${BEWERBUNG_USER:-admin}"
|
||||
KEY="${BEWERBUNG_API_KEY:-}"
|
||||
|
||||
if [[ -z "$KEY" && -f "$DB" ]]; then
|
||||
KEY="$(node -e "
|
||||
const s = require('/opt/jobbi-bewerbung/source/node_modules/sqlite3');
|
||||
const db = new s.Database(process.argv[1]);
|
||||
db.get(
|
||||
\"SELECT a.value FROM app_state a JOIN users u ON u.id = a.user_id \" +
|
||||
\"WHERE u.username = ? AND a.key = 'cfg:API_TOKEN' AND a.value != ''\",
|
||||
[process.argv[2]],
|
||||
(e, r) => process.stdout.write(r && r.value ? r.value : '')
|
||||
);
|
||||
" "$DB" "$USER_NAME" 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [[ -z "$KEY" ]]; then
|
||||
echo "bt.sh: kein API-Key für Benutzer '$USER_NAME'." >&2
|
||||
echo " -> In der App unter Einstellungen -> REST-API einen Token erzeugen," >&2
|
||||
echo " oder BEWERBUNG_API_KEY / BEWERBUNG_USER setzen." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
METHOD="${1:?method required (GET/POST/PUT/DELETE)}"
|
||||
PATH_ARG="${2:?path required, e.g. /applications}"
|
||||
BODY="${3:-}"
|
||||
|
||||
url="${BASE}${PATH_ARG}"
|
||||
args=(-sS -X "$METHOD" -H "X-API-Key: ${KEY}" -w $'\n<http %{http_code}>\n')
|
||||
|
||||
if [[ -n "$BODY" ]]; then
|
||||
args+=(-H "Content-Type: application/json" --data-binary "$BODY")
|
||||
fi
|
||||
|
||||
curl "${args[@]}" "$url"
|
||||
Reference in New Issue
Block a user