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:
+50
-26
@@ -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 -----------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user