// Tracker-Adresse ist fest (gehostete Instanz). Jeder Benutzer gibt seinen // eigenen REST-API-Key ein (X-API-Key), über den der Server den Import dem // richtigen Benutzerkonto zuordnet — siehe /api/indeed-import serverseitig. const TRACKER_URL = 'https://nextjobs.cc'; const $ = (id) => document.getElementById(id); const statusEl = $('status'); function setStatus(html, cls) { statusEl.innerHTML = html; statusEl.className = cls || ''; } // ---- Settings (API-Key) ---------------------------------------------------- chrome.storage.sync.get({ apiKey: '' }, (items) => { $('apiKey').value = items.apiKey || ''; }); $('saveBtn').addEventListener('click', () => { const key = $('apiKey').value.trim(); chrome.storage.sync.set({ apiKey: key }, () => { setStatus(key ? 'API-Key gespeichert.' : 'API-Key entfernt.', key ? 'ok' : ''); }); }); $('testBtn').addEventListener('click', async () => { const key = $('apiKey').value.trim(); if (!key) { setStatus('Bitte erst API-Key eingeben.', 'err'); return; } setStatus('Teste Verbindung …', ''); try { const res = await fetch(TRACKER_URL + '/api/v1/health', { headers: { 'X-API-Key': key }, }); if (res.ok) setStatus('Verbindung erfolgreich ✓ — Key gültig.', 'ok'); else if (res.status === 401) setStatus('Key abgelehnt (401). Bitte API-Key prüfen.', 'err'); else setStatus('Erreichbar, aber unerwartete Antwort (' + res.status + ').', 'err'); } catch (err) { setStatus('Nicht erreichbar: ' + err.message, 'err'); } }); // ---- Page extraction (injected into the active tab) ------------------------ // Runs in the page context. Best-effort job extraction from ANY site: // schema.org JobPosting (JSON-LD) → OpenGraph/meta →

/selection → canonical. function extractJobFromPage() { const txt = (s) => (s || '').replace(/\s+/g, ' ').trim(); const meta = (sel) => { const el = document.querySelector(sel); return el ? (el.getAttribute('content') || '') : ''; }; let jp = null; document.querySelectorAll('script[type="application/ld+json"]').forEach((s) => { if (jp) return; let data; try { data = JSON.parse(s.textContent); } catch (e) { return; } const arr = Array.isArray(data) ? data : (data['@graph'] ? data['@graph'] : [data]); for (const node of arr) { const t = node && node['@type']; if (t === 'JobPosting' || (Array.isArray(t) && t.indexOf('JobPosting') !== -1)) { jp = node; break; } } }); const out = { firma: '', stelle: '', ort: '', gehalt: '', stellenbeschreibung: '', quelle_url: '' }; if (jp) { out.stelle = txt(jp.title || ''); const org = jp.hiringOrganization; if (org) out.firma = txt(typeof org === 'string' ? org : (org.name || '')); const loc = Array.isArray(jp.jobLocation) ? jp.jobLocation[0] : jp.jobLocation; if (loc && loc.address) { const a = loc.address; out.ort = txt(typeof a === 'string' ? a : [a.addressLocality, a.addressRegion, a.postalCode].filter(Boolean).join(', ')); } const sal = jp.baseSalary; if (sal && sal.value) { const v = sal.value; const amount = v.value || (v.minValue != null && v.maxValue != null ? v.minValue + '-' + v.maxValue : (v.minValue || v.maxValue || '')); if (amount) out.gehalt = txt(String(amount) + ' ' + (sal.currency || '') + (v.unitText ? (' / ' + v.unitText) : '')); } if (jp.description) { const tmp = document.createElement('div'); tmp.innerHTML = jp.description; out.stellenbeschreibung = txt(tmp.innerText || tmp.textContent || ''); } } if (!out.stelle) { const h1 = document.querySelector('h1'); out.stelle = txt(meta('meta[property="og:title"]') || (h1 ? h1.innerText : '') || document.title); } if (!out.firma) { out.firma = txt(meta('meta[property="og:site_name"]') || meta('meta[name="author"]') || location.hostname.replace(/^www\./, '')); } // A non-trivial user text selection wins as the description (clear intent). const sel = txt(window.getSelection ? String(window.getSelection()) : ''); if (sel && sel.length > 40) out.stellenbeschreibung = sel; if (!out.stellenbeschreibung) out.stellenbeschreibung = txt(meta('meta[name="description"]') || meta('meta[property="og:description"]')); const canon = document.querySelector('link[rel="canonical"]'); out.quelle_url = (canon && canon.href) || meta('meta[property="og:url"]') || location.href; return out; } // Infer the "source" (art) select value from a URL host — mirrors the server. function deriveArt(url) { const host = (String(url || '').match(/^https?:\/\/([^/]+)/i) || [, ''])[1].toLowerCase(); if (!host) return 'Sonstiges'; if (host.indexOf('indeed') !== -1) return 'Indeed'; if (host.indexOf('stepstone') !== -1) return 'StepStone'; if (host.indexOf('arbeitsagentur') !== -1) return 'Arbeitsagentur'; if (/(linkedin|xing|monster|stellenanzeigen|kimeta|glassdoor|jobware|meinestadt|jobs\.|karriere\.)/.test(host)) return 'Online-Portal'; return 'Firmenwebsite'; } function fillForm(data) { data = data || {}; $('firma').value = data.firma || ''; $('stelle').value = data.stelle || ''; $('ort').value = data.ort || ''; $('gehalt').value = data.gehalt || ''; $('stellenbeschreibung').value = data.stellenbeschreibung || ''; $('quelle_url').value = data.quelle_url || ''; const art = deriveArt(data.quelle_url); const sel = $('art'); for (const opt of sel.options) { if (opt.value === art) { sel.value = art; break; } } } async function scanActivePage() { setStatus('Lese Seite aus …', ''); try { const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); if (!tab || !tab.id) { setStatus('Keine aktive Seite gefunden.', 'err'); return; } const results = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: extractJobFromPage }); const data = results && results[0] && results[0].result; fillForm(data); if (data && (data.stelle || data.firma)) setStatus('Ausgelesen - bitte prüfen und senden.', 'ok'); else setStatus('Konnte wenig auslesen - bitte manuell ergänzen.', ''); } catch (err) { // chrome://, Web Store and a few CSP-locked pages can't be scripted. setStatus('Diese Seite kann nicht automatisch ausgelesen werden - bitte manuell ausfüllen.', ''); } } // ---- Sending (via the background service worker) --------------------------- function duplicateWarning(matches) { const lines = (matches || []).slice(0, 5).map((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 anlegen?'; } function sendJob(payload) { $('sendBtn').disabled = true; setStatus('Wird gesendet …', ''); chrome.runtime.sendMessage({ type: 'IMPORT_JOB', payload: payload }, (resp) => { if (chrome.runtime.lastError) { $('sendBtn').disabled = false; setStatus('Fehler: ' + chrome.runtime.lastError.message, 'err'); return; } if (resp && resp.needKey) { $('sendBtn').disabled = false; setStatus('Bitte erst in den Einstellungen deinen API-Key hinterlegen.', 'err'); const s = document.getElementById('settings'); if (s) s.open = true; $('apiKey').focus(); return; } if (resp && resp.duplicate) { $('sendBtn').disabled = false; if (confirm(duplicateWarning(resp.matches))) { sendJob(Object.assign({}, payload, { force: true })); } else { setStatus('Abgebrochen (mögliches Duplikat).', ''); } return; } if (!resp || !resp.ok) { $('sendBtn').disabled = false; setStatus((resp && resp.error) || 'Unbekannter Fehler.', 'err'); return; } const link = resp.openUrl ? ' Entwurf öffnen' : ''; setStatus('✓ ' + (resp.message || 'Als Entwurf angelegt.') + link, 'ok'); setTimeout(() => { $('sendBtn').disabled = false; }, 1500); }); } $('sendBtn').addEventListener('click', () => { const payload = { firma: $('firma').value.trim(), stelle: $('stelle').value.trim(), ort: $('ort').value.trim(), gehalt: $('gehalt').value.trim(), stellenbeschreibung: $('stellenbeschreibung').value.trim(), quelle_url: $('quelle_url').value.trim(), art: $('art').value, }; if (!payload.firma || !payload.stelle) { setStatus('Bitte mindestens Firma und Stelle angeben.', 'err'); return; } sendJob(payload); }); $('rescanBtn').addEventListener('click', scanActivePage); // Auto-extract as soon as the popup opens. scanActivePage();