Fix task timer desync between server and browser clocks
Calculate elapsed time server-side and return it to client. Timer now adds elapsedSeconds from server to local increments since response received, instead of calculating from startAt with potentially mismatched browser clock. Backend changes: - TaskHistoryItem: add getElapsedSeconds() to calculate time since startAt - TaskHistoryItemOutput: add elapsedSeconds field - CreateTaskResponse.TaskHistoryItemResponse: add elapsedSeconds field - CreateTaskMapper: map elapsedSeconds in toTaskHistoryItemOutput() Frontend changes: - TaskTimer: track responseTimestamp when running item changes - Calculate: accumulatedSeconds + elapsedSeconds + (now - responseTimestamp) - Remove parseISO import (no longer needed) - TaskHistoryItem type: add optional elapsedSeconds field This fixes the issue where timer doesn't update until task is stopped if server and browser clocks are out of sync. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f67d34306
commit
f01d72c2bb
7 changed files with 30 additions and 12 deletions
|
|
@ -1,6 +1,7 @@
|
|||
package net.beetechgroup.beetask.entities.task;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
|
||||
public class TaskHistoryItem {
|
||||
|
|
@ -35,4 +36,10 @@ public class TaskHistoryItem {
|
|||
public boolean isRunning() {
|
||||
return Objects.isNull(endAt);
|
||||
}
|
||||
|
||||
public long getElapsedSeconds() {
|
||||
if (Objects.isNull(startAt)) return 0;
|
||||
LocalDateTime endTime = Objects.nonNull(endAt) ? endAt : LocalDateTime.now();
|
||||
return Duration.between(startAt, endTime).toSeconds();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,5 +24,6 @@ public record CreateTaskResponse(
|
|||
record TaskHistoryItemResponse(
|
||||
Long id,
|
||||
LocalDateTime startAt,
|
||||
LocalDateTime endAt) {
|
||||
LocalDateTime endAt,
|
||||
Long elapsedSeconds) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ public class TaskControllerMapper {
|
|||
: null,
|
||||
output.finishedAt(),
|
||||
output.history().stream().map(taskHistoryItemOutput -> new TaskHistoryItemResponse(
|
||||
taskHistoryItemOutput.id(), taskHistoryItemOutput.startAt(), taskHistoryItemOutput.endAt())).toList()
|
||||
taskHistoryItemOutput.id(), taskHistoryItemOutput.startAt(), taskHistoryItemOutput.endAt(), taskHistoryItemOutput.elapsedSeconds())).toList()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ public class CreateTaskMapper {
|
|||
return new TaskHistoryItemOutput(
|
||||
taskHistoryItem.getId(),
|
||||
taskHistoryItem.getStartAt(),
|
||||
taskHistoryItem.getEndAt());
|
||||
taskHistoryItem.getEndAt(),
|
||||
taskHistoryItem.getElapsedSeconds());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ import java.time.LocalDateTime;
|
|||
public record TaskHistoryItemOutput(
|
||||
Long id,
|
||||
LocalDateTime startAt,
|
||||
LocalDateTime endAt) {
|
||||
LocalDateTime endAt,
|
||||
Long elapsedSeconds) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { differenceInSeconds, parseISO } from 'date-fns'
|
||||
import { differenceInSeconds } from 'date-fns'
|
||||
import { Clock, Play, Square } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
|
|
@ -26,6 +26,7 @@ function formatDuration(seconds: number) {
|
|||
|
||||
export function TaskTimer({ task, onUpdate, canControl = true }: TaskTimerProps) {
|
||||
const [isActionLoading, setIsActionLoading] = useState(false)
|
||||
const [responseTimestamp, setResponseTimestamp] = useState<Date | null>(null)
|
||||
|
||||
const history = task.history ?? []
|
||||
const runningItem = useMemo(
|
||||
|
|
@ -36,7 +37,7 @@ export function TaskTimer({ task, onUpdate, canControl = true }: TaskTimerProps)
|
|||
const accumulatedSeconds = useMemo(() => {
|
||||
return history.reduce((acc, item) => {
|
||||
if (item.endAt) {
|
||||
return acc + differenceInSeconds(parseISO(item.endAt), parseISO(item.startAt))
|
||||
return acc + (item.elapsedSeconds ?? 0)
|
||||
}
|
||||
return acc
|
||||
}, 0)
|
||||
|
|
@ -44,12 +45,17 @@ export function TaskTimer({ task, onUpdate, canControl = true }: TaskTimerProps)
|
|||
|
||||
const [displaySeconds, setDisplaySeconds] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setResponseTimestamp(new Date())
|
||||
}, [runningItem])
|
||||
|
||||
useEffect(() => {
|
||||
let interval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
if (runningItem) {
|
||||
if (runningItem && responseTimestamp) {
|
||||
interval = setInterval(() => {
|
||||
const runningSeconds = Math.max(0, differenceInSeconds(new Date(), parseISO(runningItem.startAt)))
|
||||
const elapsedSinceResponse = Math.max(0, differenceInSeconds(new Date(), responseTimestamp))
|
||||
const runningSeconds = (runningItem.elapsedSeconds ?? 0) + elapsedSinceResponse
|
||||
setDisplaySeconds(accumulatedSeconds + runningSeconds)
|
||||
}, 1000)
|
||||
} else {
|
||||
|
|
@ -59,17 +65,18 @@ export function TaskTimer({ task, onUpdate, canControl = true }: TaskTimerProps)
|
|||
return () => {
|
||||
if (interval) clearInterval(interval)
|
||||
}
|
||||
}, [runningItem, accumulatedSeconds])
|
||||
}, [runningItem, accumulatedSeconds, responseTimestamp])
|
||||
|
||||
// Initial calculation
|
||||
useEffect(() => {
|
||||
if (runningItem) {
|
||||
const runningSeconds = Math.max(0, differenceInSeconds(new Date(), parseISO(runningItem.startAt)))
|
||||
if (runningItem && responseTimestamp) {
|
||||
const elapsedSinceResponse = Math.max(0, differenceInSeconds(new Date(), responseTimestamp))
|
||||
const runningSeconds = (runningItem.elapsedSeconds ?? 0) + elapsedSinceResponse
|
||||
setDisplaySeconds(accumulatedSeconds + runningSeconds)
|
||||
} else {
|
||||
setDisplaySeconds(accumulatedSeconds)
|
||||
}
|
||||
}, [runningItem, accumulatedSeconds])
|
||||
}, [runningItem, accumulatedSeconds, responseTimestamp])
|
||||
|
||||
async function handleStart(e: React.MouseEvent) {
|
||||
e.stopPropagation()
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export type TaskHistoryItem = {
|
|||
id?: number
|
||||
startAt: string
|
||||
endAt: string | null
|
||||
elapsedSeconds?: number
|
||||
}
|
||||
|
||||
export type TaskProject = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue