/** * options.js — ATS Resume Generator extension options page * * chrome.storage.local key contract (shared with background.js in Plan 04): * resume_b64 {string} Base64-encoded PDF bytes * resume_sha256 {string} Hex SHA-256 of the original PDF bytes * resume_filename {string} Original filename for display * api_url {string} HF Space base URL (e.g. https://user-space.hf.space) * api_token {string} Bearer token sent as X-Api-Token header * * Background service worker (background.js) reads: * chrome.storage.local.get(['resume_b64', 'api_token', 'api_url']) */ // ─── Version display ────────────────────────────────────────────────────────── document.getElementById('ext_version').textContent = 'v' + chrome.runtime.getManifest().version; // ─── Test Connection button ────────────────────────────────────────────────── document.getElementById('test_conn_btn').addEventListener('click', async () => { const url = document.getElementById('api_url').value.trim(); const token = document.getElementById('api_token').value.trim(); const statusEl = document.getElementById('test_conn_status'); if (!url) { statusEl.textContent = 'Enter API URL first.'; statusEl.style.color = '#dc2626'; return; } statusEl.textContent = 'Testing...'; statusEl.style.color = '#64748b'; try { const r = await fetch(`${url.replace(/\/$/, '')}/api/health`, { headers: { 'X-Api-Token': token || '' }, signal: AbortSignal.timeout(10000), }); if (r.ok) { statusEl.textContent = 'Connected!'; statusEl.style.color = '#16a34a'; } else if (r.status === 401) { statusEl.textContent = 'Auth failed — check token.'; statusEl.style.color = '#dc2626'; } else { statusEl.textContent = `Error (HTTP ${r.status})`; statusEl.style.color = '#dc2626'; } } catch (e) { statusEl.textContent = 'Cannot reach backend.'; statusEl.style.color = '#dc2626'; } }); // ─── Load stored settings on page open ─────────────────────────────────────── chrome.storage.local.get( ['api_url', 'api_token', 'resume_filename', 'resume_sha256', 'resume_latex', 'gen_version_default', 'autofill_profile_json', 'default_cover_letter_latex'], (data) => { if (data.api_url) { document.getElementById('api_url').value = data.api_url; } if (data.api_token) { document.getElementById('api_token').value = data.api_token; } if (data.resume_filename) { const shortHash = (data.resume_sha256 || '').slice(0, 16); document.getElementById('resume_status').textContent = `Stored: ${data.resume_filename} (SHA-256: ${shortHash}...)`; } if (data.resume_latex && data.resume_latex.trim()) { document.getElementById('resume_latex').value = data.resume_latex; document.getElementById('latex_status').textContent = `LaTeX saved (${data.resume_latex.length.toLocaleString()} chars). Takes priority over the PDF.`; } else if (self.DEFAULT_RESUME_LATEX) { // R20: seed the built-in default resume so the extension works out-of-the-box. document.getElementById('resume_latex').value = self.DEFAULT_RESUME_LATEX; chrome.storage.local.set({ resume_latex: self.DEFAULT_RESUME_LATEX }); document.getElementById('latex_status').textContent = `Default resume loaded (${self.DEFAULT_RESUME_LATEX.length.toLocaleString()} chars). Edit to use your own.`; } if (data.gen_version_default) { document.getElementById('gen_version_default').value = data.gen_version_default; } if (data.autofill_profile_json) { document.getElementById('autofill_profile_json').value = data.autofill_profile_json; } if (data.default_cover_letter_latex && data.default_cover_letter_latex.trim()) { document.getElementById('cover_letter_latex').value = data.default_cover_letter_latex; document.getElementById('cl_latex_status').textContent = `Custom template saved (${data.default_cover_letter_latex.length.toLocaleString()} chars).`; } } ); // ─── SHA-256 helper via Web Crypto API (available in extension context) ─────── async function sha256Hex(buffer) { const hashBuffer = await crypto.subtle.digest('SHA-256', buffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } // ─── ArrayBuffer → base64 string ───────────────────────────────────────────── function bufToBase64(buffer) { let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } // ─── Auto-evict generation caches to free quota for settings ───────────────── // ponytail: settings always win over stale generation results async function ensureQuota(needBytes) { const LIMIT = chrome.storage.local.QUOTA_BYTES || 10485760; const used = await new Promise(r => chrome.storage.local.getBytesInUse(null, r)); if (used + needBytes < LIMIT * 0.85) return true; await new Promise(r => chrome.storage.local.remove( ['ats_results', 'ats_app_results', 'ats_bulk_history'], r)); const after = await new Promise(r => chrome.storage.local.getBytesInUse(null, r)); return after + needBytes < LIMIT * 0.95; } // ─── Save button handler ────────────────────────────────────────────────────── document.getElementById('save_btn').addEventListener('click', async () => { const api_url = document.getElementById('api_url').value.trim(); const api_token = document.getElementById('api_token').value.trim(); const fileInput = document.getElementById('resume_file'); const statusEl = document.getElementById('status_msg'); // Validate required fields if (!api_url || !api_token) { statusEl.textContent = 'Error: API URL and token are required.'; statusEl.style.color = '#dc2626'; return; } const toStore = { api_url, api_token }; // ─── LaTeX source (priority input) ───────────────────────────────────────── const latexSrc = document.getElementById('resume_latex').value.trim(); toStore.resume_latex = latexSrc; // store '' to allow clearing it toStore.gen_version_default = document.getElementById('gen_version_default').value; const autofillProfile = document.getElementById('autofill_profile_json').value.trim(); if (autofillProfile) { try { JSON.parse(autofillProfile); toStore.autofill_profile_json = autofillProfile; } catch (_) { statusEl.textContent = 'Error: Autofill profile must be valid JSON.'; statusEl.style.color = '#dc2626'; return; } } else { toStore.autofill_profile_json = ''; } document.getElementById('latex_status').textContent = latexSrc ? `LaTeX saved (${latexSrc.length.toLocaleString()} chars). Takes priority over the PDF.` : 'No LaTeX saved.'; const clLatex = document.getElementById('cover_letter_latex').value.trim(); toStore.default_cover_letter_latex = clLatex; document.getElementById('cl_latex_status').textContent = clLatex ? `Custom template saved (${clLatex.length.toLocaleString()} chars).` : 'No custom template — using built-in default.'; // Handle optional resume upload if (fileInput.files.length > 0) { const file = fileInput.files[0]; if (!file.name.toLowerCase().endsWith('.pdf')) { statusEl.textContent = 'Error: Please select a PDF file.'; statusEl.style.color = '#dc2626'; return; } const buffer = await file.arrayBuffer(); const resume_b64 = bufToBase64(buffer); const resume_sha256 = await sha256Hex(buffer); const resume_filename = file.name; toStore.resume_b64 = resume_b64; toStore.resume_sha256 = resume_sha256; toStore.resume_filename = resume_filename; document.getElementById('resume_status').textContent = `Stored: ${resume_filename} (SHA-256: ${resume_sha256.slice(0, 16)}...)`; } // Auto-evict generation caches if storage is tight const payloadSize = JSON.stringify(toStore).length * 2; if (!(await ensureQuota(payloadSize))) { statusEl.textContent = 'Error: Storage full even after clearing caches. Remove the PDF resume or reduce LaTeX size.'; statusEl.style.color = '#dc2626'; return; } // Persist to chrome.storage.local chrome.storage.local.set(toStore, () => { if (chrome.runtime.lastError) { statusEl.textContent = 'Error saving: ' + chrome.runtime.lastError.message; statusEl.style.color = '#dc2626'; } else { statusEl.textContent = 'Settings saved successfully.'; statusEl.style.color = '#16a34a'; } }); });