Scanner: Altersfenster "älter als / jünger als" (Tage)
Ersetzt die feste Auswahl bis/über 21 Tage durch zwei freie Felder, einzeln oder kombiniert (z. B. älter 14, jünger 30). API: olderThan, youngerThan; age=recent|old bleibt für Altaufrufe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
651ad2b954
commit
1e273ddd92
@@ -212,8 +212,8 @@ Kontakt (*Kontakt → Stundensatz vor Ort / remote*).
|
|||||||
## Scanner
|
## Scanner
|
||||||
|
|
||||||
Sucht in Jobbörsen (Agentur für Arbeit, StepStone, karriere.at, Adzuna,
|
Sucht in Jobbörsen (Agentur für Arbeit, StepStone, karriere.at, Adzuna,
|
||||||
freelancermap) nach offenen Stellen in DACH – jedes Alter; im Dashboard filtert
|
freelancermap) nach offenen Stellen in DACH – jedes Alter; im Dashboard grenzen
|
||||||
*Anzeigenalter* nach „bis 21 Tage“ oder „älter als 21 Tage“. Technik
|
*Älter als* und *Jünger als* (Tage) das Alter ein, z. B. älter 14, jünger 30. Technik
|
||||||
siehe [12 Scanner](12-scanner.md).
|
siehe [12 Scanner](12-scanner.md).
|
||||||
|
|
||||||
1. **Scanner → Suchagenten → + Neuer Suchagent.** Name, Schlagwörter (Enter
|
1. **Scanner → Suchagenten → + Neuer Suchagent.** Name, Schlagwörter (Enter
|
||||||
|
|||||||
+5
-4
@@ -4,9 +4,10 @@ Der Scanner durchsucht Jobbörsen nach offenen Stellen, die ICON besetzen kann
|
|||||||
Festanstellungen ebenso wie Projekte. Region immer DACH. 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
|
Gesucht wird **ohne Altersgrenze** (Änderung vom 29.09.2026, vorher höchstens
|
||||||
21 Tage): auch ältere Anzeigen kommen herein. Im Dashboard filtert
|
21 Tage): auch ältere Anzeigen kommen herein. Im Dashboard grenzen die Felder
|
||||||
**Anzeigenalter** nach „bis 21 Tage" bzw. „älter als 21 Tage", die Sortierung
|
**Älter als** und **Jünger als** (Tage) das Alter frei ein – z. B. älter 14,
|
||||||
kennt „älteste Anzeige".
|
jünger 30. Leer heißt keine Grenze; Anzeigen ohne Datum fallen heraus, sobald
|
||||||
|
eines der Felder gesetzt ist. Die Sortierung kennt „älteste Anzeige".
|
||||||
|
|
||||||
Menü: **Scanner** (Dashboard) mit dem Unterpunkt **Suchagenten**; Zeitplan und
|
Menü: **Scanner** (Dashboard) mit dem Unterpunkt **Suchagenten**; Zeitplan und
|
||||||
API-Schlüssel unter **Einstellungen → Scanner** (nur Administratoren).
|
API-Schlüssel unter **Einstellungen → Scanner** (nur Administratoren).
|
||||||
@@ -276,7 +277,7 @@ Alle unter `/api/scanner`, alle hinter `requireAuth`.
|
|||||||
| POST | `/agents` | alle | anlegen `{ name, keywords[], alarm_keywords[], sources[], excludes[], is_active }` – Schlagwort oder Alarm-Schlagwort Pflicht |
|
| 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) |
|
| PUT | `/agents/:id` | alle | ändern (gleicher Körper) |
|
||||||
| DELETE | `/agents/:id` | alle | löschen, Treffer gehen mit |
|
| DELETE | `/agents/:id` | alle | löschen, Treffer gehen mit |
|
||||||
| 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`/`agent`/`title`/`company`/`source`/`published`/`status`/`created`; `oldest` = `published` aufsteigend) mit `dir` (`asc`/`desc`), `limit` |
|
| GET | `/matches` | alle | Filter `hours` (gefunden), `olderThan` / `youngerThan` (Tage ab Veröffentlichung, einzeln oder zusammen; `age=recent|old` = feste 21-Tage-Grenze, aus der ersten Fassung), `agentId`, `source`, `known=1`, `alarm=1`, `status` (`alle` zeigt auch Verworfene), `minScore`, `q`, `sort` (`known`/`score`/`agent`/`title`/`company`/`source`/`published`/`status`/`created`; `oldest` = `published` aufsteigend) mit `dir` (`asc`/`desc`), `limit` |
|
||||||
| GET | `/alarms` | alle | unquittierte Alarm-Treffer (höchstens 50) |
|
| GET | `/alarms` | alle | unquittierte Alarm-Treffer (höchstens 50) |
|
||||||
| POST | `/alarms/ack` | alle | `{ ids: [] }` – quittiert für alle Benutzer |
|
| POST | `/alarms/ack` | alle | `{ ids: [] }` – quittiert für alle Benutzer |
|
||||||
| PATCH | `/matches/:id` | alle | `{ status }` |
|
| PATCH | `/matches/:id` | alle | `{ status }` |
|
||||||
|
|||||||
@@ -251,6 +251,21 @@ router.get('/matches', async (req, res, next) => {
|
|||||||
);
|
);
|
||||||
params.push(AGE_DAYS);
|
params.push(AGE_DAYS);
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
|
* Freies Altersfenster in Tagen: `olderThan` = mindestens so alt,
|
||||||
|
* `youngerThan` = hoechstens so alt (z. B. aelter 14, juenger 30).
|
||||||
|
* Beide optional und mit `age` kombinierbar.
|
||||||
|
*/
|
||||||
|
const olderThan = Number(req.query.olderThan);
|
||||||
|
if (Number.isFinite(olderThan) && olderThan > 0) {
|
||||||
|
where.push('j.published_at < NOW() - INTERVAL ? DAY');
|
||||||
|
params.push(Math.round(olderThan));
|
||||||
|
}
|
||||||
|
const youngerThan = Number(req.query.youngerThan);
|
||||||
|
if (Number.isFinite(youngerThan) && youngerThan > 0) {
|
||||||
|
where.push('j.published_at >= NOW() - INTERVAL ? DAY');
|
||||||
|
params.push(Math.round(youngerThan));
|
||||||
|
}
|
||||||
if (req.query.alarm === '1') where.push('m.alarm = 1');
|
if (req.query.alarm === '1') where.push('m.alarm = 1');
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
const [showDiscarded, setShowDiscarded] = useState(false);
|
const [showDiscarded, setShowDiscarded] = useState(false);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [sort, setSort] = useState<SortState>({ key: 'known', dir: 'desc' });
|
const [sort, setSort] = useState<SortState>({ key: 'known', dir: 'desc' });
|
||||||
/** Alter der Anzeige: alle | bis 21 Tage | älter als 21 Tage */
|
/** Altersfenster der Anzeige in Tagen; leer = keine Grenze. */
|
||||||
const [age, setAge] = useState<'' | 'recent' | 'old'>('');
|
const [olderThan, setOlderThan] = useState('');
|
||||||
|
const [youngerThan, setYoungerThan] = useState('');
|
||||||
const [alarmOnly, setAlarmOnly] = useState(false);
|
const [alarmOnly, setAlarmOnly] = useState(false);
|
||||||
|
|
||||||
const wasRunning = useRef(false);
|
const wasRunning = useRef(false);
|
||||||
@@ -81,7 +82,8 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
known: knownOnly ? 1 : undefined,
|
known: knownOnly ? 1 : undefined,
|
||||||
status: showDiscarded ? 'alle' : undefined,
|
status: showDiscarded ? 'alle' : undefined,
|
||||||
q: search.trim() || undefined,
|
q: search.trim() || undefined,
|
||||||
age: age || undefined,
|
olderThan: Number(olderThan) > 0 ? Number(olderThan) : undefined,
|
||||||
|
youngerThan: Number(youngerThan) > 0 ? Number(youngerThan) : undefined,
|
||||||
alarm: alarmOnly ? 1 : undefined,
|
alarm: alarmOnly ? 1 : undefined,
|
||||||
sort: sort.key,
|
sort: sort.key,
|
||||||
dir: sort.dir,
|
dir: sort.dir,
|
||||||
@@ -91,7 +93,7 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast(err instanceof Error ? err.message : 'Treffer nicht ladbar.', 'error');
|
toast(err instanceof Error ? err.message : 'Treffer nicht ladbar.', 'error');
|
||||||
}
|
}
|
||||||
}, [hours, agentId, source, minScore, knownOnly, showDiscarded, search, age, alarmOnly, sort, toast]);
|
}, [hours, agentId, source, minScore, knownOnly, showDiscarded, search, olderThan, youngerThan, alarmOnly, sort, toast]);
|
||||||
|
|
||||||
const loadOverview = useCallback(async () => {
|
const loadOverview = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -175,7 +177,7 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-ink-500">
|
<p className="text-sm text-ink-500">
|
||||||
Offene Stellen aus Jobbörsen in DACH – jedes Alter, gefiltert wird unten nach „Anzeigenalter“.
|
Offene Stellen aus Jobbörsen in DACH – jedes Alter, eingrenzen unten mit „Älter als“ / „Jünger als“.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
|
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
|
||||||
<span
|
<span
|
||||||
@@ -272,12 +274,31 @@ export const ScannerDashboard: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</Filter>
|
</Filter>
|
||||||
<Filter label="Anzeigenalter" className="w-44">
|
<Filter label="Älter als (Tage)" className="w-28">
|
||||||
<select value={age} onChange={(event) => setAge(event.target.value as typeof age)} className={inputClass}>
|
<input
|
||||||
<option value="">alle</option>
|
type="number"
|
||||||
<option value="recent">bis 21 Tage</option>
|
min={0}
|
||||||
<option value="old">älter als 21 Tage</option>
|
value={olderThan}
|
||||||
</select>
|
onChange={(event) => setOlderThan(event.target.value)}
|
||||||
|
placeholder="–"
|
||||||
|
title="Nur Anzeigen, die mindestens so viele Tage alt sind"
|
||||||
|
className={inputClass}
|
||||||
|
/>
|
||||||
|
</Filter>
|
||||||
|
<Filter label="Jünger als (Tage)" className="w-28">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={youngerThan}
|
||||||
|
onChange={(event) => setYoungerThan(event.target.value)}
|
||||||
|
placeholder="–"
|
||||||
|
title="Nur Anzeigen, die höchstens so viele Tage alt sind"
|
||||||
|
className={`${inputClass} ${
|
||||||
|
Number(olderThan) > 0 && Number(youngerThan) > 0 && Number(youngerThan) <= Number(olderThan)
|
||||||
|
? 'border-red-400'
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
</Filter>
|
</Filter>
|
||||||
<Filter label="Ab %" className="w-20">
|
<Filter label="Ab %" className="w-20">
|
||||||
<input
|
<input
|
||||||
|
|||||||
Reference in New Issue
Block a user