Files
abacus-to-kimai-filler/lib/abacus-crawl.js
T
2026-10-07 15:53:12 +02:00

129 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 };
})();