Scanner: ohne Altersgrenze suchen, Filter Anzeigenalter, Alarm-Schlagwörter
- Suche ohne 21-Tage-Grenze (BA ohne veroeffentlichtseit, Adzuna ohne max_days_old); Einstellung Höchstalter entfällt - Dashboard: Filter Anzeigenalter (bis/älter als 21 Tage), Sortierung älteste Anzeige, Filter nur Alarm, Zeitraum heißt jetzt Gefunden - Suchagent: Schlagwörter mit Alarm; Treffer mit alarm = 1 melden sich über den AlarmWatcher (App-weit, jede Minute) mit Ton und Pop-up, Quittieren gilt für alle Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b34a1968df
commit
1ad774fdd2
@@ -212,7 +212,8 @@ Kontakt (*Kontakt → Stundensatz vor Ort / remote*).
|
||||
## Scanner
|
||||
|
||||
Sucht in Jobbörsen (Agentur für Arbeit, StepStone, karriere.at, Adzuna,
|
||||
freelancermap) nach offenen Stellen in DACH, höchstens 21 Tage alt. Technik
|
||||
freelancermap) nach offenen Stellen in DACH – jedes Alter; im Dashboard filtert
|
||||
*Anzeigenalter* nach „bis 21 Tage“ oder „älter als 21 Tage“. Technik
|
||||
siehe [12 Scanner](12-scanner.md).
|
||||
|
||||
1. **Scanner → Suchagenten → + Neuer Suchagent.** Name, Schlagwörter (Enter
|
||||
@@ -228,7 +229,11 @@ siehe [12 Scanner](12-scanner.md).
|
||||
zur Börse. Status setzen: *In Bearbeitung*, wenn sich jemand kümmert,
|
||||
*Verworfen*, wenn es nicht passt.
|
||||
|
||||
Zeitplan (Wochentage, Uhrzeit, Intervall, Höchstalter) und den kostenlosen
|
||||
**Schlagwörter mit Alarm** im Suchagenten: wird eines gefunden, gibt es einen
|
||||
kurzen Ton und ein Pop-up – egal, wo du im CRM gerade bist. *Gesehen*
|
||||
quittiert für alle.
|
||||
|
||||
Zeitplan (Wochentage, Uhrzeit, Intervall) und den kostenlosen
|
||||
Schlüssel für Adzuna pflegt ein Administrator unter
|
||||
*Einstellungen → Scanner*.
|
||||
|
||||
|
||||
+44
-10
@@ -1,8 +1,12 @@
|
||||
# 12 Scanner
|
||||
|
||||
Der Scanner durchsucht Jobbörsen nach offenen Stellen, die ICON besetzen kann –
|
||||
Festanstellungen ebenso wie Projekte. Region immer DACH, Anzeigen höchstens
|
||||
21 Tage alt (einstellbar). Eingeführt am 29.09.2026.
|
||||
Festanstellungen ebenso wie Projekte. Region immer DACH. Eingeführt am 29.09.2026.
|
||||
|
||||
Gesucht wird **ohne Altersgrenze** (Änderung vom 29.09.2026, vorher höchstens
|
||||
21 Tage): auch ältere Anzeigen kommen herein. Im Dashboard filtert
|
||||
**Anzeigenalter** nach „bis 21 Tage" bzw. „älter als 21 Tage", die Sortierung
|
||||
kennt „älteste Anzeige".
|
||||
|
||||
Menü: **Scanner** (Dashboard) mit dem Unterpunkt **Suchagenten**; Zeitplan und
|
||||
API-Schlüssel unter **Einstellungen → Scanner** (nur Administratoren).
|
||||
@@ -16,10 +20,10 @@ einfacher HTTP-Abruf (`server/src/scanner/sources.js`).
|
||||
|
||||
| Id | Börse | Land | Weg | Schlüssel |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `ba` | Agentur für Arbeit | DE | offizielle Jobsuche-API v6, JSON, Filter `veroeffentlichtseit` | keiner (öffentlicher Header `X-API-Key: jobboerse-jobsuche`) |
|
||||
| `ba` | Agentur für Arbeit | DE | offizielle Jobsuche-API v6, JSON, 100 Anzeigen je Begriff | keiner (öffentlicher Header `X-API-Key: jobboerse-jobsuche`) |
|
||||
| `stepstone` | StepStone.de | DE | HTML der Trefferliste, `data-at`-Attribute | keiner |
|
||||
| `karriere` | karriere.at | AT | HTML der Trefferliste, `.m-jobsListItem` | keiner |
|
||||
| `adzuna` | Adzuna | DE · AT · CH | REST-API, `max_days_old` | kostenlos, App ID + Key |
|
||||
| `adzuna` | Adzuna | DE · AT · CH | REST-API, neueste zuerst | kostenlos, App ID + Key |
|
||||
| `freelancermap` | freelancermap | DE | HTML, Selektoren wie im perinco-Scanner | keiner |
|
||||
|
||||
Adzuna ist die einzige Quelle für die Schweiz (jobs.ch blockt mit CAPTCHA).
|
||||
@@ -59,7 +63,6 @@ sequenceDiagram
|
||||
loop je Börse × Schlagwort (1,5 s Pause)
|
||||
P->>B: Suche
|
||||
B-->>P: Anzeigen
|
||||
P->>P: älter als maxAge? verwerfen
|
||||
end
|
||||
loop je Anzeige
|
||||
P->>DB: speichern (eindeutig je Börse + Kennung), Firma abgleichen
|
||||
@@ -101,6 +104,34 @@ mit „SAP FI" im Text → (2 + 1 + 0) / 6 = **50 %**.
|
||||
Das kostet nichts und ist nachvollziehbar; OpenAI wird vom Scanner nicht
|
||||
aufgerufen.
|
||||
|
||||
## Schlagwörter mit Alarm
|
||||
|
||||
Neben den Schlagwörtern trägt ein Agent **Schlagwörter mit Alarm**
|
||||
(`alarm_keywords`). Sie werden genauso gesucht und für die Quote gezählt; steht
|
||||
eines in einer **neu gefundenen** Anzeige, bekommt der Treffer `alarm = 1`.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant P as pipeline.js
|
||||
participant DB as icon_scanner_matches
|
||||
participant W as AlarmWatcher (jede Ansicht, jede Minute)
|
||||
P->>DB: Treffer mit alarm = 1, alarm_ack = 0
|
||||
W->>DB: GET /api/scanner/alarms
|
||||
DB-->>W: unquittierte Alarme
|
||||
W->>W: neuer Alarm? Zweiklang (WebAudio) + Pop-up
|
||||
W->>DB: Gesehen / Zum Scanner → POST /alarms/ack
|
||||
```
|
||||
|
||||
- Der Wächter hängt in `App.tsx`, nicht im Dashboard – der Alarm kommt also in
|
||||
jeder Ansicht (beim perinco-Scanner hörte man den Ton nur im Dashboard).
|
||||
- **Quittieren gilt für alle Benutzer.** Sonst klingelte derselbe Fund bei jedem
|
||||
einzeln.
|
||||
- Der Ton braucht einen vorherigen Klick auf der Seite (Autoplay-Regel der
|
||||
Browser). Ohne bleibt es still, das Pop-up kommt trotzdem.
|
||||
- Schon gespeicherte Treffer werden nicht nachträglich zu Alarmen, wenn man ein
|
||||
Alarm-Wort später ergänzt (Treffer sind eindeutig je Agent und Anzeige).
|
||||
- Im Dashboard tragen Alarm-Treffer eine rote Glocke; Filter **nur Alarm**.
|
||||
|
||||
## Bekannte Unternehmen
|
||||
|
||||
Jede Anzeige wird mit `icon_companies` abgeglichen. Beide Namen werden
|
||||
@@ -118,8 +149,7 @@ angelegtes Unternehmen wird so auch bei den Anzeigen von gestern erkannt.
|
||||
|
||||
## Zeitplan
|
||||
|
||||
Einstellungen → Scanner. Vorgabe: **stündlich, Mo–Fr, 06:00–20:00 Uhr**,
|
||||
Anzeigen höchstens **21 Tage** alt.
|
||||
Einstellungen → Scanner. Vorgabe: **stündlich, Mo–Fr, 06:00–20:00 Uhr**.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
@@ -151,7 +181,6 @@ Liste neu, sobald er fertig ist.
|
||||
| `scanner_days` | `1,2,3,4,5` | ISO-Wochentage, 1 = Montag |
|
||||
| `scanner_from` / `scanner_to` | `06:00` / `20:00` | Fenster, deutsche Zeit |
|
||||
| `scanner_interval` | `60` | Minuten, 15 bis 1440 |
|
||||
| `scanner_max_age` | `21` | Tage, 1 bis 90 |
|
||||
| `scanner_adzuna_id`, `scanner_adzuna_key` | leer | gehen nie im Klartext zum Browser |
|
||||
| `scanner_last_scan_at` | – | Beginn des letzten Laufs (ISO) |
|
||||
|
||||
@@ -168,6 +197,7 @@ erDiagram
|
||||
text keywords "JSON-Array"
|
||||
text sources "JSON-Array der Quellen-Ids"
|
||||
text excludes "JSON-Array"
|
||||
text alarm_keywords "JSON-Array, Ton + Pop-up"
|
||||
tinyint is_active
|
||||
}
|
||||
icon_scanner_jobs {
|
||||
@@ -189,6 +219,8 @@ erDiagram
|
||||
tinyint score "0-100"
|
||||
text hits "gefundene Schlagwörter"
|
||||
enum status "neu gesehen in_bearbeitung verworfen"
|
||||
tinyint alarm "Alarm-Schlagwort getroffen"
|
||||
tinyint alarm_ack "Pop-up quittiert (für alle)"
|
||||
}
|
||||
icon_scanner_runs {
|
||||
int id
|
||||
@@ -220,10 +252,12 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`.
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/overview` | alle | Kennzahlen, Quellen samt Ergebnis des letzten Laufs, Zeitplan, läuft gerade ein Lauf |
|
||||
| GET | `/agents` | alle | Suchagenten mit Trefferzahl |
|
||||
| POST | `/agents` | alle | anlegen `{ name, keywords[], sources[], excludes[], is_active }` |
|
||||
| POST | `/agents` | alle | anlegen `{ name, keywords[], alarm_keywords[], sources[], excludes[], is_active }` – Schlagwort oder Alarm-Schlagwort Pflicht |
|
||||
| PUT | `/agents/:id` | alle | ändern (gleicher Körper) |
|
||||
| DELETE | `/agents/:id` | alle | löschen, Treffer gehen mit |
|
||||
| GET | `/matches` | alle | Filter `hours`, `agentId`, `source`, `known=1`, `status` (`alle` zeigt auch Verworfene), `minScore`, `q`, `sort` (`known`/`score`/`published`), `limit` |
|
||||
| GET | `/matches` | alle | Filter `hours` (gefunden), `age` (`recent`/`old`, Grenze 21 Tage ab Veröffentlichung), `agentId`, `source`, `known=1`, `alarm=1`, `status` (`alle` zeigt auch Verworfene), `minScore`, `q`, `sort` (`known`/`score`/`published`/`oldest`), `limit` |
|
||||
| GET | `/alarms` | alle | unquittierte Alarm-Treffer (höchstens 50) |
|
||||
| POST | `/alarms/ack` | alle | `{ ids: [] }` – quittiert für alle Benutzer |
|
||||
| PATCH | `/matches/:id` | alle | `{ status }` |
|
||||
| POST | `/scan` | alle | Lauf starten, `202`; `409`, wenn schon einer läuft |
|
||||
| GET | `/settings` | Admin | Zeitplan, Schlüssel verkürzt |
|
||||
|
||||
Reference in New Issue
Block a user