first version

This commit is contained in:
2026-10-07 15:53:12 +02:00
commit 9d8c2a0488
14 changed files with 1481 additions and 0 deletions
+128
View File
@@ -0,0 +1,128 @@
// 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: 'vaadin-button[movie-id="leftButton"]',
right: 'vaadin-button[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',
};
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);
function readDate() {
const nav = document.querySelector(SEL.nav);
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.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 intervals = [], open = [];
for (const tf of document.querySelectorAll(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() || '';
return { date: readDate(), intervals: AbacusParse.union(intervals), open, total: U.toMin(totalText) };
}
// 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}`);
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();
document.querySelector(dir < 0 ? SEL.left : SEL.right).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 `${d.date} ${iv}${note}`;
if (d.total) return `${d.date} ${U.fmtDur(d.total)}${note}`;
return `# ${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
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 ${from} (at ${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 ${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 ${from} and ${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 };
})();