diff --git a/frontend/src/app/pages/Workflows/app/HomeView.tsx b/frontend/src/app/pages/Workflows/app/HomeView.tsx index cb282d8c..449c0e63 100644 --- a/frontend/src/app/pages/Workflows/app/HomeView.tsx +++ b/frontend/src/app/pages/Workflows/app/HomeView.tsx @@ -1,35 +1,61 @@ import React, { useMemo, useState } from 'react'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; -import { runMissedRuns } from '@/shared/state/missedRunsSlice'; +import { runMissedRuns, dismissMissedRuns } from '@/shared/state/missedRunsSlice'; +import { openWorkflowMonitor } from '@/shared/state/dashboardLayoutSlice'; import { fireTimesWithin } from '@/app/pages/Workflows/scheduleUtils'; -import { colorForId, WC, statusChip, statusDot } from './uiKit'; +import { colorForWorkflow, useWC, statusChip, statusDot } from './uiKit'; import { clockOf, whenText } from './model'; import type { AppNav } from './types'; -interface ComingRun { wfId: string; title: string; time: string; sortKey: number; steps: number; } -interface ComingGroup { key: string; dayNum: number; dow: string; runs: ComingRun[]; } +interface ComingRun { wfId: string; title: string; time: string; sortKey: number; steps: number; color: string; } +interface ComingGroup { key: string; dayNum: number; dow: string; runs: ComingRun[]; countLabel: string; } +const COMING_CAP = 3; +// Enough fires to cover the whole 7-day window even at the 15-min floor +// (7 * 24 * 4 = 672), so each day's "N runs" count is the real total, not +// wherever a small global cap happened to run out. +const COMING_FIRE_CAP = 700; const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { + const WC = useWC(); const dispatch = useAppDispatch(); const items = useAppSelector((s) => s.workflows.items); const allRuns = useAppSelector((s) => s.workflows.allRuns); + const active = useAppSelector((s) => s.workflows.active); const missed = useAppSelector((s) => s.missedRuns.items); const [missedExpanded, setMissedExpanded] = useState(false); + const [expandedDays, setExpandedDays] = useState>(() => new Set()); const now = new Date(); const todayLabel = now.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' }); + const ongoing = useMemo(() => active.map((a) => { + const wf = items[a.workflow_id]; + const run = allRuns.find((r) => r.id === a.run_id); + const total = wf?.steps.length || 0; + const idx = run?.active_step_idx ?? 0; + const pct = total > 0 ? Math.min(100, Math.round(((idx + 1) / total) * 100)) : 10; + return { + wfId: a.workflow_id, + title: wf?.title || a.title || 'Workflow', + color: wf ? colorForWorkflow(wf) : WC.accent, + stepLabel: total > 0 ? `Step ${Math.min(idx + 1, total)}/${total}` : 'Running', + pct, + nowText: run?.last_tool_label || 'Working…', + clock: a.started_at ? clockOf(new Date(a.started_at)) : '', + }; + }), [active, items, allRuns]); + const comingGroups = useMemo(() => { const from = new Date(now); from.setHours(0, 0, 0, 0); const to = new Date(from.getTime() + 7 * 86400000); const byDay = new Map(); for (const wf of Object.values(items)) { if (wf.unsaved) continue; - const fires = fireTimesWithin(wf, now, to, 20); + const fires = fireTimesWithin(wf, now, to, COMING_FIRE_CAP); for (const f of fires) { const key = `${f.getFullYear()}-${f.getMonth()}-${f.getDate()}`; const arr = byDay.get(key) || []; - arr.push({ wfId: wf.id, title: wf.title || 'Untitled', time: clockOf(f), sortKey: f.getTime(), steps: wf.steps.length }); + arr.push({ wfId: wf.id, title: wf.title || 'Untitled', time: clockOf(f), sortKey: f.getTime(), steps: wf.steps.length, color: colorForWorkflow(wf) }); byDay.set(key, arr); } } @@ -38,7 +64,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { const d = new Date(from.getTime() + i * 86400000); const key = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`; const runs = (byDay.get(key) || []).sort((a, b) => a.sortKey - b.sortKey); - if (runs.length) groups.push({ key, dayNum: d.getDate(), dow: d.toLocaleDateString([], { weekday: 'short' }), runs }); + if (runs.length) groups.push({ key, dayNum: d.getDate(), dow: d.toLocaleDateString([], { weekday: 'short' }), runs, countLabel: runs.length === 1 ? '1 run' : `${runs.length} runs` }); } return groups; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -46,6 +72,7 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { const recents = useMemo(() => allRuns.slice(0, 8).map((r) => ({ id: r.id, + wfId: r.workflow_id, title: items[r.workflow_id]?.title || 'Workflow', status: r.status, summary: r.error || r.last_tool_label || (r.status === 'success' ? 'Completed' : r.status), @@ -57,22 +84,53 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { return (
-
+
{todayLabel}
- {missed.length > 0 && ( + {ongoing.length > 0 && (
+
+ Ongoing runs +
+
+
+ {ongoing.map((o) => ( +
dispatch(openWorkflowMonitor({ workflowId: o.wfId }))} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.10)`, borderRadius: WC.radius.md, padding: '11px 15px', cursor: 'pointer' }}> +
+
+
+ {o.title} + {o.stepLabel} +
+
+
+
+
{o.nowText}
+
+ {o.clock} +
+ Watch + +
+
+ ))} +
+
+ )} +
+ {missed.length > 0 && ( +
Missed {missed.length}
- +
{missedVisible.map((m) => ( -
+
{m.workflow_title}
@@ -80,40 +138,54 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => {
{whenText(new Date(m.scheduled_for), now)} +
dispatch(dismissMissedRuns([m.id]))} title="Dismiss" style={{ width: 26, height: 26, borderRadius: 7, display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', color: WC.faint, flex: 'none' }}> + +
))}
{missed.length > 3 && ( - )}
)} -
+
Coming up
{comingGroups.length === 0 && (
Nothing scheduled in the next 7 days.
)} +
{comingGroups.map((g) => (
{g.dayNum}
{g.dow}
+
{g.countLabel}
- {g.runs.map((r, i) => ( -
nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: '#FFFFFF', border: '1px solid rgba(33,30,27,0.08)', borderRadius: 11, padding: '12px 15px', cursor: 'pointer' }}> -
+ {(expandedDays.has(g.key) ? g.runs : g.runs.slice(0, COMING_CAP)).map((r, i) => ( +
nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, background: WC.raised, border: `1px solid rgba(${WC.inkRGB},0.08)`, borderRadius: WC.radius.md, padding: '12px 15px', cursor: 'pointer' }}> +
{r.title} {r.steps} steps {r.time}
))} + {g.runs.length > COMING_CAP && ( + + )}
))} +
+
@@ -121,13 +193,15 @@ const HomeView: React.FC<{ nav: AppNav }> = ({ nav }) => { {recents.length === 0 &&
No runs yet.
}
{recents.map((r) => ( -
-
+
nav.selectWorkflow(r.wfId)} style={{ display: 'flex', alignItems: 'center', gap: 13, padding: '11px 4px', borderBottom: `1px solid rgba(${WC.inkRGB},0.05)`, cursor: 'pointer' }}> +
{r.title}
{r.summary}
- {r.status === 'failure' ? 'Failed' : r.status === 'success' ? 'Success' : r.status} +
+ {r.status === 'failure' ? 'Failed' : r.status === 'success' ? 'Success' : r.status} +
{whenText(r.when, now)}
))}