fix(zeit): über Mitternacht laufende Timer bleiben sichtbar (ENTWICKLUN-14)
ci / quality (push) Failing after 40s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 53s

Ein um 23:00 gestarteter, noch laufender Timer hatte Start-Datum = gestern und
fiel nach Mitternacht aus listToday/teamToday (Filter nach Start-Datum=heute) —
nur die TimerPill zeigte ihn noch.

- listToday/teamToday: Filter „overlapsToday" = heute gestartet ODER heute
  beendet (Mitternacht überschritten) ODER laufend → Eintrag bleibt sichtbar.
- Tages-Summen clampen den Anteil auf den heutigen Tag (berlinDayStartMs,
  DST-korrekt) → über Mitternacht gelaufene Blöcke werden nicht falsch zugezählt.
- ToDo-Titel werden tagesübergreifend serverseitig aufgelöst (time.now +
  listToday) → Task bleibt benannt, auch wenn das ToDo von gestern ist.
- Timeline zeigt das Start-Datum, wenn ein Eintrag nicht heute begonnen hat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-06 17:29:47 +02:00
1 parent 040dd62735
commit 8fd72bcb69
5 files changed
+107 -34

No files matched your search

+15 -1
View File
@@ -1,5 +1,12 @@
import { describe, it, expect } from 'vitest';
import { formatIsoDate, isWerktag, nextWerktag, previousWerktag, werktageBetween } from './werktag';
import {
formatIsoDate,
isWerktag,
nextWerktag,
previousWerktag,
werktageBetween,
berlinDayStartMs,
} from './werktag';
describe('werktag', () => {
it('formatIsoDate liefert YYYY-MM-DD in Berlin TZ', () => {
@@ -27,4 +34,11 @@ describe('werktag', () => {
// Mo 2026-06-08 bis Mo 2026-06-15 (exkl.): 5 Werktage
expect(werktageBetween('2026-06-08', '2026-06-15')).toBe(5);
});
it('berlinDayStartMs ist DST-korrekt', () => {
// Sommer (CEST, +2): 00:00 Berlin = 22:00 UTC am Vortag
expect(berlinDayStartMs('2026-06-06')).toBe(Date.parse('2026-06-05T22:00:00Z'));
// Winter (CET, +1): 00:00 Berlin = 23:00 UTC am Vortag
expect(berlinDayStartMs('2026-01-15')).toBe(Date.parse('2026-01-14T23:00:00Z'));
});
});
+23
View File
@@ -22,6 +22,29 @@ export function formatIsoDate(d: Date): string {
return fmt.format(d); // en-CA liefert ISO YYYY-MM-DD
}
/**
* Epoch-ms von 00:00 Europe/Berlin am gegebenen Tag (DST-korrekt). Genutzt, um
* Tagesgrenzen für Zeit-Auswertungen zu bilden (z. B. Tagesanteil eines über
* Mitternacht laufenden Timers).
*/
export function berlinDayStartMs(iso: string): number {
const utcMidnight = Date.parse(`${iso}T00:00:00Z`);
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: TIMEZONE,
hourCycle: 'h23',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
}).formatToParts(new Date(utcMidnight));
const get = (t: string) => Number(parts.find((p) => p.type === t)?.value ?? '0');
const asIfUtc = Date.UTC(get('year'), get('month') - 1, get('day'), get('hour'), get('minute'), get('second'));
const offset = asIfUtc - utcMidnight; // ms, die Berlin der UTC voraus ist
return utcMidnight - offset;
}
export function parseIsoDate(s: string): Date {
if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
throw new Error(`Ungültiges ISO-Datum: ${s}`);