Guard against duplicate applications + capture jobs on any website

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>
This commit is contained in:
2026-07-03 18:32:17 +02:00
co-authored by Claude Opus 4.8
parent d8b552e57d
commit b1c92dd77b
7 changed files with 457 additions and 87 deletions
+50 -26
View File
@@ -84,6 +84,7 @@
gehalt: getSalary(root),
stellenbeschreibung: getDescription(root),
quelle_url: getSourceUrl(),
art: 'Indeed',
};
}
@@ -119,6 +120,16 @@
status.innerHTML = html;
}
function duplicateWarning(matches) {
const lines = (matches || []).slice(0, 5).map(function (m) {
const parts = [m.firma, m.stelle].filter(Boolean).join(' — ');
const meta = [m.datum, m.status].filter(Boolean).join(', ');
return '• ' + parts + (meta ? ' (' + meta + ')' : '');
});
return 'Für diese Stelle scheint bereits eine Bewerbung zu existieren:\n\n' +
lines.join('\n') + '\n\nTrotzdem als weitere Bewerbung importieren?';
}
function onClick(btn, status) {
const job = scrapeJob();
@@ -127,37 +138,50 @@
return;
}
btn.disabled = true;
const label = btn.querySelector('span');
const originalLabel = label ? label.textContent : '';
btn.innerHTML = '<span class="bt-spin"></span><span>Wird gesendet…</span>';
setStatus(status, '', '');
chrome.runtime.sendMessage({ type: 'IMPORT_JOB', payload: job }, (resp) => {
btn.innerHTML = ICON_SEND + '<span>' + (originalLabel || 'An Bewerbungs-Tracker senden') + '</span>';
const send = (payload) => {
btn.disabled = true;
btn.innerHTML = '<span class="bt-spin"></span><span>Wird gesendet…</span>';
setStatus(status, '', '');
if (chrome.runtime.lastError) {
btn.disabled = false;
setStatus(status, 'Fehler: ' + chrome.runtime.lastError.message, 'bt-err');
return;
}
if (!resp || !resp.ok) {
btn.disabled = false;
setStatus(status, (resp && resp.error) || 'Unbekannter Fehler.', 'bt-err');
return;
}
chrome.runtime.sendMessage({ type: 'IMPORT_JOB', payload: payload }, (resp) => {
btn.innerHTML = ICON_SEND + '<span>' + (originalLabel || 'An Bewerbungs-Tracker senden') + '</span>';
const link = resp.openUrl
? ` <a href="${resp.openUrl}" target="_blank" rel="noopener">Entwurf öffnen</a>`
: '';
setStatus(
status,
'✓ ' + (resp.message || 'Als Entwurf angelegt. Unterlagen werden erstellt.') + link,
'bt-ok'
);
// Re-enable after a moment so the user can send again if needed.
setTimeout(() => { btn.disabled = false; }, 1500);
});
if (chrome.runtime.lastError) {
btn.disabled = false;
setStatus(status, 'Fehler: ' + chrome.runtime.lastError.message, 'bt-err');
return;
}
if (resp && resp.duplicate) {
btn.disabled = false;
if (confirm(duplicateWarning(resp.matches))) {
send(Object.assign({}, payload, { force: true }));
} else {
setStatus(status, 'Import abgebrochen (mögliches Duplikat).', '');
}
return;
}
if (!resp || !resp.ok) {
btn.disabled = false;
setStatus(status, (resp && resp.error) || 'Unbekannter Fehler.', 'bt-err');
return;
}
const link = resp.openUrl
? ` <a href="${resp.openUrl}" target="_blank" rel="noopener">Entwurf öffnen</a>`
: '';
setStatus(
status,
'✓ ' + (resp.message || 'Als Entwurf angelegt. Unterlagen werden erstellt.') + link,
'bt-ok'
);
setTimeout(() => { btn.disabled = false; }, 1500);
});
};
send(job);
}
// ----- Injection + SPA handling -----------------------------------------