Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 39 additions & 30 deletions extension-ready/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,45 +385,52 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {

const proxyUrl = await getProxyUrl();
let token = await ensureInstallToken(proxyUrl);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 45000);
const keepAlive = setInterval(() => chrome.storage.local.get('_sw_keepalive'), 20000);

let response = await fetch(`${proxyUrl}/api/cv/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({
try {
const body = JSON.stringify({
cvText,
jobDescription: message.jobDescription,
jobTitle: message.jobTitle || '',
company: message.company || '',
confirmedSkills: message.confirmedSkills || [],
}),
});
});

if (response.status === 401) {
await clearInstallToken();
token = await ensureInstallToken(proxyUrl);
response = await fetch(`${proxyUrl}/api/cv/analyze`, {
let response = await fetch(`${proxyUrl}/api/cv/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
body: JSON.stringify({
cvText,
jobDescription: message.jobDescription,
jobTitle: message.jobTitle || '',
company: message.company || '',
confirmedSkills: message.confirmedSkills || [],
}),
signal: controller.signal,
body,
});
}

if (response.status === 429) throw new Error(rateLimitError(response));
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `Error ${response.status}`);
}
if (response.status === 401) {
await clearInstallToken();
token = await ensureInstallToken(proxyUrl);
response = await fetch(`${proxyUrl}/api/cv/analyze`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
signal: controller.signal,
body,
});
}

if (response.status === 429) throw new Error(rateLimitError(response));
if (!response.ok) {
const err = await response.json().catch(() => ({}));
throw new Error(err.error || `Error ${response.status}`);
}

const data = await response.json();
sendResponse({ success: true, ...data });
const data = await response.json();
sendResponse({ success: true, ...data });
} finally {
clearTimeout(timeout);
clearInterval(keepAlive);
}
} catch (e) {
sendResponse({ error: e.message });
if (e?.name === 'AbortError') sendResponse({ error: 'Analysis timed out — please try again' });
else sendResponse({ error: e.message });
}
})();
return true;
Expand Down Expand Up @@ -575,9 +582,9 @@ async function handleStreamingAPICall(payload, requestId, tabId, frameId) {
const enrichedPayload = { ...payload, stream: true };

try {
const token = await ensureInstallToken(proxyUrl);
let token = await ensureInstallToken(proxyUrl);

const response = await fetch(`${proxyUrl}/api/generate`, {
const doRequest = () => fetch(`${proxyUrl}/api/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Expand All @@ -587,10 +594,12 @@ async function handleStreamingAPICall(payload, requestId, tabId, frameId) {
body: JSON.stringify(enrichedPayload)
});

let response = await doRequest();

if (response.status === 401) {
await clearInstallToken();
await ensureInstallToken(proxyUrl); // refresh token for next attempt
throw new Error('Token expired — please try again');
token = await ensureInstallToken(proxyUrl);
response = await doRequest();
}

if (!response.ok) {
Expand Down
14 changes: 12 additions & 2 deletions extension-ready/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,28 @@

.da-modal-close {
margin-left: auto;
background: none;
appearance: none;
width: 36px;
height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.08);
border: none;
border-radius: 8px;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
line-height: 1;
opacity: 0.8;
transition: opacity 0.2s;
flex-shrink: 0;
transition: opacity 0.2s, background 0.2s;
-webkit-tap-highlight-color: transparent;
}

.da-modal-close:hover {
background: rgba(255, 255, 255, 0.16);
opacity: 1;
}

Expand Down
2 changes: 1 addition & 1 deletion extension-ready/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class DraftApplyExtension {
<img class="da-modal-logo" src="${chrome.runtime.getURL('icons/icon128.png')}" alt="">
<span class="da-header-name">DraftApply</span>
<span class="da-context-badge" id="da-context-badge">No context</span>
<button class="da-modal-close" aria-label="Close">&times;</button>
<button class="da-modal-close" type="button" aria-label="Close">&times;</button>
</div>
<div class="da-modal-body">
<div class="da-context-info" id="da-context-info"></div>
Expand Down
29 changes: 18 additions & 11 deletions extension-ready/popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,6 @@ document.addEventListener('DOMContentLoaded', async () => {
let savingDraftTimer = null;
let statsResetTimer = null;

// Load saved state
await loadState();
await checkProxy();
await checkPageStatus();
await restoreTailorDraft();
await refreshStatsUI();

// ── Event listeners ──────────────────────────────────────────────────────

elements.saveCvBtn.addEventListener('click', saveCV);
Expand Down Expand Up @@ -141,6 +134,16 @@ document.addEventListener('DOMContentLoaded', async () => {
if (file) processFile(file);
});

// Load saved state after binding handlers so the popup never feels dead while
// proxy/page checks are warming up.
await Promise.allSettled([
loadState(),
checkProxy(),
checkPageStatus(),
restoreTailorDraft(),
refreshStatsUI(),
]);

// ── CV management ─────────────────────────────────────────────────────────

async function loadState() {
Expand Down Expand Up @@ -723,10 +726,14 @@ document.addEventListener('DOMContentLoaded', async () => {
async function downloadAsPdf() {
const text = elements.tailorOutput.value;
if (!text) return;
await chrome.storage.local.set({ tailoredCvExport: text });
chrome.tabs.create({ url: chrome.runtime.getURL('cv-export.html') });
await window.DraftApplyStats?.track?.('cvExports');
await refreshStatsUI();
try {
await chrome.storage.local.set({ tailoredCvExport: text });
await chrome.tabs.create({ url: chrome.runtime.getURL('cv-export.html') });
await window.DraftApplyStats?.track?.('cvExports');
await refreshStatsUI();
} catch (e) {
showTailorMessage('Could not open the export page. Please try again.', 'error');
}
}

function showTailorMessage(text, type = 'success') {
Expand Down
41 changes: 26 additions & 15 deletions extension-ready/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
cvExports: 'CV Export',
cvsTailored: 'Tailor CV',
};
let writeQueue = Promise.resolve();

function getStorage(storage) {
if (storage) return storage;
Expand Down Expand Up @@ -137,27 +138,37 @@
}

async function track(action, options = {}) {
if (!ACTIONS.includes(action)) return read(options);
const storage = getStorage(options.storage);
if (!storage) return emptyStats();
return enqueueWrite(async () => {
if (!ACTIONS.includes(action)) return read(options);
const storage = getStorage(options.storage);
if (!storage) return emptyStats();

const amount = Math.max(1, Math.floor(Number(options.amount || 1)));
const stats = await read({ storage });
const day = localDayKey(options.date || new Date());
const amount = Math.max(1, Math.floor(Number(options.amount || 1)));
const stats = await read({ storage });
const day = localDayKey(options.date || new Date());

stats.totals[action] += amount;
stats.days[day] = stats.days[day] || {};
for (const key of ACTIONS) stats.days[day][key] = Number(stats.days[day][key] || 0);
stats.days[day][action] += amount;
stats.totals[action] += amount;
stats.days[day] = stats.days[day] || {};
for (const key of ACTIONS) stats.days[day][key] = Number(stats.days[day][key] || 0);
stats.days[day][action] += amount;

await storage.set({ [STATS_KEY]: stats });
return stats;
await storage.set({ [STATS_KEY]: stats });
return stats;
});
}

async function reset(options = {}) {
const storage = getStorage(options.storage);
if (!storage) return;
await storage.remove(STATS_KEY);
return enqueueWrite(async () => {
const storage = getStorage(options.storage);
if (!storage) return;
await storage.remove(STATS_KEY);
});
}

function enqueueWrite(task) {
const next = writeQueue.catch(() => {}).then(task);
writeQueue = next.catch(() => {});
return next;
}

globalThis.DraftApplyStats = {
Expand Down
17 changes: 17 additions & 0 deletions tests/stats.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,21 @@ describe('productivity stats helper', () => {
expect(stored.totals.answersInserted).toBe(1);
expect(stored.days['2026-05-04'].answersInserted).toBe(1);
});

it('serializes same-context writes so quick repeated actions do not lose counts', async () => {
const stats = loadStatsHelper();
const storage = fakeStorage();

await Promise.all([
stats.track('answersInserted', { storage, date: new Date(2026, 4, 4, 9) }),
stats.track('answersInserted', { storage, date: new Date(2026, 4, 4, 9) }),
stats.track('cvExports', { storage, date: new Date(2026, 4, 4, 9) }),
]);

const stored = storage.data[stats.STATS_KEY];
expect(stored.totals.answersInserted).toBe(2);
expect(stored.totals.cvExports).toBe(1);
expect(stored.days['2026-05-04'].answersInserted).toBe(2);
expect(stored.days['2026-05-04'].cvExports).toBe(1);
});
});
Loading