From afbda31a7c328ab3fff64dd9a7b1e2d4bc01832c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 24 Dec 2025 02:56:39 +0000 Subject: [PATCH] feat: show next scheduled sync time in calendar status - Track when calendar sync task completes and calculate next run time - Display countdown to next sync (e.g., 'Next sync in 45s') - Use TinyTick's useScheduledTaskRunIds and getTaskRunInfo to get scheduled timestamp - Export CALENDAR_SYNC_INTERVAL from apple-calendar service for reuse Co-Authored-By: yujonglee --- .../components/settings/calendar/status.tsx | 116 ++++++++++++++++-- apps/desktop/src/components/task-manager.tsx | 3 +- .../src/services/apple-calendar/index.ts | 1 + 3 files changed, 105 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/components/settings/calendar/status.tsx b/apps/desktop/src/components/settings/calendar/status.tsx index b90924df35..6cf52c074a 100644 --- a/apps/desktop/src/components/settings/calendar/status.tsx +++ b/apps/desktop/src/components/settings/calendar/status.tsx @@ -1,40 +1,132 @@ import { RefreshCwIcon } from "lucide-react"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { + useManager, + useRunningTaskRunIds, + useScheduledTaskRunIds, useScheduleTaskRunCallback, - useTaskRunRunning, } from "tinytick/ui-react"; import { Button } from "@hypr/ui/components/ui/button"; import { Spinner } from "@hypr/ui/components/ui/spinner"; -import { CALENDAR_SYNC_TASK_ID } from "../../../services/apple-calendar"; +import { + CALENDAR_SYNC_INTERVAL, + CALENDAR_SYNC_TASK_ID, +} from "../../../services/apple-calendar"; import * as main from "../../../store/tinybase/main"; export function CalendarStatus() { + const manager = useManager(); const calendars = main.UI.useTable("calendars", main.STORE_ID); const selectedCount = useMemo(() => { return Object.values(calendars).filter((cal) => cal.enabled).length; }, [calendars]); - const [currentTaskRunId, setCurrentTaskRunId] = useState( - undefined, - ); - const scheduleTaskRun = useScheduleTaskRunCallback( CALENDAR_SYNC_TASK_ID, undefined, 0, ); - const isRunning = useTaskRunRunning(currentTaskRunId ?? ""); + const runningTaskRunIds = useRunningTaskRunIds(); + const scheduledTaskRunIds = useScheduledTaskRunIds(); + + const [lastCompletedAt, setLastCompletedAt] = useState(null); + const [nextRunIn, setNextRunIn] = useState(null); + + const isRunning = useMemo(() => { + if (!manager) return false; + return runningTaskRunIds.some((id) => { + const info = manager.getTaskRunInfo(id); + return info?.taskId === CALENDAR_SYNC_TASK_ID; + }); + }, [manager, runningTaskRunIds]); + + const scheduledNextTimestamp = useMemo(() => { + if (!manager) return null; + for (const id of scheduledTaskRunIds) { + const info = manager.getTaskRunInfo(id); + if (info?.taskId === CALENDAR_SYNC_TASK_ID) { + return info.nextTimestamp; + } + } + return null; + }, [manager, scheduledTaskRunIds]); + + // Track when task completes (transitions from running to not running) + useEffect(() => { + if ( + !isRunning && + runningTaskRunIds.length === 0 && + lastCompletedAt === null + ) { + // On initial mount, estimate last completion based on when next run is scheduled + if (scheduledNextTimestamp) { + const estimatedLastCompletion = + scheduledNextTimestamp - CALENDAR_SYNC_INTERVAL; + if (estimatedLastCompletion > 0) { + setLastCompletedAt(estimatedLastCompletion); + } + } + } + }, [ + isRunning, + runningTaskRunIds.length, + lastCompletedAt, + scheduledNextTimestamp, + ]); + + // When task finishes running, update lastCompletedAt + const wasRunningRef = useMemo(() => ({ current: isRunning }), []); + useEffect(() => { + if (wasRunningRef.current && !isRunning) { + setLastCompletedAt(Date.now()); + } + wasRunningRef.current = isRunning; + }, [isRunning, wasRunningRef]); + + // Update countdown timer + useEffect(() => { + const updateNextRunIn = () => { + if (scheduledNextTimestamp) { + const remaining = Math.max( + 0, + Math.floor((scheduledNextTimestamp - Date.now()) / 1000), + ); + setNextRunIn(remaining); + } else if (lastCompletedAt) { + const nextRun = lastCompletedAt + CALENDAR_SYNC_INTERVAL; + const remaining = Math.max( + 0, + Math.floor((nextRun - Date.now()) / 1000), + ); + setNextRunIn(remaining); + } else { + setNextRunIn(null); + } + }; + + updateNextRunIn(); + const intervalId = setInterval(updateNextRunIn, 1000); + return () => clearInterval(intervalId); + }, [scheduledNextTimestamp, lastCompletedAt]); const handleRefetch = useCallback(() => { - const taskRunId = scheduleTaskRun(); - setCurrentTaskRunId(taskRunId); + scheduleTaskRun(); }, [scheduleTaskRun]); + const getStatusText = () => { + if (isRunning) { + return "Syncing..."; + } + if (nextRunIn !== null && nextRunIn > 0) { + return `Next sync in ${nextRunIn}s`; + } + return "Syncs every minute automatically"; + }; + if (selectedCount === 0) { return null; } @@ -45,9 +137,7 @@ export function CalendarStatus() { {selectedCount} calendar{selectedCount !== 1 ? "s" : ""} selected - - {isRunning ? "Syncing..." : "Syncs every minute automatically"} - + {getStatusText()}