Duplicate safeguard: before creating an application (manual add and browser import) the server checks for an existing one for the same job — matched on a normalised source URL (job-id params like Indeed's jk pin the posting across paths/tracking) or an identical company + role (case/umlaut/whitespace-insensitive). On a match it returns 409 with the matches; the web form and the extension show the existing entry and re-submit with force=true only if the user confirms. Not a hard block, so legitimate re-applications stay possible. Universal capture: the extension popup becomes an editable capture form that works on any site. It extracts the active page on demand (schema.org JobPosting JSON-LD -> OpenGraph/meta -> h1/title/selection -> canonical URL), lets the user review/correct, and sends. The import route is now source-agnostic and derives the application source (art) from the URL instead of hardcoding Indeed; the Indeed on-page button remains as a fast path. Adds scripting/activeTab permissions. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
67 lines
2.2 KiB
JavaScript
67 lines
2.2 KiB
JavaScript
// Service worker: performs the cross-origin POST to the Bewerbungs-Tracker.
|
|
// Routing the request through the background worker (instead of the content
|
|
// script) keeps it independent of the Indeed page's Content-Security-Policy.
|
|
|
|
const DEFAULT_TRACKER_URL = 'http://localhost:3000';
|
|
|
|
function getTrackerUrl() {
|
|
return new Promise((resolve) => {
|
|
chrome.storage.sync.get({ trackerUrl: DEFAULT_TRACKER_URL }, (items) => {
|
|
let url = (items.trackerUrl || DEFAULT_TRACKER_URL).trim();
|
|
url = url.replace(/\/+$/, ''); // strip trailing slashes
|
|
resolve(url);
|
|
});
|
|
});
|
|
}
|
|
|
|
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
if (message && message.type === 'IMPORT_JOB') {
|
|
(async () => {
|
|
try {
|
|
const base = await getTrackerUrl();
|
|
const res = await fetch(base + '/api/indeed-import', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(message.payload),
|
|
});
|
|
|
|
let data = {};
|
|
try { data = await res.json(); } catch (_) { /* non-JSON response */ }
|
|
|
|
if (!res.ok) {
|
|
// Duplicate guard: surface the existing matches so the user can decide.
|
|
if (res.status === 409 && data && data.duplicate) {
|
|
sendResponse({
|
|
ok: false,
|
|
duplicate: true,
|
|
matches: data.matches || [],
|
|
error: (data && data.error) || 'Für diese Stelle existiert bereits eine Bewerbung.',
|
|
});
|
|
return;
|
|
}
|
|
sendResponse({
|
|
ok: false,
|
|
error: (data && data.error) || `Server antwortete mit ${res.status}`,
|
|
});
|
|
return;
|
|
}
|
|
|
|
sendResponse({
|
|
ok: true,
|
|
id: data.id,
|
|
message: data.message,
|
|
openUrl: data.url ? base + data.url : null,
|
|
});
|
|
} catch (err) {
|
|
sendResponse({
|
|
ok: false,
|
|
error:
|
|
'Konnte den Bewerbungs-Tracker nicht erreichen. Läuft er, und ist die ' +
|
|
'Adresse in den Erweiterungs-Einstellungen korrekt? (' + String(err.message) + ')',
|
|
});
|
|
}
|
|
})();
|
|
return true; // keep the message channel open for the async response
|
|
}
|
|
});
|