From 2c9ba75111ee6d8d328fcfe9374ddf978c1410e5 Mon Sep 17 00:00:00 2001 From: 7174Andy Date: Sat, 1 Aug 2026 19:42:20 -0700 Subject: [PATCH] feat: split running jobs out of History and collapse History History mixed live runs with finished ones and always rendered in full. Three sections now: Scheduled, Running, and a collapsed-by-default History, bucketed by scheduleBucket() so each schedule lands in exactly one. The polling predicate derives from the same buckets instead of re-encoding the "still moving" rule separately. Co-Authored-By: Claude Opus 5 (1M context) --- components/home-content.tsx | 75 +++++++++++++++++++------------------ types/schedule.ts | 13 +++++++ 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/components/home-content.tsx b/components/home-content.tsx index df50ec3..4a6f468 100644 --- a/components/home-content.tsx +++ b/components/home-content.tsx @@ -5,6 +5,7 @@ import { format } from "date-fns"; import { toZonedTime } from "date-fns-tz"; import { ScheduleForm } from "@/components/schedule/schedule-form"; import { getSchedules, deleteSchedule, type ScheduleResponse } from "@/lib/actions/schedules"; +import { scheduleBucket } from "@/types/schedule"; // The database only changes as fast as the cron tick that writes it (every // 1 minute), so polling faster than this buys nothing. Background tabs are @@ -272,15 +273,19 @@ export function HomeContent() { })(); }, [fetchSchedules]); + const bucketed: Record, ScheduleResponse[]> = { + scheduled: [], + running: [], + history: [], + }; + for (const schedule of schedules) { + bucketed[scheduleBucket(schedule.status, schedule.runConclusion)].push(schedule); + } + // Anything not in a terminal state can still change on its own, so keep // polling. A dashboard where every schedule has settled starts no timer and // issues no requests. - const isActive = schedules.some( - (schedule) => - schedule.status === "pending" || - schedule.status === "processing" || - (schedule.status === "triggered" && schedule.runConclusion === null), - ); + const isActive = bucketed.scheduled.length > 0 || bucketed.running.length > 0; useEffect(() => { if (!isActive) return; @@ -312,9 +317,6 @@ export function HomeContent() { setEditingSchedule(null); } - const pendingSchedules = schedules.filter((s) => s.status === "pending"); - const completedSchedules = schedules.filter((s) => s.status !== "pending"); - if (isLoading) { return (
@@ -393,35 +395,36 @@ export function HomeContent() {
- {pendingSchedules.length > 0 && ( -
- -
- {pendingSchedules.map((schedule) => ( - - ))} + {( + [ + ["Scheduled", bucketed.scheduled], + ["Running", bucketed.running], + ] as const + ).map(([label, items]) => + items.length === 0 ? null : ( +
+ +
+ {items.map((schedule) => ( + + ))} +
-
+ ), )} - {completedSchedules.length > 0 && ( -
- -
- {completedSchedules.map((schedule) => ( + {bucketed.history.length > 0 && ( +
+ + History ({bucketed.history.length}) + +
+ {bucketed.history.map((schedule) => ( ))}
-
+ )}
)} diff --git a/types/schedule.ts b/types/schedule.ts index ad5ee7d..4700040 100644 --- a/types/schedule.ts +++ b/types/schedule.ts @@ -3,6 +3,19 @@ // triggered / failed: terminal, non-editable export type ScheduleStatus = "pending" | "processing" | "triggered" | "failed"; +// Which dashboard section a schedule belongs to. "triggered" splits across +// two: the GitHub run is still live until a conclusion comes back. +export function scheduleBucket( + status: ScheduleStatus, + runConclusion: string | null, +): "scheduled" | "running" | "history" { + if (status === "pending") return "scheduled"; + if (status === "processing" || (status === "triggered" && runConclusion === null)) { + return "running"; + } + return "history"; +} + export interface ScheduleFormData { repository: string | null; workflow: string | null;