Spaces:
Sleeping
Sleeping
| /** | |
| * 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'; | |
| } | |
| }); | |
| }); | |