fix(zeit): Gesamt-Zeit als Wall-Clock statt Summe paralleler Tasks
ci / quality (push) Failing after 1m8s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 1m55s

"Zeit im Team heute" (und "Zeit heute") summierten alle Task-Dauern —
wer 5 Tasks parallel trackt, hatte 10h statt 2h Anwesenheit. Gesamt wird
jetzt als Vereinigung überlappender Intervalle berechnet (unionSeconds).
Pro-Aufgabe/Kategorie bleibt separat als Summe erhalten und wird als
"getrackt X" angezeigt; der Verteilungsbalken nutzt diese Summe als Nenner.

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 15:06:27 +02:00
1 parent 43eeabaea2
commit b7b54fcec5
2 files changed
+61 -10

No files matched your search

+51 -9
View File
@@ -8,6 +8,31 @@ import { todayIso } from '../lib/werktag.js';
const idInput = z.object({ id: z.string().uuid() });
/**
* Wall-Clock-Sekunden: Vereinigung überlappender Zeitintervalle. Parallel
* laufende Einträge (mehrere Tasks gleichzeitig) zählen nur einmal — damit
* „Gesamt heute" die tatsächlich anwesende Zeit zeigt, nicht die Summe.
*/
function unionSeconds(intervals: Array<readonly [number, number]>): number {
const valid = intervals.filter(([s, e]) => e > s).sort((a, b) => a[0] - b[0]);
if (valid.length === 0) return 0;
let total = 0;
let curStart = valid[0]![0];
let curEnd = valid[0]![1];
for (let i = 1; i < valid.length; i++) {
const [s, e] = valid[i]!;
if (s <= curEnd) {
if (e > curEnd) curEnd = e;
} else {
total += curEnd - curStart;
curStart = s;
curEnd = e;
}
}
total += curEnd - curStart;
return Math.round(total / 1000);
}
async function stopCurrent(
tx: import('@cockpit/db').Db,
userId: string,
@@ -117,12 +142,19 @@ export const timeRouter = router({
),
)
.orderBy(desc(timeEntry.startedAt));
const totalSeconds = rows.reduce((acc, r) => {
if (r.endedAt && r.durationSeconds) return acc + r.durationSeconds;
if (!r.endedAt) return acc + Math.round((Date.now() - r.startedAt.getTime()) / 1000);
return acc;
const now = Date.now();
// „Gesamt" = Wall-Clock (überlappende Tasks zählen einmal).
const totalSeconds = unionSeconds(
rows.map((r) => [r.startedAt.getTime(), r.endedAt ? r.endedAt.getTime() : now] as const),
);
// „getrackt" = Summe aller Task-Zeiten (kann bei parallelem Tracken größer sein).
const trackedSeconds = rows.reduce((acc, r) => {
if (r.endedAt) {
return acc + (r.durationSeconds ?? Math.round((r.endedAt.getTime() - r.startedAt.getTime()) / 1000));
}
return acc + Math.round((now - r.startedAt.getTime()) / 1000);
}, 0);
return { entries: rows, totalSeconds };
return { entries: rows, totalSeconds, trackedSeconds };
}),
/**
@@ -244,9 +276,10 @@ export const timeRouter = router({
};
type Bucket = {
userId: string;
totalSeconds: number;
trackedSeconds: number; // Summe aller Task-Zeiten (Parallel-Tracking)
byCategory: Record<string, number>;
active: Active | null;
intervals: Array<readonly [number, number]>;
};
const now = Date.now();
@@ -255,16 +288,18 @@ export const timeRouter = router({
for (const r of rows) {
const bucket: Bucket = perUser.get(r.userId) ?? {
userId: r.userId,
totalSeconds: 0,
trackedSeconds: 0,
byCategory: {},
active: null,
intervals: [],
};
const duration = r.endedAt
? (r.durationSeconds ??
Math.round((r.endedAt.getTime() - r.startedAt.getTime()) / 1000))
: Math.round((now - r.startedAt.getTime()) / 1000);
bucket.totalSeconds += duration;
bucket.trackedSeconds += duration;
bucket.byCategory[r.category] = (bucket.byCategory[r.category] ?? 0) + duration;
bucket.intervals.push([r.startedAt.getTime(), r.endedAt ? r.endedAt.getTime() : now]);
if (!r.endedAt) {
bucket.active = {
startedAt: r.startedAt.toISOString(),
@@ -277,6 +312,13 @@ export const timeRouter = router({
perUser.set(r.userId, bucket);
}
return Array.from(perUser.values());
// „Gesamt" pro Person = Wall-Clock (Vereinigung der Intervalle).
return Array.from(perUser.values()).map((b) => ({
userId: b.userId,
totalSeconds: unionSeconds(b.intervals),
trackedSeconds: b.trackedSeconds,
byCategory: b.byCategory,
active: b.active,
}));
}),
});