Fixed a few minor bugs

This commit is contained in:
2026-09-19 19:58:13 +02:00
parent 870457955b
commit 9bb9260c09
11 changed files with 520 additions and 135 deletions

View File

@@ -9,16 +9,16 @@
import {
cleanupOldDays,
closeAndStart,
closeDay,
createDay,
deleteEntry,
getActiveDay,
getDay,
getEntries,
getHistory,
getLastClosedDay,
importBackup,
patchDay,
patchActiveDay,
patchEntry,
putDay,
reopenDay,
setEntryStart,
} from './db.js';
import { buildModel, suggestClients, suggestTasks, TRANSPORT_TASK } from './predict.js';
@@ -33,6 +33,9 @@ export const dayState = $state({
/** Last closed day + its entries — what the resume screen renders. */
lastClosed: null,
lastClosedEntries: [],
closedDays: [],
historyEntries: [],
todayRecord: null,
/** Prediction model, rebuilt whenever history changes. */
model: null,
/** True once the configured end time has passed and the worker has not answered. */
@@ -56,6 +59,7 @@ const pendingReconciliations = new Set();
*/
export function reconcile(now = Date.now()) {
if (dayState.importing) return Promise.resolve();
if (pendingReconciliations.size) return [...pendingReconciliations][0];
const pending = reconcileDay(now);
pendingReconciliations.add(pending);
pending.then(
@@ -68,25 +72,24 @@ export function reconcile(now = Date.now()) {
async function reconcileDay(now) {
dayState.today = localDateKey(now);
const active = await getActiveDay();
// Settle stale work before retention, so an open day cannot vanish on return.
if (active && active.date < dayState.today) {
await closeDay(active.date, now, true);
return reconcileDay(now);
}
// Applying retention here is what makes the setting mean anything; leaving it to
// the manual button in settings meant it effectively never ran.
if (cleanedUpFor !== dayState.today) {
cleanedUpFor = dayState.today;
try {
await cleanupOldDays(settings.retentionDays);
await cleanupOldDays(settings.retentionDays, now);
} catch (error) {
console.error('[timelog] retention cleanup failed', error);
}
}
const active = await getActiveDay();
// A day left open from an earlier date never stays open.
if (active && active.date < dayState.today) {
await forceClose(active);
return reconcile(now);
}
// Auto-start. Only when no record exists for today at all: a day the worker has
// already ended must not spring back to life.
if (!active) {
@@ -103,21 +106,14 @@ async function reconcileDay(now) {
await refresh(now);
}
/** Close a stale day at the earlier of its planned end and its last recorded activity. */
async function forceClose(day) {
const entries = await getEntries(day.date);
const lastEnd = entries.reduce((max, e) => Math.max(max, e.endTs ?? e.startTs), 0);
const endedTs = Math.min(day.plannedEndTs, lastEnd || day.plannedEndTs);
await closeAndStart(day.date, endedTs, null);
await patchDay(day.date, { status: 'closed', endedTs });
}
/**
* Create a day and immediately open the transport task: per the workflow, the drive
* out from base *is* the first task, and "Arrivé sur site" is what ends it.
*/
async function openDay(dateKey, startTs) {
await putDay({
const { entries, days } = await getHistory();
const model = buildModel(entries, days);
await createDay({
date: dateKey,
status: 'active',
plannedStartTs: atLocalTime(dateKey, settings.dayStart),
@@ -127,11 +123,7 @@ async function openDay(dateKey, startTs) {
lunchType: settings.lunchType,
restaurant: null,
closePromptedTs: null,
});
const { entries, days } = await getHistory();
const model = buildModel(entries, days);
await closeAndStart(dateKey, startTs, {
}, {
kind: 'travel',
task: TRANSPORT_TASK,
client: model.lastClient,
@@ -139,16 +131,23 @@ async function openDay(dateKey, startTs) {
}
/** Reload everything from the database and rebuild the model. */
let refreshVersion = 0;
export async function refresh(now = Date.now()) {
const active = await getActiveDay();
dayState.day = active ?? null;
dayState.entries = active ? await getEntries(active.date) : [];
const lastClosed = await getLastClosedDay();
dayState.lastClosed = lastClosed ?? null;
dayState.lastClosedEntries = lastClosed ? await getEntries(lastClosed.date) : [];
const version = ++refreshVersion;
const { entries, days } = await getHistory();
if (version !== refreshVersion) return;
const active = days.find((d) => d.status === 'active');
const closed = days.filter((d) => d.status === 'closed').sort((a, b) => b.date.localeCompare(a.date));
const lastClosed = closed[0];
const entriesFor = (day) => day ? entries.filter((e) => e.date === day.date).sort((a, b) => a.seq - b.seq) : [];
dayState.today = localDateKey(now);
dayState.todayRecord = days.find((d) => d.date === dayState.today) ?? null;
dayState.day = active ?? null;
dayState.entries = entriesFor(active);
dayState.lastClosed = lastClosed ?? null;
dayState.lastClosedEntries = entriesFor(lastClosed);
dayState.closedDays = closed;
dayState.historyEntries = entries;
dayState.model = buildModel(entries, days);
dayState.needsClosePrompt = Boolean(
@@ -161,7 +160,8 @@ export async function refresh(now = Date.now()) {
/** Manual start, for a Saturday or an early departure. */
export async function startDayNow(now = Date.now()) {
await openDay(dayState.today, now);
if (dayState.importing) return;
await openDay(localDateKey(now), now);
await refresh(now);
}
@@ -173,27 +173,27 @@ export async function startDayNow(now = Date.now()) {
* @param {number} [now]
*/
export async function advance({ kind = 'work', task, client }, now = Date.now()) {
if (!dayState.day) return;
await closeAndStart(dayState.day.date, now, { kind, task, client: client ?? null });
if (!dayState.day || dayState.importing) return;
await closeAndStart(dayState.day.date, now, { kind, task, client: client ?? null }, running()?.id ?? null);
await refresh(now);
}
export async function startLunch(now = Date.now()) {
if (!dayState.day) return;
if (!dayState.day || dayState.importing || running()?.kind === 'lunch') return;
await closeAndStart(dayState.day.date, now, {
kind: 'lunch',
task: 'Pause déjeuner',
client: null,
});
}, running()?.id ?? null);
await refresh(now);
}
export async function setLunch({ lunchType, restaurant }) {
if (!dayState.day) return;
await patchDay(dayState.day.date, {
if (!dayState.day || dayState.importing) return;
await patchActiveDay(dayState.day.date, () => ({
lunchType,
restaurant: lunchType === 'restaurant' ? (restaurant ?? null) : null,
});
}));
await refresh();
}
@@ -247,46 +247,27 @@ export async function applyImport(payload, mode) {
}
export async function endDay(now = Date.now()) {
if (!dayState.day) return;
if (!dayState.day || dayState.importing) return;
const date = dayState.day.date;
await closeAndStart(date, now, null);
await patchDay(date, { status: 'closed', endedTs: now });
await closeDay(date, now);
await refresh(now);
}
/** "Toujours en cours" — push the end out an hour so the prompt returns later. */
export async function keepWorking(now = Date.now()) {
if (!dayState.day) return;
await patchDay(dayState.day.date, {
plannedEndTs: Math.max(dayState.day.plannedEndTs, now) + HOUR,
if (!dayState.day || dayState.importing) return;
const expectedEnd = dayState.day.plannedEndTs;
await patchActiveDay(dayState.day.date, (day) => day.plannedEndTs !== expectedEnd ? null : ({
plannedEndTs: Math.max(day.plannedEndTs, now) + HOUR,
closePromptedTs: now,
});
}));
await refresh(now);
}
/** Undo a premature end-of-day: reopens today and starts a fresh task from now. */
/** Undo a premature end-of-day, continuing the last task without a gap. */
export async function reopenToday(now = Date.now()) {
const day = await getDay(dayState.today);
if (!day || day.status !== 'closed') return;
await patchDay(dayState.today, {
status: 'active',
endedTs: null,
plannedEndTs: Math.max(day.plannedEndTs, now + HOUR),
});
const { entries, days } = await getHistory();
const model = buildModel(entries, days);
const [best] = suggestTasks(model, {
prevTask: null,
prevElapsedMs: 0,
seq: (await getEntries(dayState.today)).length,
ts: now,
client: model.lastClient,
});
await closeAndStart(dayState.today, now, {
kind: 'work',
task: best?.value ?? TRANSPORT_TASK,
client: model.lastClient,
});
if (dayState.importing) return;
await reopenDay(localDateKey(now), now);
await refresh(now);
}