From 25f323ff49603973e563d9be8dc72ef5ebb141ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Wed, 7 Oct 2026 16:01:15 +0200 Subject: [PATCH] fix crawling + correct date/time format --- README.md | 4 +- app/app.css | 1 + app/app.html | 16 +++---- app/app.js | 37 ++++++++-------- lib/abacus-crawl-start.js | 3 ++ lib/abacus-crawl.js | 92 ++++++++++++++++++++++++++++++--------- lib/parse.js | 14 +++--- lib/util.js | 43 ++++++++++++++++++ popup/popup.html | 4 +- popup/popup.js | 34 +++++++++++---- 10 files changed, 183 insertions(+), 65 deletions(-) create mode 100644 lib/abacus-crawl-start.js diff --git a/README.md b/README.md index 1e323db..2db9be5 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Or `npx web-ext run` from this folder. *Generic extraction* (under "Other pages") is the old table/text scraper, for other layouts. 2. Dashboard opens with one line per day (editable): ``` - 2026-10-05 08:00-12:00 13:00-17:24 # stamped intervals - 2026-10-06 8:24 # total only + 05/10/2026 08:00-12:00 13:00-17:24 # stamped intervals + 06/10/2026 8:24 # total only ``` If extraction fails, select the table in Abacus, copy, and use *Paste raw text*. 3. **Compare with Kimai & plan**: for each day it computes `Abacus - existing Kimai`, then fills the gap: diff --git a/app/app.css b/app/app.css index 6da8596..b90e05a 100644 --- a/app/app.css +++ b/app/app.css @@ -36,3 +36,4 @@ td.num { font-variant-numeric: tabular-nums; white-space: nowrap; } .weekdays { display: flex; gap: 6px; } .hidden { display: none !important; } pre { white-space: pre-wrap; font-size: 12px; max-height: 300px; overflow: auto; } +.invalid { border-color: var(--err); } diff --git a/app/app.html b/app/app.html index 89be378..54fd946 100644 --- a/app/app.html +++ b/app/app.html @@ -20,7 +20,7 @@

1. Abacus hours

Open MyAbacus, then click the toolbar icon → “Extract hours from this tab”. Or paste below.

- +
@@ -39,8 +39,8 @@

2. Plan

- - + +
@@ -66,7 +66,7 @@
- +
@@ -79,9 +79,9 @@
- - - + + +
@@ -126,7 +126,7 @@
- +
diff --git a/app/app.js b/app/app.js index fd6c645..332e779 100644 --- a/app/app.js +++ b/app/app.js @@ -70,8 +70,8 @@ function updateDaysSummary() { ? `${days.length} day(s), ${U.fmtDur(total)} total` + (errors.length ? ` — ${errors.join('; ')}` : '') : errors.join('; '); $('days-summary').className = errors.length ? 'warn' : 'muted'; - if (days.length && !$('from').value) $('from').value = days[0].date; - if (days.length && !$('to').value) $('to').value = days[days.length - 1].date; + if (days.length && !$('from').value) U.setField($('from'), days[0].date); + if (days.length && !$('to').value) U.setField($('to'), days[days.length - 1].date); } $('days').addEventListener('input', () => { @@ -95,7 +95,7 @@ $('parse-raw').addEventListener('click', () => { async function buildPlan() { $('plan-status').textContent = 'Loading Kimai entries…'; $('plan-status').className = 'muted'; - const from = $('from').value, to = $('to').value; + const from = U.getField($('from')), to = U.getField($('to')); const days = currentDays().days.filter((d) => (!from || d.date >= from) && (!to || d.date <= to)); if (!days.length) throw new Error('No Abacus days in range'); @@ -134,7 +134,7 @@ function renderPlan() { type: 'checkbox', checked: p.include, disabled: !p.entries.length, onchange: (ev) => { p.include = ev.target.checked; }, })), - h('td', { class: 'num' }, `${U.weekdayName(p.date)} ${p.date}`), + h('td', { class: 'num' }, `${U.weekdayName(p.date)} ${U.fmtDate(p.date)}`), h('td', { class: 'num' }, U.fmtDur(p.target)), h('td', { class: 'num' }, U.fmtDur(p.existingMin)), h('td', { class: 'num' }, p.plannedMin ? U.fmtDur(p.plannedMin) : '—'), @@ -205,13 +205,13 @@ function resetRuleForm(rule = null) { $('rule-form-title').textContent = rule ? `Edit rule “${rule.name}”` : 'New rule'; $('r-name').value = rule?.name || ''; $('r-hours').value = rule && +rule.minutes ? U.fmtDur(rule.minutes) : ''; - $('r-start').value = rule?.startTime || ''; + U.setField($('r-start'), rule?.startTime); $('r-enabled').checked = rule ? rule.enabled : true; $('r-desc').value = rule?.description || ''; $('r-every').value = rule?.everyNWeeks || 1; - $('r-anchor').value = rule?.anchorDate || ''; - $('r-from').value = rule?.validFrom || ''; - $('r-to').value = rule?.validTo || ''; + U.setField($('r-anchor'), rule?.anchorDate); + U.setField($('r-from'), rule?.validFrom); + U.setField($('r-to'), rule?.validTo); const wd = rule?.weekdays || [1]; $('r-weekdays').querySelectorAll('input').forEach((i) => { i.checked = wd.includes(+i.value); }); fillProjectSelect($('r-project'), rule?.project); @@ -227,6 +227,8 @@ $('r-save').addEventListener('click', async () => { const weekdays = [...$('r-weekdays').querySelectorAll('input:checked')].map((i) => +i.value); const err = (m) => { $('r-status').textContent = m; $('r-status').className = 'err'; }; if (hoursRaw && !minutes) return err('Invalid hours'); + const bad = ['r-start', 'r-anchor', 'r-from', 'r-to'].find((id) => $(id).value.trim() && !U.getField($(id))); + if (bad) return err(`Invalid ${$(bad).dataset.kind === 'date' ? 'date (dd/mm/yyyy)' : 'time (hh:mm)'}`); if (!weekdays.length) return err('Pick at least one weekday'); if (!$('r-project').value || !$('r-activity').value) return err('Pick project and activity'); @@ -235,14 +237,14 @@ $('r-save').addEventListener('click', async () => { name: $('r-name').value.trim() || 'Rule', enabled: $('r-enabled').checked, weekdays, minutes, - startTime: $('r-start').value, + startTime: U.getField($('r-start')), project: +$('r-project').value, activity: +$('r-activity').value, description: $('r-desc').value.trim(), everyNWeeks: Math.max(1, +$('r-every').value || 1), - anchorDate: $('r-anchor').value, - validFrom: $('r-from').value, - validTo: $('r-to').value, + anchorDate: U.getField($('r-anchor')), + validFrom: U.getField($('r-from')), + validTo: U.getField($('r-to')), }; const i = state.rules.findIndex((r) => r.id === rule.id); if (i >= 0) state.rules[i] = rule; else state.rules.push(rule); @@ -256,7 +258,7 @@ function describeWhen(r) { let s = r.weekdays.map((d) => U.WEEKDAYS[d - 1]).join(', '); if (+r.everyNWeeks > 1) s += ` · every ${r.everyNWeeks} weeks`; if (r.startTime) s += ` @ ${r.startTime}`; - if (r.validFrom || r.validTo) s += ` · ${r.validFrom || '…'} → ${r.validTo || '…'}`; + if (r.validFrom || r.validTo) s += ` · ${U.fmtDate(r.validFrom) || '…'} → ${U.fmtDate(r.validTo) || '…'}`; return s; } @@ -296,7 +298,7 @@ function loadSettingsForm() { $('s-token').value = s.kimaiToken; $('s-desc').value = s.defaultDescription; $('s-tags').value = s.tags; - $('s-daystart').value = s.dayStart; + U.setField($('s-daystart'), s.dayStart); $('s-skip').checked = s.skipIfRulePresent; $('p-selector').value = s.parse.rowSelector; $('p-mode').value = s.parse.timeMode; @@ -319,7 +321,7 @@ function readSettingsForm() { defaultActivity: +$('s-activity').value || null, defaultDescription: $('s-desc').value.trim(), tags: $('s-tags').value.trim(), - dayStart: $('s-daystart').value || '08:00', + dayStart: U.getField($('s-daystart')) || '08:00', skipIfRulePresent: $('s-skip').checked, parse: { rowSelector: $('p-selector').value.trim(), @@ -410,13 +412,14 @@ $('s-import').addEventListener('change', async (ev) => { /* ---------- init ---------- */ (async () => { + U.setupFields(); const s = await Store.get(); state.settings = s.settings; state.rules = s.rules; state.catalog = s.catalog; $('days').value = s.daysText; if (s.extractInfo) { - $('extract-info').textContent = `Last extraction: ${s.extractInfo.count} day(s) from ${s.extractInfo.url} at ${new Date(s.extractInfo.at).toLocaleString()}`; + $('extract-info').textContent = `Last extraction: ${s.extractInfo.count} day(s) from ${s.extractInfo.url} at ${U.fmtDateTime(new Date(s.extractInfo.at))}`; $('raw-last').textContent = (s.extractInfo.raw || []).join('\n'); } loadSettingsForm(); @@ -436,7 +439,7 @@ browser.storage.onChanged.addListener((changes) => { } if (changes.extractInfo?.newValue) { const i = changes.extractInfo.newValue; - $('extract-info').textContent = `Last extraction: ${i.count} day(s) from ${i.url} at ${new Date(i.at).toLocaleString()}`; + $('extract-info').textContent = `Last extraction: ${i.count} day(s) from ${i.url} at ${U.fmtDateTime(new Date(i.at))}`; $('raw-last').textContent = (i.raw || []).join('\n'); } }); diff --git a/lib/abacus-crawl-start.js b/lib/abacus-crawl-start.js new file mode 100644 index 0000000..47be75b --- /dev/null +++ b/lib/abacus-crawl-start.js @@ -0,0 +1,3 @@ +// Last file of the injection (same call as util/parse/abacus-crawl, so globals are shared). +// Range comes from storage since files can't take arguments. The returned promise is the injection result. +browser.storage.local.get('crawlRequest').then(({ crawlRequest: r }) => AbacusCrawl.run(r.from, r.to)); diff --git a/lib/abacus-crawl.js b/lib/abacus-crawl.js index d494d5b..73925aa 100644 --- a/lib/abacus-crawl.js +++ b/lib/abacus-crawl.js @@ -3,20 +3,58 @@ // in browser.storage.local as it goes, so the popup can be closed meanwhile. var AbacusCrawl = (() => { const SEL = { - left: 'vaadin-button[movie-id="leftButton"]', - right: 'vaadin-button[movie-id="rightButton"]', + left: '[movie-id="leftButton"]', + right: '[movie-id="rightButton"]', nav: '.proj-date-navigation', - from: 'vaadin-text-field[id^="id_history_from_"]', - to: 'vaadin-text-field[id^="id_history_to_"]', - total: '#inOut .total-label .va-label-text', + from: '[id^="id_history_from_"]', + to: '[id^="id_history_to_"]', + total: '.total-label .va-label-text', }; const DATE_RE = /(\d{1,2})\.(\d{1,2})\.(\d{4})/; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); - const present = () => !!document.querySelector(SEL.right); + // Vaadin renders inside shadow roots: plain document.querySelector can't see them. + // Firefox content scripts also get closed roots through openOrClosedShadowRoot. + const shadowOf = (el) => el.openOrClosedShadowRoot || el.shadowRoot || null; + + function allRoots(root = document, out = []) { + out.push(root); + const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); + for (let n = walker.currentNode; n; n = walker.nextNode()) { + const sr = n.nodeType === 1 && shadowOf(n); + if (sr) allRoots(sr, out); + } + return out; + } + + function deepFind(sel) { + for (const r of allRoots()) { + const el = r.querySelector(sel); + if (el) return el; + } + return null; + } + + function findRightButton() { + return deepFind(SEL.right) || deepFind('.fa-angle-right')?.closest('vaadin-button, button') || null; + } + + // Root node (document or shadow root) holding the card; re-located if Vaadin re-renders it + let card = null; + function root() { + if (!card || !card.btn.isConnected) { + const btn = findRightButton(); + card = btn ? { btn, node: btn.getRootNode() } : null; + } + return card?.node || null; + } + const q = (sel) => root()?.querySelector(sel) || null; + const qa = (sel) => [...(root()?.querySelectorAll(sel) || [])]; + + const present = () => !!root(); function readDate() { - const nav = document.querySelector(SEL.nav); + const nav = q(SEL.nav) || card?.btn.parentElement; if (!nav) return null; // sr-only "Date actuelle 02.10.2026", else the date picker input const m = DATE_RE.exec(nav.textContent) || DATE_RE.exec(nav.querySelector('input')?.value || ''); @@ -24,27 +62,38 @@ var AbacusCrawl = (() => { } // Native inside the vaadin-text-field (custom element props aren't visible from here) - const fieldValue = (tf) => (tf.querySelector('input')?.value || '').trim(); + const fieldValue = (tf) => ((tf.tagName === 'INPUT' ? tf : tf.querySelector('input'))?.value || '').trim(); const suffix = (tf) => tf.id.replace(/^id_history_(from|to)_/, ''); function readDay() { - const tos = new Map([...document.querySelectorAll(SEL.to)].map((tf) => [suffix(tf), fieldValue(tf)])); + const tos = new Map(qa(SEL.to).map((tf) => [suffix(tf), fieldValue(tf)])); const intervals = [], open = []; - for (const tf of document.querySelectorAll(SEL.from)) { + for (const tf of qa(SEL.from)) { const s = U.toMin(fieldValue(tf)), e = U.toMin(tos.get(suffix(tf)) || ''); if (s == null) continue; if (e == null) open.push(U.fmtTime(s)); else if (e > s) intervals.push({ start: s, end: e }); } - const totalText = document.querySelector(SEL.total)?.textContent.trim() || ''; + const totalText = q(SEL.total)?.textContent.trim() || ''; return { date: readDate(), intervals: AbacusParse.union(intervals), open, total: U.toMin(totalText) }; } + // What this frame sees, to explain a "not found" + function diagnose() { + const roots = allRoots(); + return { + url: location.href, + shadowRoots: roots.length - 1, + angleRight: roots.some((r) => r.querySelector('.fa-angle-right')), + title: roots.some((r) => /Gestion des temps/.test(r.textContent || '')), + }; + } + // Wait until the date changed and the card content stopped moving async function waitLoaded(prevDate, timeout = 15000) { const t0 = Date.now(); while (readDate() === prevDate) { - if (Date.now() - t0 > timeout) throw new Error(`MyAbacus did not move away from ${prevDate}`); + if (Date.now() - t0 > timeout) throw new Error(`MyAbacus did not move away from ${U.fmtDate(prevDate)}`); await sleep(100); } let last = '', stable = 0; @@ -58,7 +107,8 @@ var AbacusCrawl = (() => { async function step(dir) { const prev = readDate(); - document.querySelector(dir < 0 ? SEL.left : SEL.right).click(); + const btn = dir < 0 ? (q(SEL.left) || deepFind('.fa-angle-left')?.closest('vaadin-button, button')) : findRightButton(); + btn.click(); await waitLoaded(prev); } @@ -70,15 +120,15 @@ var AbacusCrawl = (() => { if (d.open.length) notes.push(`clocked in at ${d.open.join(', ')} without leaving`); if (d.total != null && d.intervals.length && d.total !== sum) notes.push(`Abacus total says ${U.fmtDur(d.total)}`); const note = notes.length ? ` # ${notes.join('; ')}` : ''; - if (iv) return `${d.date} ${iv}${note}`; - if (d.total) return `${d.date} ${U.fmtDur(d.total)}${note}`; - return `# ${d.date} nothing${note}`; + if (iv) return `${U.fmtDate(d.date)} ${iv}${note}`; + if (d.total) return `${U.fmtDate(d.date)} ${U.fmtDur(d.total)}${note}`; + return `# ${U.fmtDate(d.date)} nothing${note}`; }).join('\n'); } async function run(from, to) { if (window.__abacusCrawling) return { error: 'already running' }; - if (!present()) return { skipped: true }; // other frame + if (!present()) return { skipped: true, ...diagnose() }; // other frame window.__abacusCrawling = true; const progress = (p) => browser.storage.local.set({ crawl: { ...p, at: new Date().toISOString() } }); const days = []; @@ -92,7 +142,7 @@ var AbacusCrawl = (() => { for (const dir of [-1, 1]) { while (dir < 0 ? cur > from : cur < from) { if (++n > maxSteps) throw new Error('Too many steps'); - await progress({ status: 'running', message: `Navigating to ${from} (at ${cur})` }); + await progress({ status: 'running', message: `Navigating to ${U.fmtDate(from)} (at ${U.fmtDate(cur)})` }); await step(dir); cur = readDate(); } @@ -102,7 +152,7 @@ var AbacusCrawl = (() => { const { crawlStop } = await browser.storage.local.get('crawlStop'); if (crawlStop) throw new Error('Stopped'); days.push(readDay()); - await progress({ status: 'running', message: `Read ${cur} (${days.length} day(s))` }); + await progress({ status: 'running', message: `Read ${U.fmtDate(cur)} (${days.length} day(s))` }); if (cur >= to || ++n > maxSteps) break; await step(1); cur = readDate(); @@ -113,7 +163,7 @@ var AbacusCrawl = (() => { daysText: text, extractInfo: { at: new Date().toISOString(), url: location.href, count: parsed.length, raw: days.map((d) => JSON.stringify(d)) }, }); - await progress({ status: 'done', message: `Done: ${parsed.length} worked day(s) between ${from} and ${to}` }); + await progress({ status: 'done', message: `Done: ${parsed.length} worked day(s) between ${U.fmtDate(from)} and ${U.fmtDate(to)}` }); return { days }; } catch (e) { if (days.length) await browser.storage.local.set({ daysText: toLines(days) + `\n# stopped: ${e.message}` }); @@ -124,5 +174,5 @@ var AbacusCrawl = (() => { } } - return { run, readDay, readDate, present }; + return { run, readDay, readDate, present, diagnose }; })(); diff --git a/lib/parse.js b/lib/parse.js index 1903083..a5f77c2 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -122,13 +122,13 @@ var AbacusParse = (() => { } // Normalized one-line-per-day format used in the dashboard editor: - // 2026-10-07 08:00-12:00 13:00-17:24 - // 2026-10-08 8:24 + // 07/10/2026 08:00-12:00 13:00-17:24 + // 08/10/2026 8:24 function toText(days) { return days .map((d) => d.intervals.length - ? `${d.date} ${d.intervals.map((iv) => `${U.fmtTime(iv.start)}-${U.fmtTime(iv.end)}`).join(' ')}` - : `${d.date} ${U.fmtDur(d.minutes)}`) + ? `${U.fmtDate(d.date)} ${d.intervals.map((iv) => `${U.fmtTime(iv.start)}-${U.fmtTime(iv.end)}`).join(' ')}` + : `${U.fmtDate(d.date)} ${U.fmtDur(d.minutes)}`) .join('\n'); } @@ -137,8 +137,10 @@ var AbacusParse = (() => { text.split('\n').forEach((line, n) => { line = line.replace(/#.*/, '').trim(); if (!line) return; - const m = /^(\d{4}-\d{2}-\d{2})\s+(.+)$/.exec(line); - if (!m) return errors.push(`line ${n + 1}: expected "YYYY-MM-DD ..."`); + const m = /^(\S+)\s+(.+)$/.exec(line); + const date = m && U.parseUserDate(m[1]); + if (!date) return errors.push(`line ${n + 1}: expected "dd/mm/yyyy ..."`); + m[1] = date; const ivs = ranges(m[2]); if (ivs.length) return days.push({ date: m[1], intervals: union(ivs), minutes: sumIntervals(union(ivs)) }); const min = U.parseHours(m[2]); diff --git a/lib/util.js b/lib/util.js index c2772b0..1db43b2 100644 --- a/lib/util.js +++ b/lib/util.js @@ -57,8 +57,51 @@ var U = (() => { const uid = () => Math.random().toString(36).slice(2, 10); + /* Swiss display: 01/09/2026, 13:30. Internally always ISO dates + HH:MM. */ + const fmtDate = (iso) => (iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}` : ''); + + // "1/9/2026", "01.09.2026", "01-09-26", "2026-09-01" -> "2026-09-01" (null if invalid) + const parseUserDate = (s) => { + s = String(s || '').trim(); + let m, y, mo, d; + if ((m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s))) [, y, mo, d] = m; + else if ((m = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4}|\d{2})$/.exec(s))) [, d, mo, y] = m; + else return null; + if (y.length === 2) y = '20' + y; + const dt = new Date(+y, +mo - 1, +d); + return dt.getMonth() === +mo - 1 && dt.getDate() === +d ? isoDate(dt) : null; + }; + + const fmtDateTime = (dt) => `${fmtDate(isoDate(dt))} ${fmtTime(dt.getHours() * 60 + dt.getMinutes())}`; + + // "930", "9:30", "9.30", "9h30", "9" -> "09:30" (null if invalid) + const parseUserTime = (s) => { + s = String(s || '').trim(); + let m; + if ((m = /^(\d{1,2})$/.exec(s))) m = [s, m[1], '0']; + else if (!(m = /^(\d{1,2})[:.h]?(\d{2})$/.exec(s))) return null; + const min = +m[1] * 60 + +m[2]; + return +m[1] < 24 && +m[2] < 60 ? fmtTime(min) : null; + }; + + // Text inputs with data-kind="date" | "time": read/write ISO / HH:MM, show Swiss format + const getField = (el) => (el.dataset.kind === 'date' ? parseUserDate(el.value) : parseUserTime(el.value)) || ''; + const setField = (el, v) => { el.value = el.dataset.kind === 'date' ? fmtDate(v) : v || ''; }; + const setupFields = (root = document) => { + root.querySelectorAll('input[data-kind]').forEach((el) => { + el.placeholder ||= el.dataset.kind === 'date' ? 'dd/mm/yyyy' : 'hh:mm'; + el.size = el.dataset.kind === 'date' ? 10 : 5; + el.addEventListener('blur', () => { + const v = getField(el); + el.classList.toggle('invalid', !!el.value.trim() && !v); + if (v) setField(el, v); + }); + }); + }; + return { pad, toMin, fmtTime, fmtDur, parseHours, parseDate, isoDate, addDays, weekday, weekdayName, WEEKDAYS, monday, weeksBetween, kimaiDateTime, uid, + fmtDate, parseUserDate, fmtDateTime, parseUserTime, getField, setField, setupFields, }; })(); diff --git a/popup/popup.html b/popup/popup.html index dc76216..92e27d1 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -14,8 +14,8 @@

Abacus → Kimai

- - + +
diff --git a/popup/popup.js b/popup/popup.js index 12bfaf7..136ea89 100644 --- a/popup/popup.js +++ b/popup/popup.js @@ -45,7 +45,7 @@ async function extract() { return; } const total = merged.reduce((a, d) => a + d.minutes, 0); - $status.textContent = `Found ${merged.length} day(s), ${U.fmtDur(total)} total\n${merged[0].date} → ${merged[merged.length - 1].date}`; + $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(); } @@ -63,19 +63,34 @@ function showCrawl(c) { } async function crawl() { - const from = $from.value, to = $to.value; + // 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 }; - await browser.scripting.executeScript({ target, files: ['lib/util.js', 'lib/parse.js', 'lib/abacus-crawl.js'] }); + $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, - func: (f, t) => AbacusCrawl.run(f, t), - args: [from, to], + files: ['/lib/util.js', '/lib/parse.js', '/lib/abacus-crawl.js', '/lib/abacus-crawl-start.js'], }); - const res = results.map((r) => r.result).filter(Boolean); - if (res.every((r) => r.skipped)) throw new Error('No "Gestion des temps" card found on this tab'); + 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(); } @@ -91,8 +106,9 @@ browser.storage.onChanged.addListener((ch) => { if (ch.crawl) showCrawl(ch.crawl (async () => { const { crawlRange, crawl: last } = await browser.storage.local.get(['crawlRange', 'crawl']); const today = U.isoDate(new Date()); - $from.value = crawlRange?.from || U.monday(today); - $to.value = crawlRange?.to || today; + 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); })();