fix crawling + correct date/time format
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
// Last file of the injection (same call as util/parse/abacus-crawl, so globals are shared).
|
||||
// Range comes from storage since files can't take arguments. The returned promise is the injection result.
|
||||
browser.storage.local.get('crawlRequest').then(({ crawlRequest: r }) => AbacusCrawl.run(r.from, r.to));
|
||||
+71
-21
@@ -3,20 +3,58 @@
|
||||
// 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"]',
|
||||
left: '[movie-id="leftButton"]',
|
||||
right: '[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',
|
||||
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));
|
||||
|
||||
const present = () => !!document.querySelector(SEL.right);
|
||||
// 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 = document.querySelector(SEL.nav);
|
||||
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 || '');
|
||||
@@ -24,27 +62,38 @@ var AbacusCrawl = (() => {
|
||||
}
|
||||
|
||||
// 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 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([...document.querySelectorAll(SEL.to)].map((tf) => [suffix(tf), fieldValue(tf)]));
|
||||
const tos = new Map(qa(SEL.to).map((tf) => [suffix(tf), fieldValue(tf)]));
|
||||
const intervals = [], open = [];
|
||||
for (const tf of document.querySelectorAll(SEL.from)) {
|
||||
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 = document.querySelector(SEL.total)?.textContent.trim() || '';
|
||||
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 ${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;
|
||||
@@ -58,7 +107,8 @@ var AbacusCrawl = (() => {
|
||||
|
||||
async function step(dir) {
|
||||
const prev = readDate();
|
||||
document.querySelector(dir < 0 ? SEL.left : SEL.right).click();
|
||||
const btn = dir < 0 ? (q(SEL.left) || deepFind('.fa-angle-left')?.closest('vaadin-button, button')) : findRightButton();
|
||||
btn.click();
|
||||
await waitLoaded(prev);
|
||||
}
|
||||
|
||||
@@ -70,15 +120,15 @@ var AbacusCrawl = (() => {
|
||||
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}`;
|
||||
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 }; // other frame
|
||||
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 = [];
|
||||
@@ -92,7 +142,7 @@ var AbacusCrawl = (() => {
|
||||
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 progress({ status: 'running', message: `Navigating to ${U.fmtDate(from)} (at ${U.fmtDate(cur)})` });
|
||||
await step(dir);
|
||||
cur = readDate();
|
||||
}
|
||||
@@ -102,7 +152,7 @@ var AbacusCrawl = (() => {
|
||||
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))` });
|
||||
await progress({ status: 'running', message: `Read ${U.fmtDate(cur)} (${days.length} day(s))` });
|
||||
if (cur >= to || ++n > maxSteps) break;
|
||||
await step(1);
|
||||
cur = readDate();
|
||||
@@ -113,7 +163,7 @@ var AbacusCrawl = (() => {
|
||||
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}` });
|
||||
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}` });
|
||||
@@ -124,5 +174,5 @@ var AbacusCrawl = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
return { run, readDay, readDate, present };
|
||||
return { run, readDay, readDate, present, diagnose };
|
||||
})();
|
||||
|
||||
+8
-6
@@ -122,13 +122,13 @@ var AbacusParse = (() => {
|
||||
}
|
||||
|
||||
// Normalized one-line-per-day format used in the dashboard editor:
|
||||
// 2026-10-07 08:00-12:00 13:00-17:24
|
||||
// 2026-10-08 8:24
|
||||
// 07/10/2026 08:00-12:00 13:00-17:24
|
||||
// 08/10/2026 8:24
|
||||
function toText(days) {
|
||||
return days
|
||||
.map((d) => d.intervals.length
|
||||
? `${d.date} ${d.intervals.map((iv) => `${U.fmtTime(iv.start)}-${U.fmtTime(iv.end)}`).join(' ')}`
|
||||
: `${d.date} ${U.fmtDur(d.minutes)}`)
|
||||
? `${U.fmtDate(d.date)} ${d.intervals.map((iv) => `${U.fmtTime(iv.start)}-${U.fmtTime(iv.end)}`).join(' ')}`
|
||||
: `${U.fmtDate(d.date)} ${U.fmtDur(d.minutes)}`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
@@ -137,8 +137,10 @@ var AbacusParse = (() => {
|
||||
text.split('\n').forEach((line, n) => {
|
||||
line = line.replace(/#.*/, '').trim();
|
||||
if (!line) return;
|
||||
const m = /^(\d{4}-\d{2}-\d{2})\s+(.+)$/.exec(line);
|
||||
if (!m) return errors.push(`line ${n + 1}: expected "YYYY-MM-DD ..."`);
|
||||
const m = /^(\S+)\s+(.+)$/.exec(line);
|
||||
const date = m && U.parseUserDate(m[1]);
|
||||
if (!date) return errors.push(`line ${n + 1}: expected "dd/mm/yyyy ..."`);
|
||||
m[1] = date;
|
||||
const ivs = ranges(m[2]);
|
||||
if (ivs.length) return days.push({ date: m[1], intervals: union(ivs), minutes: sumIntervals(union(ivs)) });
|
||||
const min = U.parseHours(m[2]);
|
||||
|
||||
+43
@@ -57,8 +57,51 @@ var U = (() => {
|
||||
|
||||
const uid = () => Math.random().toString(36).slice(2, 10);
|
||||
|
||||
/* Swiss display: 01/09/2026, 13:30. Internally always ISO dates + HH:MM. */
|
||||
const fmtDate = (iso) => (iso ? `${iso.slice(8, 10)}/${iso.slice(5, 7)}/${iso.slice(0, 4)}` : '');
|
||||
|
||||
// "1/9/2026", "01.09.2026", "01-09-26", "2026-09-01" -> "2026-09-01" (null if invalid)
|
||||
const parseUserDate = (s) => {
|
||||
s = String(s || '').trim();
|
||||
let m, y, mo, d;
|
||||
if ((m = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec(s))) [, y, mo, d] = m;
|
||||
else if ((m = /^(\d{1,2})[./-](\d{1,2})[./-](\d{4}|\d{2})$/.exec(s))) [, d, mo, y] = m;
|
||||
else return null;
|
||||
if (y.length === 2) y = '20' + y;
|
||||
const dt = new Date(+y, +mo - 1, +d);
|
||||
return dt.getMonth() === +mo - 1 && dt.getDate() === +d ? isoDate(dt) : null;
|
||||
};
|
||||
|
||||
const fmtDateTime = (dt) => `${fmtDate(isoDate(dt))} ${fmtTime(dt.getHours() * 60 + dt.getMinutes())}`;
|
||||
|
||||
// "930", "9:30", "9.30", "9h30", "9" -> "09:30" (null if invalid)
|
||||
const parseUserTime = (s) => {
|
||||
s = String(s || '').trim();
|
||||
let m;
|
||||
if ((m = /^(\d{1,2})$/.exec(s))) m = [s, m[1], '0'];
|
||||
else if (!(m = /^(\d{1,2})[:.h]?(\d{2})$/.exec(s))) return null;
|
||||
const min = +m[1] * 60 + +m[2];
|
||||
return +m[1] < 24 && +m[2] < 60 ? fmtTime(min) : null;
|
||||
};
|
||||
|
||||
// Text inputs with data-kind="date" | "time": read/write ISO / HH:MM, show Swiss format
|
||||
const getField = (el) => (el.dataset.kind === 'date' ? parseUserDate(el.value) : parseUserTime(el.value)) || '';
|
||||
const setField = (el, v) => { el.value = el.dataset.kind === 'date' ? fmtDate(v) : v || ''; };
|
||||
const setupFields = (root = document) => {
|
||||
root.querySelectorAll('input[data-kind]').forEach((el) => {
|
||||
el.placeholder ||= el.dataset.kind === 'date' ? 'dd/mm/yyyy' : 'hh:mm';
|
||||
el.size = el.dataset.kind === 'date' ? 10 : 5;
|
||||
el.addEventListener('blur', () => {
|
||||
const v = getField(el);
|
||||
el.classList.toggle('invalid', !!el.value.trim() && !v);
|
||||
if (v) setField(el, v);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
pad, toMin, fmtTime, fmtDur, parseHours, parseDate, isoDate, addDays,
|
||||
weekday, weekdayName, WEEKDAYS, monday, weeksBetween, kimaiDateTime, uid,
|
||||
fmtDate, parseUserDate, fmtDateTime, parseUserTime, getField, setField, setupFields,
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user