From f458a83599b32d1fad5829814aea9659e854821c Mon Sep 17 00:00:00 2001 From: Nessar Date: Mon, 19 Oct 2026 09:52:03 +0200 Subject: [PATCH 1/3] fix(calendar): mark today on the Europe/Paris date The calendar highlighted today from the browser's local date while the default planning horizon starts on the Europe/Paris date, so the two disagreed for a few hours a day (and always, for users in another time zone). The horizon's first day was then not marked as today. This also failed the CI test run in UTC containers between 22:00 and 24:00 UTC. Co-Authored-By: Claude Sonnet 5.5 --- src/app/services/calendar-service.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/services/calendar-service.ts b/src/app/services/calendar-service.ts index bedccdc..67ed933 100644 --- a/src/app/services/calendar-service.ts +++ b/src/app/services/calendar-service.ts @@ -85,11 +85,9 @@ export class CalendarService { const holidays = new Set( this.getHolidays(first.getFullYear(), last.getFullYear()).map((h) => h.getTime()), ); - const { - date: todayDate, - month: todayMonth, - year: todayYear, - } = this.dateCache.getTodayComponents(); + const [todayYear, todayMonth, todayDate] = planningDateInParis(new Date()) + .split('-') + .map(Number); const months: CalendarProjection['months'] = []; const ranges: SelectedDate[] = []; const cursor = new Date(first); @@ -124,7 +122,7 @@ export class CalendarService { ); if ( day.getDate() === todayDate && - day.getMonth() === todayMonth && + day.getMonth() + 1 === todayMonth && day.getFullYear() === todayYear ) ranges.push(new SelectedDate(DayType.TODAY, new DateRange(day, day))); -- 2.54.0 From af20c96f01ec43b6a9050845bba72d502c057754 Mon Sep 17 00:00:00 2001 From: Nessar Date: Mon, 19 Oct 2026 09:52:03 +0200 Subject: [PATCH 2/3] test(planning): hold the worker until the preparation cancel arrives The worker reads its queue only between event-loop turns, so the calendar preparation could finish before the cancel sent on its second progress update arrived, and the test received a complete plan. Use the real-worker harness hold already applied to the checkpoint cancellation test, and cancel on the first preparation update so the hold is released. Co-Authored-By: Claude Sonnet 5.5 --- src/app/services/leave-planner.worker.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/services/leave-planner.worker.spec.ts b/src/app/services/leave-planner.worker.spec.ts index 61a2423..36f1e81 100644 --- a/src/app/services/leave-planner.worker.spec.ts +++ b/src/app/services/leave-planner.worker.spec.ts @@ -587,8 +587,8 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data })); ); it('stops during calendar preparation without a plan or an obligation conflict', async () => { + holdForCancelAtStage = 'preparation'; const stages: string[] = []; - let preparationUpdates = 0; const calculation = createPlanningCalculation({ horizon: { start: '2024-01-01', end: '2027-01-01' }, balances: [], @@ -596,7 +596,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data })); declaredDays: [{ date: '2024-01-02', type: 'other' }], progress: (stage) => { stages.push(stage); - if (stage === 'preparation' && ++preparationUpdates === 2) calculation.cancel(); + if (stage === 'preparation') calculation.cancel(); }, }); const outcome = await calculation.outcome; @@ -606,7 +606,7 @@ parentPort.on('message', (data) => globalThis.self.onmessage({ data })); }); if (outcome.kind !== 'PlanningIncomplete') return; expect(outcome.incumbent).toBeUndefined(); - expect(preparationUpdates).toBeGreaterThanOrEqual(2); + expect(stages).toContain('preparation'); expect(stages).not.toContain('obligations'); expect(await calculation.explanationError).toBeUndefined(); expect(heartbeats).toBeGreaterThan(0); -- 2.54.0 From 036b13613cf14fcb7d92de76232cc7bbde75c1bf Mon Sep 17 00:00:00 2001 From: Nessar Date: Mon, 19 Oct 2026 10:21:54 +0200 Subject: [PATCH 3/3] test(calendar): pin the clock for the Paris today marker The test of the today marker read the wall clock, so it only exercised the Paris/local date mismatch between 22:00 and 24:00 UTC and passed with or without the fix the rest of the day. Pin the clock to 22:16 UTC, already the next day in Paris, so a UTC runner fails without the fix at any time. Refs #301 Co-Authored-By: Claude Sonnet 5.5 --- src/app/services/calendar-service.spec.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/app/services/calendar-service.spec.ts b/src/app/services/calendar-service.spec.ts index ec749a9..912c641 100644 --- a/src/app/services/calendar-service.spec.ts +++ b/src/app/services/calendar-service.spec.ts @@ -8,6 +8,7 @@ import { SelectedDates, } from './calendar-service'; import { DateRange } from '@angular/material/datepicker'; +import { vi } from '../../testing/vitest-compat'; describe('CalendarService', () => { let service: CalendarService; @@ -197,6 +198,8 @@ describe('CalendarService', () => { service.project({ placements: [], horizon }); const day = (key: string) => new Date(`${key}T00:00:00`); + afterEach(() => vi.useRealTimers()); + it('projects weekends, holidays and today for the default planning horizon', () => { const { selections } = project({ start: '2026-01-01', end: '2027-01-01' }); expect(selections.dayTypes(day('2026-05-09'))).toContain(DayType.WEEKEND); @@ -207,6 +210,15 @@ describe('CalendarService', () => { ); }); + it('marks today on the Europe/Paris date when the runtime date is still the day before', () => { + vi.setSystemTime(new Date('2026-10-17T22:16:00Z')); + const horizon = service.getDefaultPlanningHorizon(); + expect(horizon.start).toBe('2026-10-18'); + const { selections } = project(horizon); + expect(selections.getClasses(day('2026-10-18'))).toContain(DayType.TODAY); + expect(selections.getClasses(day('2026-10-17'))).not.toContain(DayType.TODAY); + }); + it('lists the covered months, including partial boundary months and year changes', () => { expect(project({ start: '2024-11-20', end: '2025-02-02' }).months).toEqual([ { monthIndex: 10, year: 2024 }, -- 2.54.0