179 lines
7.1 KiB
JavaScript
179 lines
7.1 KiB
JavaScript
// 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 <input slot="input"> 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 };
|
||
})();
|