const $status = document.getElementById('status'); // Runs inside the MyAbacus page (every frame). Returns raw row texts; parsing happens here. function collectRows(rowSelector) { const DATE = /\d{1,2}[./]\d{1,2}[./]\d{2,4}|\d{4}-\d{2}-\d{2}/; const clean = (t) => (t || '').replace(/\s*\n\s*/g, ' \t ').trim(); let rows = []; try { rows = [...document.querySelectorAll(rowSelector || 'tr')].map((el) => clean(el.innerText)).filter(Boolean); } catch (e) { /* bad selector */ } if (rows.some((r) => DATE.test(r))) return { mode: 'rows', url: location.href, rows }; // Fallback: plain text lines (grids rendered with divs) const lines = (document.body?.innerText || '').split('\n').map((l) => l.trim()).filter(Boolean); return { mode: 'lines', url: location.href, rows: lines }; } async function extract() { $status.textContent = 'Extracting…'; const { settings } = await Store.get(); const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); const results = await browser.scripting.executeScript({ target: { tabId: tab.id, allFrames: true }, func: collectRows, args: [settings.parse.rowSelector], }); const p = settings.parse; const opts = { timeMode: p.timeMode, durationIndex: +p.durationIndex, sameDateRows: p.sameDateRows, year: +p.year || new Date().getFullYear() }; let days = []; const raw = []; for (const r of results) { if (!r.result) continue; raw.push(...r.result.rows); days = days.concat(AbacusParse.parseRows(r.result.rows, { ...opts, carryDate: r.result.mode === 'lines' })); } const { days: merged } = AbacusParse.fromText(AbacusParse.toText(days)); await Store.set({ daysText: AbacusParse.toText(merged), extractInfo: { at: new Date().toISOString(), url: tab.url, count: merged.length, raw: raw.slice(0, 2000) }, }); if (!merged.length) { $status.textContent = 'No dated hours found on this page.\nOpen the dashboard → "Raw text" to see what was read, or paste the table manually.'; return; } const total = merged.reduce((a, d) => a + d.minutes, 0); $status.textContent = `Found ${merged.length} day(s), ${U.fmtDur(total)} total\n${U.fmtDate(merged[0].date)} → ${U.fmtDate(merged[merged.length - 1].date)}`; browser.runtime.openOptionsPage(); } /* ---------- MyAbacus day-by-day crawl ---------- */ const $from = document.getElementById('from'); const $to = document.getElementById('to'); function showCrawl(c) { if (!c) return; $status.textContent = c.message; $status.className = c.status === 'error' ? 'err' : c.status === 'done' ? 'ok' : 'muted'; const running = c.status === 'running'; document.getElementById('crawl').disabled = running; document.getElementById('stop').classList.toggle('hidden', !running); } async function crawl() { // Must be the first await (still inside the click gesture). Needed for MyAbacus iframes: // activeTab alone only covers the top frame, and MV3 host permissions aren't always granted at install. const ALL = { origins: [''] }; if (!(await browser.permissions.contains(ALL))) { $status.textContent = 'Firefox will ask for site access (needed to read MyAbacus iframes). Then click again.'; if (await browser.permissions.request(ALL)) $status.textContent = 'Access granted: click “Read days” again.'; return; } const granted = true; const from = U.getField($from), to = U.getField($to); if (!from || !to || from > to) throw new Error('Pick a valid date range'); await browser.storage.local.set({ crawlRange: { from, to } }); const [tab] = await browser.tabs.query({ active: true, currentWindow: true }); const target = { tabId: tab.id, allFrames: true }; $status.textContent = 'Looking for the card…'; await browser.storage.local.set({ crawlRequest: { from, to } }); // One single call: Firefox doesn't reliably share globals between separate executeScript calls const results = await browser.scripting.executeScript({ target, files: ['/lib/util.js', '/lib/parse.js', '/lib/abacus-crawl.js', '/lib/abacus-crawl-start.js'], }); const res = results.map((r) => r.result || { skipped: true, url: `frame ${r.frameId}`, injectError: r.error?.message || String(r.error || 'no result') }); if (res.every((r) => r.skipped)) { const frames = res.map((r) => r.injectError ? `• ${r.url}: injection failed (${r.injectError})` : `• ${r.url}: ${r.shadowRoots} shadow roots, angle-right icon: ${r.angleRight ? 'yes' : 'no'}, title: ${r.title ? 'yes' : 'no'}`); throw new Error(`No "Gestion des temps" card found.${granted ? '' : ' (site access was refused: iframes can\'t be read)'}\n${frames.join('\n')}`); } const err = res.find((r) => r.error); if (!err) browser.runtime.openOptionsPage(); } document.getElementById('crawl').addEventListener('click', () => crawl().catch((e) => { $status.textContent = 'Error: ' + e.message; $status.className = 'err'; })); document.getElementById('stop').addEventListener('click', () => browser.storage.local.set({ crawlStop: true })); browser.storage.onChanged.addListener((ch) => { if (ch.crawl) showCrawl(ch.crawl.newValue); }); (async () => { const { crawlRange, crawl: last } = await browser.storage.local.get(['crawlRange', 'crawl']); const today = U.isoDate(new Date()); U.setupFields(); U.setField($from, crawlRange?.from || U.monday(today)); U.setField($to, crawlRange?.to || today); // Only show a stale "running" if it's recent (page may have been reloaded mid-crawl) if (last && (last.status !== 'running' || Date.now() - new Date(last.at) < 30000)) showCrawl(last); })(); document.getElementById('extract').addEventListener('click', () => extract().catch((e) => { $status.textContent = 'Error: ' + e.message; })); document.getElementById('open').addEventListener('click', () => browser.runtime.openOptionsPage());