Added automation

This commit is contained in:
2026-09-20 14:38:17 +02:00
parent daa4bbd028
commit c9c524ff2a
13 changed files with 489 additions and 28 deletions

View File

@@ -8,8 +8,6 @@
import {
cleanupOldDays,
closeAndStart,
closeDay,
createDay,
deleteEntry,
getActiveDay,
@@ -18,10 +16,12 @@ import {
importBackup,
patchActiveDay,
patchEntry,
readSettings,
reopenDay,
setEntryStart,
} from './db.js';
import { buildModel, suggestClients, suggestTasks, TRANSPORT_TASK } from './predict.js';
import { automateDay, manualAdvance, previousContext } from './automation.js';
import { loadSettings, settings } from './settings.svelte.js';
import { atLocalTime, HOUR, localDateKey, roundTs, weekday } from './time.js';
@@ -36,6 +36,7 @@ export const dayState = $state({
closedDays: [],
historyEntries: [],
todayRecord: null,
previousWork: null,
/** Prediction model, rebuilt whenever history changes. */
model: null,
/** True once the configured end time has passed and the worker has not answered. */
@@ -70,12 +71,15 @@ export function reconcile(now = Date.now()) {
}
async function reconcileDay(now) {
// A settings change in another tab must also stop this tab's clock actions.
const persisted = await readSettings();
if (persisted) Object.assign(settings, persisted);
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);
await automateDay(active.date, settings, now, { stale: true });
return reconcileDay(now);
}
@@ -94,15 +98,18 @@ async function reconcileDay(now) {
// already ended must not spring back to life.
if (!active) {
const existing = await getDay(dayState.today);
if (!existing && settings.workdays.includes(weekday(dayState.today))) {
if (!existing && settings.autoStartDay && settings.workdays.includes(weekday(dayState.today))) {
const plannedStart = atLocalTime(dayState.today, settings.dayStart);
if (now >= plannedStart) {
await openDay(dayState.today, plannedStart);
await automateDay(dayState.today, settings, now);
return refresh(now);
}
}
}
if (active) await automateDay(active.date, settings, now);
await refresh(now);
}
@@ -110,14 +117,16 @@ async function reconcileDay(now) {
* 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) {
async function openDay(dateKey, startTs, manual = false) {
const { entries, days } = await getHistory();
const model = buildModel(entries, days);
await createDay({
date: dateKey,
status: 'active',
plannedStartTs: atLocalTime(dateKey, settings.dayStart),
plannedEndTs: atLocalTime(dateKey, settings.dayEnd),
plannedEndTs: manual && settings.autoEndDay
? Math.max(atLocalTime(dateKey, settings.dayEnd), startTs + HOUR)
: atLocalTime(dateKey, settings.dayEnd),
startedTs: startTs,
endedTs: null,
lunchType: settings.lunchType,
@@ -127,6 +136,7 @@ async function openDay(dateKey, startTs) {
kind: 'travel',
task: TRANSPORT_TASK,
client: model.lastClient,
automatic: true,
});
}
@@ -142,6 +152,7 @@ export async function refresh(now = Date.now()) {
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.previousWork = previousContext(days, entries, active?.date ?? dayState.today).previousWork;
dayState.day = active ?? null;
dayState.entries = entriesFor(active);
dayState.lastClosed = lastClosed ?? null;
@@ -161,7 +172,7 @@ export async function refresh(now = Date.now()) {
/** Manual start, for a Saturday or an early departure. */
export async function startDayNow(now = Date.now()) {
if (dayState.importing) return;
await openDay(localDateKey(now), now);
await openDay(localDateKey(now), now, true);
await refresh(now);
}
@@ -174,17 +185,13 @@ export async function startDayNow(now = Date.now()) {
*/
export async function advance({ kind = 'work', task, client }, now = Date.now()) {
if (!dayState.day || dayState.importing) return;
await closeAndStart(dayState.day.date, now, { kind, task, client: client ?? null }, running()?.id ?? null);
await manualAdvance(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 || dayState.importing || running()?.kind === 'lunch') return;
await closeAndStart(dayState.day.date, now, {
kind: 'lunch',
task: 'Pause déjeuner',
client: null,
}, running()?.id ?? null);
await automateDay(dayState.day.date, settings, now, { lunch: true, expectedId: running()?.id ?? null });
await refresh(now);
}
@@ -193,6 +200,7 @@ export async function setLunch({ lunchType, restaurant }) {
await patchActiveDay(dayState.day.date, () => ({
lunchType,
restaurant: lunchType === 'restaurant' ? (restaurant ?? null) : null,
lunchManual: true,
}));
await refresh();
}
@@ -249,7 +257,7 @@ export async function applyImport(payload, mode) {
export async function endDay(now = Date.now()) {
if (!dayState.day || dayState.importing) return;
const date = dayState.day.date;
await closeDay(date, now);
await automateDay(date, settings, now, { end: true });
await refresh(now);
}
@@ -293,7 +301,9 @@ export function predictNext(now = Date.now()) {
const clients = suggestClients(model, now);
return {
task: tasks[0]?.value ?? TRANSPORT_TASK,
task: settings.autoPreviousTask && current?.kind === 'travel' && current.seq === 0
? dayState.previousWork?.task ?? 'Travaux sur site'
: tasks[0]?.value ?? TRANSPORT_TASK,
client: current?.client ?? clients[0]?.value ?? null,
tasks,
clients,