From 5960c1e85efec06536b551f56f5d998fc93329d6 Mon Sep 17 00:00:00 2001 From: Anso Date: Fri, 26 Jun 2026 22:15:06 -0400 Subject: [PATCH] feat: show node and fleet targets on schedule timeline pills (#1480) Timeline pills and the mobile schedule list now identify what each scheduled run acts on instead of repeating the task name. Pills stay compact (firing time plus a category-aware target) and carry the full detail on hover: - Stack actions show the stack name. - Fleet snapshots show "Entire fleet". - Fleet auto-updates and node-scoped prune/scan show the selected node. - The hover tooltip adds the action label, task name, and node. The mobile list resolves node names too, so prune and scan rows name the node rather than the literal "system". A shared scheduleTargetDescriptor helper removes the target-label logic that was duplicated across the desktop and mobile views. The lifecycle lane is renamed "Stack lifecycle" to match the action-picker category wording. --- docs/features/scheduled-operations.mdx | 10 ++-- .../components/ScheduledOperationsView.tsx | 12 ++-- .../ScheduledOperationsView.test.tsx | 39 ++++++++++++- .../src/components/mobile/MobileSchedules.tsx | 52 ++++++++++++----- .../mobile/__tests__/MobileSchedules.test.tsx | 57 +++++++++++++++---- .../lib/__tests__/scheduledActions.test.ts | 47 +++++++++++++++ frontend/src/lib/scheduledActions.ts | 33 ++++++++++- 7 files changed, 214 insertions(+), 36 deletions(-) diff --git a/docs/features/scheduled-operations.mdx b/docs/features/scheduled-operations.mdx index c1c6a58f..4c0314b0 100644 --- a/docs/features/scheduled-operations.mdx +++ b/docs/features/scheduled-operations.mdx @@ -18,7 +18,7 @@ Schedules is a unified surface for every recurring maintenance operation Sencho Open the **Schedules** tab from the top navigation bar. The page opens on the Timeline view with the masthead, the five lane track, and a bottom time axis. - Schedules page in Timeline view. Header reads 'Scheduled Operations' on the left with a clock icon; on the right are a Timeline / All tasks toggle, a Refresh button, and a 'New Schedule' primary button. Below, a kicker 'NEXT 24 HOURS' sits above an italic display heading 'Next 24 hours' and a monospace date range 'Thu, May 14 11:21 → Fri, May 15 11:21'. A right-aligned 'Next' pill shows '03:00' with the subtitle 'Vul Scan · in 15h 38m'. Five lanes (Lifecycle, Updates, Security, Maintenance, Backups) run horizontally; a purple '03:00 Vul Scan' pill sits on the Security lane and a cyan '04:00 Nightly Snapshot' pill sits on the Backups lane. A cyan vertical 'now' rail glows at the left edge. Six time ticks (11:21, 16:09, 20:57, 01:45, 06:33, 11:21) run along the bottom axis. + Schedules page in Timeline view. Header reads 'Scheduled Operations' on the left with a clock icon; on the right are a Timeline / All tasks toggle, a Refresh button, and a 'New Schedule' primary button. Below, a kicker 'NEXT 24 HOURS' sits above an italic display heading 'Next 24 hours' and a monospace date range 'Thu, May 14 11:21 → Fri, May 15 11:21'. A right-aligned 'Next' pill shows '03:00' with the subtitle 'Vul Scan · in 15h 38m'. Five lanes (Stack lifecycle, Updates, Security, Maintenance, Backups) run horizontally; a purple '03:00 hub' pill sits on the Security lane and a cyan '04:00 Entire fleet' pill sits on the Backups lane. A cyan vertical 'now' rail glows at the left edge. Six time ticks (11:21, 16:09, 20:57, 01:45, 06:33, 11:21) run along the bottom axis. ## Timeline view @@ -26,8 +26,8 @@ Open the **Schedules** tab from the top navigation bar. The page opens on the Ti The Timeline plots every firing of every enabled task across a rolling 24-hour window starting from the current minute. - **Masthead.** A `NEXT 24 HOURS` kicker, an italic display heading, the window's start and end timestamps in a monospace range, and a right-anchored **Next** pill that reads out the time and task name of the next firing and a relative countdown. -- **Five lanes.** Lifecycle (label blue), Updates (success green), Security (label purple), Maintenance (warning amber), and Backups (brand cyan). The Lifecycle lane holds the five stack-lifecycle actions (Backup Stack Compose Files, Start / Bring Up Stack, Restart Stack, Stop Stack, Take Stack Down); Updates holds per-node and fleet image updates; Security holds vulnerability scans; Maintenance holds node resource prunes; Backups holds fleet snapshots. -- **Pills.** One pill per firing within the window, positioned proportionally to the firing's time. Each pill shows the firing time and the task name. Pills are color-matched to their lane. Click a pill to open the run history sheet for that task. +- **Five lanes.** Stack lifecycle (label blue), Updates (success green), Security (label purple), Maintenance (warning amber), and Backups (brand cyan). The Stack lifecycle lane holds the five stack-lifecycle actions (Backup Stack Compose Files, Start / Bring Up Stack, Restart Stack, Stop Stack, Take Stack Down); Updates holds per-node and fleet image updates; Security holds vulnerability scans; Maintenance holds node resource prunes; Backups holds fleet snapshots. +- **Pills.** One pill per firing within the window, positioned proportionally to the firing's time. Each pill shows the firing time and a target: the stack for stack actions, the selected node for prune and scan, and "Entire fleet" for a fleet snapshot. Hover a pill for the full detail (action, task name, and node). Pills are color-matched to their lane. Click a pill to open the run history sheet for that task. - **Now rail.** A glowing vertical rail at the current minute, anchored to the left of the track at page open and drifting right as time passes (the page recomputes positions periodically). - **Axis.** Six monospace time ticks run along the bottom, evenly spaced through the window. @@ -71,10 +71,10 @@ The All tasks toggle swaps the lane track for a sortable table. ## Creating a scheduled task -Click **New Schedule** in the header. The form opens in a centered modal. The Action picker lists every supported operation, grouped by category: Lifecycle, Updates, Security, Maintenance, and Backups. +Click **New Schedule** in the header. The form opens in a centered modal. The Action picker lists every supported operation, grouped by category: Stack lifecycle, Updates, Security, Maintenance, and Backups. - The New scheduled task modal with the Action combobox expanded. The dropdown groups actions under category headers: Lifecycle (Backup Stack Compose Files, Start / Bring Up Stack, Restart Stack, Stop Stack, Take Stack Down), Updates (Auto-update Stack, Auto-update All Stacks on Node), Security (Scan Node Images), Maintenance (Prune Node Resources), and Backups (Create Fleet Snapshot). Below the picker, partly visible, sit a Services row with 'echo' and 'prober' checkboxes, the Cron Expression input, the Enabled toggle, and the Delete after successful run checkbox. + The New scheduled task modal with the Action combobox expanded. The dropdown groups actions under category headers: Stack lifecycle (Backup Stack Compose Files, Start / Bring Up Stack, Restart Stack, Stop Stack, Take Stack Down), Updates (Auto-update Stack, Auto-update All Stacks on Node), Security (Scan Node Images), Maintenance (Prune Node Resources), and Backups (Create Fleet Snapshot). Below the picker, partly visible, sit a Services row with 'echo' and 'prober' checkboxes, the Cron Expression input, the Enabled toggle, and the Delete after successful run checkbox. Common fields: diff --git a/frontend/src/components/ScheduledOperationsView.tsx b/frontend/src/components/ScheduledOperationsView.tsx index 4a835a0d..971b2f9b 100644 --- a/frontend/src/components/ScheduledOperationsView.tsx +++ b/frontend/src/components/ScheduledOperationsView.tsx @@ -21,6 +21,7 @@ import { SCHEDULED_ACTION_CATEGORIES, getActionById, resolveTaskAction, + scheduleTargetDescriptor, DEFAULT_SCHEDULED_ACTION_ID, RISK_BADGE_CLASSES, RISK_DOT_CLASSES, @@ -347,6 +348,7 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter, p const cronDescription = getCronDescription(formCron); const cronFieldError = getCronFieldError(formCron); const nodeOptions = useMemo(() => nodes.map(n => ({ value: String(n.id), label: n.name })), [nodes]); + const nodeNameById = useMemo(() => new Map(nodes.map(n => [n.id, n.name])), [nodes]); const actionOptions = useMemo( () => SCHEDULED_ACTIONS.map(o => ({ @@ -498,9 +500,11 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter, p {lanePills.map((pill, idx) => { const leftPct = ((pill.runAt - now) / TIMELINE_WINDOW_MS) * 100; const clamped = Math.max(0, Math.min(100, leftPct)); - const targetLabel = pill.task.target_type === 'stack' - ? pill.task.target_id ?? pill.task.name - : pill.task.name; + const nodeName = pill.task.node_id != null ? nodeNameById.get(pill.task.node_id) : undefined; + const targetLabel = scheduleTargetDescriptor(pill.task, nodeName); + const actionLabel = resolveTaskAction(pill.task)?.label ?? pill.task.action; + const tooltip = `${actionLabel} · ${pill.task.name} · ${formatHourTick(pill.runAt)}` + + (nodeName ? ` · ${nodeName}` : ''); return (