Files
abacus-to-kimai-filler/lib/abacus-crawl.js
T

179 lines
7.1 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: '[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 };
})();