File size: 9,255 Bytes
3651f88
 
 
 
 
 
 
 
 
 
 
 
 
 
d5a4481
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3651f88
 
d5a4481
3651f88
 
 
 
 
 
 
 
 
 
 
 
a52b643
7759bfb
 
 
a52b643
 
 
 
 
 
7759bfb
f057ca2
 
 
 
 
 
d5a4481
 
 
 
 
3651f88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d38bb5
 
 
 
 
 
 
 
 
 
 
 
3651f88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7759bfb
 
 
f057ca2
 
 
 
 
 
 
 
 
 
 
 
 
 
7759bfb
 
 
 
d5a4481
 
 
 
 
 
3651f88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8d38bb5
 
 
 
 
 
 
 
3651f88
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
/**
 * 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';
    }
  });
});