// Injected into the MyAbacus page (content script world, needs util.js + parse.js). // Walks the "Gestion des temps" card day by day with the ‹ › buttons and stores results // in browser.storage.local as it goes, so the popup can be closed meanwhile. var AbacusCrawl = (() => { const SEL = { left: '[movie-id="leftButton"]', right: '[movie-id="rightButton"]', nav: '.proj-date-navigation', 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)); // 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 = 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 || ''); return m ? `${m[3]}-${m[2].padStart(2, '0')}-${m[1].padStart(2, '0')}` : null; } // Native inside the vaadin-text-field (custom element props aren't visible from here) 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(qa(SEL.to).map((tf) => [suffix(tf), fieldValue(tf)])); const intervals = [], open = []; 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 = 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 ${U.fmtDate(prevDate)}`); await sleep(100); } let last = '', stable = 0; while (stable < 3 && Date.now() - t0 < timeout) { await sleep(150); const snap = JSON.stringify(readDay()); stable = snap === last ? stable + 1 : 0; last = snap; } } async function step(dir) { const prev = readDate(); const btn = dir < 0 ? (q(SEL.left) || deepFind('.fa-angle-left')?.closest('vaadin-button, button')) : findRightButton(); btn.click(); await waitLoaded(prev); } function toLines(days) { return days.map((d) => { const iv = d.intervals.map((i) => `${U.fmtTime(i.start)}-${U.fmtTime(i.end)}`).join(' '); const sum = AbacusParse.sumIntervals(d.intervals); const notes = []; 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 `${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, ...diagnose() }; // other frame window.__abacusCrawling = true; const progress = (p) => browser.storage.local.set({ crawl: { ...p, at: new Date().toISOString() } }); const days = []; try { await browser.storage.local.remove('crawlStop'); let cur = readDate(); if (!cur) throw new Error('Cannot read the current date in the card'); const maxSteps = 400; let n = 0; // Move to `from` (two one-way loops: never oscillates if a day can't be shown) 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 ${U.fmtDate(from)} (at ${U.fmtDate(cur)})` }); await step(dir); cur = readDate(); } } // Read each day up to `to` for (;;) { const { crawlStop } = await browser.storage.local.get('crawlStop'); if (crawlStop) throw new Error('Stopped'); days.push(readDay()); await progress({ status: 'running', message: `Read ${U.fmtDate(cur)} (${days.length} day(s))` }); if (cur >= to || ++n > maxSteps) break; await step(1); cur = readDate(); } const text = toLines(days); const parsed = AbacusParse.fromText(text).days; await browser.storage.local.set({ 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 ${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}` }); await progress({ status: 'error', message: e.message }); return { error: e.message }; } finally { window.__abacusCrawling = false; } } return { run, readDay, readDate, present, diagnose }; })();