diff --git a/extension-ready/background.js b/extension-ready/background.js index 85bb064..796fa51 100644 --- a/extension-ready/background.js +++ b/extension-ready/background.js @@ -14,6 +14,20 @@ const pendingRequests = new Map(); // requestId -> AbortController +function rateLimitError(response) { + const resetHeader = response.headers.get('RateLimit-Reset') || response.headers.get('X-RateLimit-Reset'); + if (resetHeader) { + const resetSec = Number(resetHeader); + if (!isNaN(resetSec) && resetSec > 1e9) { + const resetTime = new Date(resetSec * 1000); + const hh = resetTime.getHours().toString().padStart(2, '0'); + const mm = resetTime.getMinutes().toString().padStart(2, '0'); + return `Rate limit reached — you can try again at ${hh}:${mm}.`; + } + } + return 'Rate limit reached — please try again in up to 60 minutes.'; +} + const DEFAULT_PROXY_URL = 'https://draftapply.onrender.com'; async function getProxyUrl() { @@ -104,7 +118,7 @@ async function ensureContentScriptInjected(tabId) { }); await chrome.scripting.executeScript({ target: { tabId, allFrames: true }, - files: ['page-extractor.js', 'content.js'] + files: ['stats.js', 'page-extractor.js', 'content.js'] }); } catch (err) { console.warn('Could not inject content script:', err.message); @@ -343,6 +357,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { }); } + 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}`); @@ -399,6 +414,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { }); } + 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}`); @@ -456,7 +472,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { if (!mainFrameReady) { try { await chrome.scripting.insertCSS({ target: { tabId, frameIds: [0] }, files: ['content.css'] }); - await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, files: ['page-extractor.js', 'content.js'] }); + await chrome.scripting.executeScript({ target: { tabId, frameIds: [0] }, files: ['stats.js', 'page-extractor.js', 'content.js'] }); } catch { /* restricted page */ } // Brief delay for content script to initialize after fresh injection await new Promise(r => setTimeout(r, 300)); diff --git a/extension-ready/content.js b/extension-ready/content.js index 1b31221..c01cb1f 100644 --- a/extension-ready/content.js +++ b/extension-ready/content.js @@ -379,6 +379,7 @@ class DraftApplyExtension { this.setNativeValue(target, message.answer); this.dispatchInputEvents(target, message.answer); this.showNotification('Answer inserted!'); + globalThis.DraftApplyStats?.track?.('answersInserted')?.catch?.(() => {}); } catch (e) { console.warn('[DraftApply] Insert from parent failed:', e); } @@ -1276,6 +1277,7 @@ class DraftApplyExtension { this.currentField = target; this.hideModal(); this.showNotification('Answer inserted!'); + globalThis.DraftApplyStats?.track?.('answersInserted')?.catch?.(() => {}); } catch (e) { console.warn('[DraftApply] Insert failed:', e); this.showNotification('Could not insert into that field. Try clicking the field and typing once, then Insert again.', 'error'); diff --git a/extension-ready/cv-export.html b/extension-ready/cv-export.html index 610b717..bacbb74 100644 --- a/extension-ready/cv-export.html +++ b/extension-ready/cv-export.html @@ -198,6 +198,9 @@ Chrome only adds those when no @page margin is set via CSS. */ @page { margin: 0; } + /* Keep each experience/education entry together — no mid-entry page breaks */ + .cv-entry { break-inside: avoid; } + @media print { body { background: white; } #toolbar { display: none !important; } @@ -207,6 +210,13 @@ padding: 20mm 18mm; min-height: auto; } + .cv-entry { break-inside: avoid; } + .cv-entry-row { break-after: avoid; } + .cv-job-title { break-before: avoid; break-after: avoid; } + .cv-role-focus { break-before: avoid; break-after: avoid; } + .cv-bullets { break-before: avoid; } + .cv-bullets li { break-inside: avoid; } + .cv-section-header { break-after: avoid; } } @@ -215,6 +225,7 @@
${esc(title)}
`; afterEntryRow = false; @@ -356,7 +514,7 @@ function formatCvToHtml(rawText) { } // Pending company + next short non-date line → flush company (no dates), emit as job title - if (pendingCompany !== null && line.length < 70 && !isContactLine(line) && !isDateLine(line)) { + if (pendingCompany !== null && line.length < 120 && !isContactLine(line) && !isDateLine(line)) { flushPendingCompany(null); afterEntryRow = false; const title = stripJobTitleLabel(line); @@ -393,5 +551,6 @@ function formatCvToHtml(rawText) { closeList(); flushPendingCompany(null); + closeEntry(); return html; } diff --git a/extension-ready/manifest.json b/extension-ready/manifest.json index 00307c5..43f9318 100644 --- a/extension-ready/manifest.json +++ b/extension-ready/manifest.json @@ -51,7 +51,7 @@ "https://*.glassdoor.com/*", "https://*.glassdoor.co.uk/*" ], - "js": ["page-extractor.js", "content.js"], + "js": ["stats.js", "page-extractor.js", "content.js"], "css": ["content.css"], "run_at": "document_idle", "all_frames": true diff --git a/extension-ready/page-extractor.js b/extension-ready/page-extractor.js index 491bcf7..310a8bb 100644 --- a/extension-ready/page-extractor.js +++ b/extension-ready/page-extractor.js @@ -435,15 +435,22 @@ class PageExtractor { extractCleanPageText() { if (!document.body) return ''; - const clone = document.body.cloneNode(true); - const removeSelectors = [ - 'script', 'style', 'nav', 'footer', 'header', - 'aside', '.sidebar', '.navigation', '.menu', - '.cookie', '.popup', '.modal', '.ad', '.advertisement' - ]; - - for (const selector of removeSelectors) { - clone.querySelectorAll(selector).forEach(el => el.remove()); + let bodyText = ''; + try { + const clone = document.body.cloneNode(true); + const removeSelectors = [ + 'script', 'style', 'nav', 'footer', 'header', + 'aside', '.sidebar', '.navigation', '.menu', + '.cookie', '.popup', '.modal', '.ad', '.advertisement' + ]; + for (const selector of removeSelectors) { + clone.querySelectorAll(selector).forEach(el => el.remove()); + } + bodyText = clone.textContent || ''; + } catch { + // cloneNode can throw on pages with date/number inputs whose value attribute + // is "undefined" or out of range (e.g. Ashby application forms) + bodyText = document.body.innerText || document.body.textContent || ''; } const shadowText = this.openRoots() @@ -451,7 +458,7 @@ class PageExtractor { .map(root => root.textContent || '') .join('\n'); - return this.cleanText(`${clone.textContent || ''}\n${shadowText}`); + return this.cleanText(`${bodyText}\n${shadowText}`); } /** diff --git a/extension-ready/popup.html b/extension-ready/popup.html index 165c0ed..ee1a67b 100644 --- a/extension-ready/popup.html +++ b/extension-ready/popup.html @@ -308,6 +308,95 @@ background: #fafafa; } + /* ── Productivity stats ── */ + .stats-card { + background: white; + border: 1.5px solid #e2e8f0; + border-radius: 9px; + margin-top: 10px; + overflow: hidden; + } + + .stats-toggle { + width: 100%; + display: flex; + align-items: center; + gap: 8px; + padding: 9px 11px; + background: transparent; + border: 0; + font-family: inherit; + text-align: left; + cursor: pointer; + color: inherit; + } + + .stats-title { + font-size: 10.5px; + font-weight: 700; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.45px; + flex: 0 0 auto; + } + + .stats-summary { + min-width: 0; + flex: 1; + color: #475569; + font-size: 11.5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .stats-chevron { + color: #94a3b8; + font-size: 13px; + line-height: 1; + transition: transform 0.2s; + } + + .stats-card.expanded .stats-chevron { transform: rotate(180deg); } + + .stats-details { + border-top: 1px solid #f1f5f9; + padding: 10px 11px 11px; + } + + .stats-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px 10px; + } + + .stats-item-value { + color: #1e293b; + font-size: 13px; + font-weight: 700; + line-height: 1.2; + } + + .stats-item-label { + color: #94a3b8; + font-size: 10.5px; + margin-top: 2px; + line-height: 1.25; + } + + .stats-reset-btn { + margin-top: 10px; + padding: 0; + background: none; + border: 0; + color: #94a3b8; + font: inherit; + font-size: 11px; + cursor: pointer; + } + + .stats-reset-btn:hover { color: #ef4444; } + /* ── Page status ── */ .page-status-card { display: flex; @@ -758,6 +847,48 @@ + +