Scanner: Stellenanzeigen aus Jobbörsen (DACH, max. 21 Tage)

Neuer Menüpunkt Scanner mit Dashboard und Suchagenten, Zeitplan und
API-Schlüssel unter Einstellungen -> Scanner.

- Quellen: Agentur für Arbeit (API), StepStone, karriere.at,
  freelancermap (HTML) sowie Adzuna und Jooble (kostenlose Schlüssel)
- Quote = gefundene Schlagwörter, Titel zählt doppelt; keine KI
- Abgleich mit icon_companies, bekannte Firmen oben mit Abzeichen
- Zeitplan Vorgabe stündlich Mo-Fr 06-20 Uhr, einstellbar
- Sperre gegen parallele Läufe wie im perinco-Scanner

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-29 10:49:44 +02:00
co-authored by Claude Opus 5.5
parent 6fe5b18c11
commit 1b82f580db
23 changed files with 2894 additions and 2 deletions
+102
View File
@@ -432,3 +432,105 @@ export interface DashboardData {
export const dashboardApi = {
load: () => get<DashboardData>('/dashboard'),
};
/* ---------------------------------------------------------------- Scanner */
export type ScannerSourceId = 'ba' | 'stepstone' | 'karriere' | 'adzuna' | 'jooble' | 'freelancermap';
export type ScannerMatchStatus = 'neu' | 'gesehen' | 'in_bearbeitung' | 'verworfen';
export interface ScannerAgent {
id: number;
name: string;
keywords: string[];
sources: ScannerSourceId[];
excludes: string[];
is_active: boolean;
matches: number;
}
export interface ScannerSchedule {
active: boolean;
/** ISO-Wochentage, 1 = Montag */
days: number[];
from: string;
to: string;
/** Minuten */
interval: number;
/** höchstes Alter einer Anzeige in Tagen */
maxAge: number;
}
export interface ScannerSource {
id: ScannerSourceId;
label: string;
countries: string;
ready: boolean;
needsKey: 'adzuna' | 'jooble' | null;
last: { ok: boolean; count: number; errors: string[] } | null;
}
export interface ScannerOverview {
stats: { agents: number; jobs: number; matches: number; fresh: number; known: number };
sources: ScannerSource[];
schedule: ScannerSchedule;
lastScanAt: string | null;
lastRun: {
id: number;
trigger: 'auto' | 'manual';
started_at: string;
finished_at: string;
jobs_found: number;
matches_new: number;
} | null;
status: { running: boolean; since: string | null };
}
export interface ScannerMatch {
id: number;
score: number;
hits: string[];
status: ScannerMatchStatus;
created_at: string;
agent_id: number;
agent_name: string;
job_id: number;
source: ScannerSourceId;
title: string;
company: string | null;
location: string | null;
country: string | null;
employment_type: string | null;
url: string;
description: string | null;
published_at: string | null;
company_id: number | null;
company_name: string | null;
is_customer: boolean | null;
known: boolean;
}
export interface ScannerSecrets {
adzunaId: string | null;
adzunaKey: string | null;
joobleKey: string | null;
}
export type ScannerAgentInput = Pick<ScannerAgent, 'name' | 'keywords' | 'sources' | 'excludes' | 'is_active'>;
const put = <T>(path: string, body: unknown) =>
apiRequest<T>(path, { method: 'PUT', body: JSON.stringify(body) });
export const scannerApi = {
overview: () => get<ScannerOverview>('/scanner/overview'),
agents: () => get<{ agents: ScannerAgent[] }>('/scanner/agents'),
createAgent: (agent: ScannerAgentInput) => post<{ agent: ScannerAgent }>('/scanner/agents', agent),
updateAgent: (id: number, agent: ScannerAgentInput) => put<{ agent: ScannerAgent }>(`/scanner/agents/${id}`, agent),
removeAgent: (id: number) => del<{ ok: true }>(`/scanner/agents/${id}`),
matches: (params: Record<string, unknown> = {}) =>
get<{ total: number; matches: ScannerMatch[] }>(`/scanner/matches${query(params)}`),
setStatus: (id: number, status: ScannerMatchStatus) => patch<{ ok: true }>(`/scanner/matches/${id}`, { status }),
scan: () => post<{ started: true }>('/scanner/scan'),
settings: () => get<{ settings: ScannerSchedule; secrets: ScannerSecrets }>('/scanner/settings'),
saveSettings: (changes: Partial<ScannerSchedule> & Partial<Record<keyof ScannerSecrets, string>>) =>
patch<{ settings: ScannerSchedule; secrets: ScannerSecrets }>('/scanner/settings', changes),
};