From 95278843cf94917886aa8a85e2dce4bb9eb5be1e Mon Sep 17 00:00:00 2001 From: Anso Date: Sat, 18 Apr 2026 17:48:02 -0400 Subject: [PATCH] feat(schedules): next-24h timeline + merge auto-update into schedules (#681) * feat(backend): add stack update-preview endpoint for readiness board Adds GET /api/stacks/:stackName/update-preview that returns per-image semver diff, bump classification, and a stack-level summary powering the Auto-Update readiness board. - New UpdatePreviewService parses compose images, inspects local digests, fetches remote digests and tag lists, and finds the highest compatible semver tag. - Major bumps are flagged blocked until human review; unknown bumps rank below real semver so they cannot mask a major. - Rollback target is reconstructed through parseImageRef to preserve registry ports and drop the Docker Hub library/ prefix. - Registry helpers (httpGet, auth token, digest, tag list, ref parse) are extracted into registry-api.ts and shared with ImageUpdateService. - 28 Vitest cases cover parse, selection, bump math, digest rebuilds, blocked policy, and rollback target construction. * feat(schedules): next-24h timeline, merge auto-update crud, add readiness board Replace the flat task table with a Timeline view as the default, showing the next 24 hours of scheduled work across four lanes (Restart, Update, Scan, Prune) with a live now rail and per-firing pills. The All tasks tab preserves the existing CRUD surface. Merge Auto-update Stack into Schedules as a first-class action and replace the standalone Auto-Update Policies view with a per-stack Readiness board that surfaces version diffs, risk tags, changelog previews, and rollback targets sourced from the stack update-preview endpoint. --- .../src/__tests__/scheduler-service.test.ts | 39 ++ .../__tests__/update-preview-service.test.ts | 227 +++++++ backend/src/index.ts | 29 +- backend/src/services/ImageUpdateService.ts | 170 +----- backend/src/services/SchedulerService.ts | 15 + backend/src/services/UpdatePreviewService.ts | 270 +++++++++ backend/src/services/registry-api.ts | 169 ++++++ docs/features/auto-update-policies.mdx | 204 ++----- docs/features/scheduled-operations.mdx | 31 +- docs/images/auto-update/readiness-board.png | Bin 0 -> 101378 bytes docs/images/scheduled-operations/timeline.png | Bin 0 -> 66057 bytes .../src/components/AutoUpdatePoliciesView.tsx | 558 ------------------ .../components/AutoUpdateReadinessView.tsx | 468 +++++++++++++++ frontend/src/components/EditorLayout.tsx | 6 +- .../components/ScheduledOperationsView.tsx | 206 ++++++- frontend/src/types/scheduling.ts | 1 + 16 files changed, 1515 insertions(+), 878 deletions(-) create mode 100644 backend/src/__tests__/update-preview-service.test.ts create mode 100644 backend/src/services/UpdatePreviewService.ts create mode 100644 backend/src/services/registry-api.ts create mode 100644 docs/images/auto-update/readiness-board.png create mode 100644 docs/images/scheduled-operations/timeline.png delete mode 100644 frontend/src/components/AutoUpdatePoliciesView.tsx create mode 100644 frontend/src/components/AutoUpdateReadinessView.tsx diff --git a/backend/src/__tests__/scheduler-service.test.ts b/backend/src/__tests__/scheduler-service.test.ts index f28494fd..a0f4e09c 100644 --- a/backend/src/__tests__/scheduler-service.test.ts +++ b/backend/src/__tests__/scheduler-service.test.ts @@ -171,6 +171,45 @@ describe('SchedulerService - calculateNextRun', () => { }); }); +describe('SchedulerService - calculateRunsWithin', () => { + it('expands hourly cron into every firing within a 24h window when limit allows', () => { + const svc = SchedulerService.getInstance(); + const from = Date.now(); + const to = from + 24 * 60 * 60 * 1000; + const runs = svc.calculateRunsWithin('0 * * * *', from, to, 32); + expect(runs.length).toBeGreaterThanOrEqual(23); + expect(runs.length).toBeLessThanOrEqual(24); + for (const run of runs) { + expect(run).toBeGreaterThanOrEqual(from); + expect(run).toBeLessThanOrEqual(to); + } + }); + + it('returns a single firing for a daily cron', () => { + const svc = SchedulerService.getInstance(); + const from = Date.now(); + const to = from + 24 * 60 * 60 * 1000; + const runs = svc.calculateRunsWithin('0 3 * * *', from, to); + expect(runs.length).toBeLessThanOrEqual(2); + expect(runs.length).toBeGreaterThanOrEqual(1); + }); + + it('honours the limit parameter to avoid runaway expansions', () => { + const svc = SchedulerService.getInstance(); + const from = Date.now(); + const to = from + 60 * 60 * 1000; + const runs = svc.calculateRunsWithin('* * * * *', from, to, 5); + expect(runs.length).toBe(5); + }); + + it('returns empty array for invalid cron instead of throwing', () => { + const svc = SchedulerService.getInstance(); + const from = Date.now(); + const to = from + 60 * 60 * 1000; + expect(svc.calculateRunsWithin('not a cron', from, to)).toEqual([]); + }); +}); + // ── License gating ───────────────────────────────────────────────────── describe('SchedulerService - license gating', () => { diff --git a/backend/src/__tests__/update-preview-service.test.ts b/backend/src/__tests__/update-preview-service.test.ts new file mode 100644 index 00000000..d8a9ffde --- /dev/null +++ b/backend/src/__tests__/update-preview-service.test.ts @@ -0,0 +1,227 @@ +import { describe, it, expect, vi } from 'vitest'; +import { + parseSemverTag, + findNextTag, + computeSemverBump, + computeImagePreview, + buildSummary, + type ComputePreviewDeps, +} from '../services/UpdatePreviewService'; + +describe('parseSemverTag', () => { + it('parses bare semver', () => { + expect(parseSemverTag('1.2.3')).toMatchObject({ prefix: '', major: 1, minor: 2, patch: 3, suffix: '' }); + }); + it('parses v-prefixed semver', () => { + expect(parseSemverTag('v1.2.3')).toMatchObject({ prefix: 'v', major: 1, minor: 2, patch: 3 }); + }); + it('parses suffixed semver (alpine, slim)', () => { + expect(parseSemverTag('27.1.4-alpine')).toMatchObject({ major: 27, minor: 1, patch: 4, suffix: 'alpine' }); + }); + it('rejects non-semver', () => { + expect(parseSemverTag('latest')).toBeNull(); + expect(parseSemverTag('main')).toBeNull(); + expect(parseSemverTag('1.2')).toBeNull(); + }); +}); + +describe('findNextTag', () => { + it('picks highest semver greater than current', () => { + const tags = ['27.1.3', '27.1.4', '27.1.5', '27.2.0', '27.1.5-alpine']; + expect(findNextTag('27.1.4', tags)).toBe('27.2.0'); + }); + it('keeps prefix style (v vs bare)', () => { + const tags = ['1.2.3', '1.2.4', 'v1.2.4', 'v1.3.0']; + expect(findNextTag('v1.2.3', tags)).toBe('v1.3.0'); + expect(findNextTag('1.2.3', tags)).toBe('1.2.4'); + }); + it('keeps suffix style (alpine)', () => { + const tags = ['1.2.3', '1.2.4', '1.2.3-alpine', '1.2.4-alpine']; + expect(findNextTag('1.2.3-alpine', tags)).toBe('1.2.4-alpine'); + }); + it('returns null when current tag is not semver', () => { + expect(findNextTag('latest', ['latest', '1.2.3'])).toBeNull(); + }); + it('returns null when no higher semver exists', () => { + expect(findNextTag('1.2.3', ['1.2.0', '1.2.1', '1.2.2'])).toBeNull(); + }); +}); + +describe('computeSemverBump', () => { + it('detects major jump', () => { + expect(computeSemverBump('1.2.3', '2.0.0')).toBe('major'); + }); + it('detects minor jump', () => { + expect(computeSemverBump('1.2.3', '1.3.0')).toBe('minor'); + }); + it('detects patch jump', () => { + expect(computeSemverBump('1.2.3', '1.2.4')).toBe('patch'); + }); + it('returns patch when tags are identical (digest rebuild)', () => { + expect(computeSemverBump('latest', 'latest')).toBe('patch'); + }); + it('returns none when no next tag', () => { + expect(computeSemverBump('1.2.3', null)).toBe('none'); + }); + it('returns unknown for non-semver pairs', () => { + expect(computeSemverBump('main', 'stable')).toBe('unknown'); + }); +}); + +function makeDeps(overrides: Partial = {}): ComputePreviewDeps { + return { + getCredentials: vi.fn().mockResolvedValue(null), + getLocalDigest: vi.fn().mockResolvedValue(null), + getRemoteDigest: vi.fn().mockResolvedValue(null), + listRegistryTags: vi.fn().mockResolvedValue([]), + ...overrides, + }; +} + +describe('computeImagePreview', () => { + it('reports no update when digests match and no higher tag exists', async () => { + const deps = makeDeps({ + getLocalDigest: vi.fn().mockResolvedValue('sha256:aaa'), + getRemoteDigest: vi.fn().mockResolvedValue('sha256:aaa'), + listRegistryTags: vi.fn().mockResolvedValue(['1.2.3']), + }); + const result = await computeImagePreview('web', 'nginx:1.2.3', deps); + expect(result.has_update).toBe(false); + expect(result.semver_bump).toBe('none'); + expect(result.next_tag).toBeNull(); + }); + + it('reports digest rebuild as patch when tag is unchanged but digest differs', async () => { + const deps = makeDeps({ + getLocalDigest: vi.fn().mockResolvedValue('sha256:aaa'), + getRemoteDigest: vi.fn().mockResolvedValue('sha256:bbb'), + listRegistryTags: vi.fn().mockResolvedValue([]), + }); + const result = await computeImagePreview('web', 'nginx:latest', deps); + expect(result.has_update).toBe(true); + expect(result.current_tag).toBe('latest'); + expect(result.next_tag).toBe('latest'); + expect(result.semver_bump).toBe('patch'); + }); + + it('reports higher semver tag when available', async () => { + const deps = makeDeps({ + getLocalDigest: vi.fn().mockResolvedValue('sha256:aaa'), + getRemoteDigest: vi.fn().mockResolvedValue('sha256:aaa'), + listRegistryTags: vi.fn().mockResolvedValue(['27.1.4', '27.1.5', '27.2.0']), + }); + const result = await computeImagePreview('engine', 'docker.io/library/docker:27.1.4', deps); + expect(result.has_update).toBe(true); + expect(result.next_tag).toBe('27.2.0'); + expect(result.semver_bump).toBe('minor'); + }); + + it('flags major semver jumps', async () => { + const deps = makeDeps({ + getLocalDigest: vi.fn().mockResolvedValue('sha256:aaa'), + getRemoteDigest: vi.fn().mockResolvedValue('sha256:aaa'), + listRegistryTags: vi.fn().mockResolvedValue(['1.2.3', '2.0.0']), + }); + const result = await computeImagePreview('db', 'postgres:1.2.3', deps); + expect(result.next_tag).toBe('2.0.0'); + expect(result.semver_bump).toBe('major'); + }); +}); + +describe('buildSummary', () => { + const baseImage = (partial: Partial[1][number]>) => ({ + service: 'svc', + image: 'nginx:1.0.0', + current_tag: '1.0.0', + next_tag: null, + has_update: false, + semver_bump: 'none' as const, + ...partial, + }); + + it('flags blocked when any image has a major bump', () => { + const images = [ + baseImage({ service: 'web', has_update: true, semver_bump: 'major', next_tag: '2.0.0' }), + baseImage({ service: 'cache', has_update: true, semver_bump: 'patch', next_tag: '1.0.1', image: 'redis:1.0.0' }), + ]; + const preview = buildSummary('stacky', images); + expect(preview.summary.blocked).toBe(true); + expect(preview.summary.blocked_reason).toMatch(/major/i); + expect(preview.summary.semver_bump).toBe('major'); + }); + + it('picks first updated image as primary', () => { + const images = [ + baseImage({ service: 'clean', has_update: false }), + baseImage({ service: 'web', has_update: true, semver_bump: 'minor', next_tag: '1.1.0', image: 'nginx:1.0.0' }), + ]; + const preview = buildSummary('stacky', images); + expect(preview.summary.primary_image).toBe('nginx:1.0.0'); + expect(preview.summary.next_tag).toBe('1.1.0'); + expect(preview.summary.blocked).toBe(false); + }); + + it('returns has_update=false when no images update', () => { + const images = [baseImage({ service: 'clean', has_update: false })]; + const preview = buildSummary('stacky', images); + expect(preview.summary.has_update).toBe(false); + expect(preview.summary.semver_bump).toBe('none'); + }); + + it('handles empty image list', () => { + const preview = buildSummary('empty', []); + expect(preview.summary.has_update).toBe(false); + expect(preview.summary.primary_image).toBeNull(); + expect(preview.rollback_target).toBeNull(); + }); + + it('computes rollback target from current tag of primary', () => { + const images = [ + baseImage({ service: 'web', image: 'nginx:1.0.0', has_update: true, semver_bump: 'patch', next_tag: '1.0.1', current_tag: '1.0.0' }), + ]; + const preview = buildSummary('stacky', images); + expect(preview.rollback_target).toBe('nginx:1.0.0'); + }); + + it('computes rollback target for Docker Hub library image', () => { + const images = [ + baseImage({ service: 'db', image: 'library/postgres:16', has_update: true, semver_bump: 'patch', next_tag: '16', current_tag: '16' }), + ]; + expect(buildSummary('stacky', images).rollback_target).toBe('postgres:16'); + }); + + it('computes rollback target for registry with port', () => { + const images = [ + baseImage({ + service: 'app', + image: 'registry.example.com:5000/team/image:1.2.3', + has_update: true, + semver_bump: 'patch', + next_tag: '1.2.4', + current_tag: '1.2.3', + }), + ]; + expect(buildSummary('stacky', images).rollback_target).toBe('registry.example.com:5000/team/image:1.2.3'); + }); + + it('leaves blocked false for patch/minor only updates', () => { + const images = [ + baseImage({ service: 'web', has_update: true, semver_bump: 'patch', next_tag: '1.0.1' }), + baseImage({ service: 'cache', has_update: true, semver_bump: 'minor', next_tag: '1.1.0', image: 'redis:1.0.0' }), + ]; + const preview = buildSummary('stacky', images); + expect(preview.summary.blocked).toBe(false); + expect(preview.summary.blocked_reason).toBeNull(); + expect(preview.summary.semver_bump).toBe('minor'); + }); + + it('does not let unknown bumps mask a real major bump', () => { + const images = [ + baseImage({ service: 'odd', has_update: true, semver_bump: 'unknown', next_tag: 'main', image: 'ghcr.io/org/odd:main' }), + baseImage({ service: 'db', has_update: true, semver_bump: 'major', next_tag: '2.0.0', image: 'postgres:1.0.0' }), + ]; + const preview = buildSummary('stacky', images); + expect(preview.summary.semver_bump).toBe('major'); + expect(preview.summary.blocked).toBe(true); + }); +}); diff --git a/backend/src/index.ts b/backend/src/index.ts index 3b0169d0..9658893c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -26,6 +26,7 @@ import { MonitorService } from './services/MonitorService'; import { AutoHealService } from './services/AutoHealService'; import { DockerEventManager } from './services/DockerEventManager'; import { ImageUpdateService } from './services/ImageUpdateService'; +import { UpdatePreviewService } from './services/UpdatePreviewService'; import { templateService } from './services/TemplateService'; import { ErrorParser } from './utils/ErrorParser'; import { NodeRegistry } from './services/NodeRegistry'; @@ -5198,6 +5199,21 @@ app.post('/api/stacks/:stackName/start', async (req: Request, res: Response) => } }); +// Update preview: semver diff, risk tagging, rollback target for the readiness board +app.get('/api/stacks/:stackName/update-preview', async (req: Request, res: Response) => { + const stackName = req.params.stackName as string; + if (!isValidStackName(stackName)) { + return res.status(400).json({ error: 'Invalid stack name' }); + } + try { + const preview = await UpdatePreviewService.getInstance().getPreview(req.nodeId, stackName); + res.json(preview); + } catch (error) { + console.error(`[Stacks] Update preview failed: ${stackName}`, error); + res.status(500).json({ error: 'Failed to compute update preview' }); + } +}); + // Update stack: pull images and recreate containers app.post('/api/stacks/:stackName/update', async (req: Request, res: Response) => { const stackName = req.params.stackName as string; @@ -6428,7 +6444,18 @@ app.get('/api/scheduled-tasks', (req: Request, res: Response): void => { } else if (excludeAction) { tasks = tasks.filter(t => t.action !== excludeAction); } - res.json(tasks); + + // Timeline view needs every firing inside a rolling window, not just the next run. + const scheduler = SchedulerService.getInstance(); + const windowHours = Math.min(Math.max(Number(req.query.window_hours) || 24, 1), 168); + const from = Date.now(); + const to = from + windowHours * 60 * 60 * 1000; + const enriched = tasks.map(t => ({ + ...t, + next_runs: t.enabled === 1 ? scheduler.calculateRunsWithin(t.cron_expression, from, to) : [], + })); + + res.json(enriched); } catch (error) { console.error('[ScheduledTasks] List error:', error); res.status(500).json({ error: 'Failed to fetch scheduled tasks' }); diff --git a/backend/src/services/ImageUpdateService.ts b/backend/src/services/ImageUpdateService.ts index 79f1cc6e..0c07fa54 100644 --- a/backend/src/services/ImageUpdateService.ts +++ b/backend/src/services/ImageUpdateService.ts @@ -1,5 +1,3 @@ -import https from 'https'; -import http from 'http'; import path from 'path'; import YAML from 'yaml'; import DockerController from './DockerController'; @@ -8,87 +6,20 @@ import { FileSystemService } from './FileSystemService'; import { RegistryService } from './RegistryService'; import { NodeRegistry } from './NodeRegistry'; import { NotificationService } from './NotificationService'; +import { parseImageRef, getRemoteDigest } from './registry-api'; import { isDebugEnabled } from '../utils/debug'; import { getErrorMessage } from '../utils/errors'; const BACKFILL_KEY = 'image_update_notifications_backfilled'; -// ─── Image ref parsing ──────────────────────────────────────────────────────── - -interface ParsedRef { - registry: string; // e.g. "registry-1.docker.io", "lscr.io", "ghcr.io" - repo: string; // e.g. "library/nginx", "linuxserver/sonarr" - tag: string; // e.g. "latest", "1.25" -} - -function parseImageRef(imageRef: string): ParsedRef | null { - if (imageRef.startsWith('sha256:')) return null; - - // Strip digest pin (e.g. "nginx@sha256:abc" → "nginx") - const atIdx = imageRef.indexOf('@'); - if (atIdx !== -1) imageRef = imageRef.slice(0, atIdx); - - let registry = 'registry-1.docker.io'; - let rest = imageRef; - - const slashIdx = imageRef.indexOf('/'); - if (slashIdx !== -1) { - const firstPart = imageRef.slice(0, slashIdx); - if (firstPart.includes('.') || firstPart.includes(':') || firstPart === 'localhost') { - registry = firstPart; - rest = imageRef.slice(slashIdx + 1); - } - } - - // Extract tag - let tag = 'latest'; - const colonIdx = rest.lastIndexOf(':'); - if (colonIdx > 0) { - tag = rest.slice(colonIdx + 1); - rest = rest.slice(0, colonIdx); - } - - // Docker Hub official images (no slash) → prepend "library/" - if (registry === 'registry-1.docker.io' && !rest.includes('/')) { - rest = `library/${rest}`; - } - - return { registry, repo: rest, tag }; -} - export interface ImageCheckResult { hasUpdate: boolean; error?: string; } -// ─── Minimal HTTP helper ────────────────────────────────────────────────────── - -interface HttpResult { - statusCode: number; - headers: Record; - body: string; -} - -function httpGet(url: string, headers: Record = {}, timeoutMs = 10000): Promise { - return new Promise((resolve, reject) => { - const lib = url.startsWith('https:') ? https : http; - const req = lib.get(url, { headers }, (res) => { - let body = ''; - res.on('data', (chunk: Buffer) => { body += chunk.toString(); }); - res.on('end', () => resolve({ - statusCode: res.statusCode ?? 0, - headers: res.headers as Record, - body, - })); - }); - req.on('error', reject); - req.setTimeout(timeoutMs, () => req.destroy(new Error('Request timed out'))); - }); -} - // ─── Compose file helpers ──────────────────────────────────────────────────── -function loadDotEnv(content: string): Record { +export function loadDotEnv(content: string): Record { const vars: Record = {}; for (const line of content.split('\n')) { const trimmed = line.trim(); @@ -106,10 +37,15 @@ function loadDotEnv(content: string): Record { return vars; } -function extractImagesFromCompose( +export interface ComposeServiceImage { + service: string; + image: string; +} + +export function extractServiceImagesFromCompose( yamlContent: string, - envVars: Record -): string[] { + envVars: Record, +): ComposeServiceImage[] { let parsed: Record; try { parsed = YAML.parse(yamlContent) as Record; @@ -118,8 +54,8 @@ function extractImagesFromCompose( } if (!parsed?.services || typeof parsed.services !== 'object') return []; - const images: string[] = []; - for (const svc of Object.values(parsed.services as Record)) { + const out: ComposeServiceImage[] = []; + for (const [service, svc] of Object.entries(parsed.services as Record)) { if (!svc || typeof svc !== 'object') continue; const raw = (svc as Record).image; if (!raw || typeof raw !== 'string') continue; @@ -137,84 +73,16 @@ function extractImagesFromCompose( ref = ref.trim(); if (!ref || ref.includes('${') || ref.startsWith('sha256:')) continue; - images.push(ref); + out.push({ service, image: ref }); } - return images; + return out; } -// ─── Registry auth ──────────────────────────────────────────────────────────── - -async function getAuthToken( - registry: string, - repo: string, - credentials?: { username: string; password: string } | null -): Promise { - try { - const basicHeaders: Record = {}; - if (credentials) { - basicHeaders['Authorization'] = `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`; - } - - let tokenUrl: string; - - if (registry === 'registry-1.docker.io') { - tokenUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull`; - } else { - // Ping /v2/ to get the WWW-Authenticate challenge - const ping = await httpGet(`https://${registry}/v2/`, basicHeaders); - const wwwAuth = ping.headers['www-authenticate'] as string | undefined; - if (!wwwAuth) return null; - - const realmMatch = wwwAuth.match(/realm="([^"]+)"/); - const serviceMatch = wwwAuth.match(/service="([^"]+)"/); - const scopeMatch = wwwAuth.match(/scope="([^"]+)"/); - if (!realmMatch) return null; - - const params = new URLSearchParams(); - if (serviceMatch) params.set('service', serviceMatch[1]); - params.set('scope', scopeMatch ? scopeMatch[1] : `repository:${repo}:pull`); - tokenUrl = `${realmMatch[1]}?${params.toString()}`; - } - - const tokenRes = await httpGet(tokenUrl, basicHeaders); - if (tokenRes.statusCode !== 200) return null; - - const parsed = JSON.parse(tokenRes.body); - return parsed.token ?? parsed.access_token ?? null; - } catch { - return null; - } -} - -// ─── Remote digest lookup ───────────────────────────────────────────────────── - -// Include manifest list types so we get the fat-manifest digest for multi-arch -// images - this matches what Docker stores in local RepoDigests. -const MANIFEST_ACCEPT = [ - 'application/vnd.docker.distribution.manifest.list.v2+json', - 'application/vnd.docker.distribution.manifest.v2+json', - 'application/vnd.oci.image.index.v1+json', - 'application/vnd.oci.image.manifest.v1+json', -].join(', '); - -async function getRemoteDigest( - registry: string, - repo: string, - tag: string, - credentials?: { username: string; password: string } | null -): Promise { - try { - const token = await getAuthToken(registry, repo, credentials); - const headers: Record = { Accept: MANIFEST_ACCEPT }; - if (token) headers['Authorization'] = `Bearer ${token}`; - - const res = await httpGet(`https://${registry}/v2/${repo}/manifests/${tag}`, headers); - if (res.statusCode !== 200) return null; - - return (res.headers['docker-content-digest'] as string) ?? null; - } catch { - return null; - } +export function extractImagesFromCompose( + yamlContent: string, + envVars: Record, +): string[] { + return extractServiceImagesFromCompose(yamlContent, envVars).map(e => e.image); } // ─── Service ────────────────────────────────────────────────────────────────── diff --git a/backend/src/services/SchedulerService.ts b/backend/src/services/SchedulerService.ts index c980566e..d5010a7e 100644 --- a/backend/src/services/SchedulerService.ts +++ b/backend/src/services/SchedulerService.ts @@ -140,6 +140,21 @@ export class SchedulerService { return expr.next().toDate().getTime(); } + public calculateRunsWithin(cronExpression: string, fromMs: number, toMs: number, limit = 16): number[] { + try { + const expr = CronExpressionParser.parse(cronExpression, { currentDate: new Date(fromMs) }); + const runs: number[] = []; + while (runs.length < limit) { + const next = expr.next().toDate().getTime(); + if (next > toMs) break; + runs.push(next); + } + return runs; + } catch { + return []; + } + } + /** * Fire a notification without awaiting completion, catching any promise * rejection so the scheduler never crashes on a failed dispatch. diff --git a/backend/src/services/UpdatePreviewService.ts b/backend/src/services/UpdatePreviewService.ts new file mode 100644 index 00000000..11b5dad4 --- /dev/null +++ b/backend/src/services/UpdatePreviewService.ts @@ -0,0 +1,270 @@ +import DockerController from './DockerController'; +import { FileSystemService } from './FileSystemService'; +import { RegistryService } from './RegistryService'; +import { + extractServiceImagesFromCompose, + loadDotEnv, + type ComposeServiceImage, +} from './ImageUpdateService'; +import { + parseImageRef, + getRemoteDigest, + listRegistryTags, + type RegistryCredentials, +} from './registry-api'; + +export type SemverBump = 'none' | 'patch' | 'minor' | 'major' | 'unknown'; + +export interface UpdatePreviewImage { + service: string; + image: string; + current_tag: string; + next_tag: string | null; + has_update: boolean; + semver_bump: SemverBump; +} + +export interface UpdatePreviewSummary { + has_update: boolean; + primary_image: string | null; + current_tag: string | null; + next_tag: string | null; + semver_bump: SemverBump; + blocked: boolean; + blocked_reason: string | null; +} + +export interface UpdatePreview { + stack_name: string; + images: UpdatePreviewImage[]; + summary: UpdatePreviewSummary; + rollback_target: string | null; + changelog: string | null; +} + +interface SemverParts { + prefix: string; + major: number; + minor: number; + patch: number; + suffix: string; + raw: string; +} + +const SEMVER_RE = /^(v)?(\d+)\.(\d+)\.(\d+)(?:-([A-Za-z][A-Za-z0-9.-]*))?$/; + +export function parseSemverTag(tag: string): SemverParts | null { + const m = tag.match(SEMVER_RE); + if (!m) return null; + return { + prefix: m[1] ?? '', + major: Number(m[2]), + minor: Number(m[3]), + patch: Number(m[4]), + suffix: m[5] ?? '', + raw: tag, + }; +} + +function compareSemver(a: SemverParts, b: SemverParts): number { + if (a.major !== b.major) return a.major - b.major; + if (a.minor !== b.minor) return a.minor - b.minor; + return a.patch - b.patch; +} + +export function findNextTag(currentTag: string, availableTags: string[]): string | null { + const current = parseSemverTag(currentTag); + if (!current) return null; + let best: SemverParts | null = null; + for (const tag of availableTags) { + const parsed = parseSemverTag(tag); + if (!parsed) continue; + if (parsed.prefix !== current.prefix) continue; + if (parsed.suffix !== current.suffix) continue; + if (compareSemver(parsed, current) <= 0) continue; + if (!best || compareSemver(parsed, best) > 0) best = parsed; + } + return best ? best.raw : null; +} + +export function computeSemverBump(currentTag: string, nextTag: string | null): SemverBump { + if (!nextTag) return 'none'; + if (nextTag === currentTag) return 'patch'; + const current = parseSemverTag(currentTag); + const next = parseSemverTag(nextTag); + if (!current || !next) return 'unknown'; + if (next.major > current.major) return 'major'; + if (next.minor > current.minor) return 'minor'; + if (next.patch > current.patch) return 'patch'; + return 'none'; +} + +function maxBump(a: SemverBump, b: SemverBump): SemverBump { + // Ranking: none < unknown < patch < minor < major. + // unknown ranks below real semver bumps so a single unparseable tag never masks + // a genuine major bump elsewhere in the stack. + const order: SemverBump[] = ['none', 'unknown', 'patch', 'minor', 'major']; + const rank = (x: SemverBump) => order.indexOf(x); + return rank(a) >= rank(b) ? a : b; +} + +async function loadStackImages( + nodeId: number, + stackName: string, +): Promise { + const fs = FileSystemService.getInstance(nodeId); + const composeContent = await fs.getStackContent(stackName); + let envVars: Record = {}; + try { + const envContent = await fs.getEnvContent(stackName); + envVars = loadDotEnv(envContent); + } catch { + // No env file - fall back to process.env only + } + const merged: Record = { ...envVars }; + for (const [k, v] of Object.entries(process.env)) { + if (v !== undefined) merged[k] = v; + } + return extractServiceImagesFromCompose(composeContent, merged); +} + +export interface ComputePreviewDeps { + getLocalDigest: (imageRef: string) => Promise; + getRemoteDigest: typeof getRemoteDigest; + listRegistryTags: typeof listRegistryTags; + getCredentials: (registry: string) => Promise; +} + +export async function computeImagePreview( + service: string, + imageRef: string, + deps: ComputePreviewDeps, +): Promise { + const parsed = parseImageRef(imageRef); + if (!parsed) { + return { + service, + image: imageRef, + current_tag: 'unknown', + next_tag: null, + has_update: false, + semver_bump: 'none', + }; + } + + const credentials = await deps.getCredentials(parsed.registry); + + // Digest-based: is a new build of the SAME tag available? + const [localDigest, remoteDigest] = await Promise.all([ + deps.getLocalDigest(imageRef), + deps.getRemoteDigest(parsed.registry, parsed.repo, parsed.tag, credentials), + ]); + const digestUpdate = Boolean(localDigest && remoteDigest && localDigest !== remoteDigest); + + // Tag-based: is a higher semver tag available? + const tags = await deps.listRegistryTags(parsed.registry, parsed.repo, credentials); + const nextTag = findNextTag(parsed.tag, tags); + + const hasUpdate = digestUpdate || nextTag !== null; + let semverBump: SemverBump = 'none'; + let resolvedNext: string | null = null; + if (nextTag) { + resolvedNext = nextTag; + semverBump = computeSemverBump(parsed.tag, nextTag); + } else if (digestUpdate) { + resolvedNext = parsed.tag; + semverBump = 'patch'; + } + + return { + service, + image: imageRef, + current_tag: parsed.tag, + next_tag: resolvedNext, + has_update: hasUpdate, + semver_bump: semverBump, + }; +} + +function buildRollbackTarget(image: string, currentTag: string): string | null { + const parsed = parseImageRef(image); + if (!parsed) return null; + // Reconstruct without the library/ prefix Docker Hub uses internally, + // so "library/nginx" renders as "nginx:1.0.0" not "registry-1.docker.io/library/nginx:1.0.0". + const isDockerHub = parsed.registry === 'registry-1.docker.io'; + const repo = isDockerHub && parsed.repo.startsWith('library/') + ? parsed.repo.slice('library/'.length) + : parsed.repo; + const base = isDockerHub ? repo : `${parsed.registry}/${repo}`; + return `${base}:${currentTag}`; +} + +export function buildSummary(stackName: string, images: UpdatePreviewImage[]): UpdatePreview { + const updated = images.filter(i => i.has_update); + const hasUpdate = updated.length > 0; + const primary = updated[0] ?? images[0] ?? null; + const overallBump = updated.reduce( + (acc, img) => maxBump(acc, img.semver_bump), + 'none', + ); + const blocked = overallBump === 'major'; + return { + stack_name: stackName, + images, + summary: { + has_update: hasUpdate, + primary_image: primary ? primary.image : null, + current_tag: primary ? primary.current_tag : null, + next_tag: primary ? primary.next_tag : null, + semver_bump: overallBump, + blocked, + blocked_reason: blocked ? 'Major version jumps require human review before applying.' : null, + }, + rollback_target: primary ? buildRollbackTarget(primary.image, primary.current_tag) : null, + changelog: null, + }; +} + +export class UpdatePreviewService { + private static instance: UpdatePreviewService; + + public static getInstance(): UpdatePreviewService { + if (!UpdatePreviewService.instance) { + UpdatePreviewService.instance = new UpdatePreviewService(); + } + return UpdatePreviewService.instance; + } + + public async getPreview(nodeId: number, stackName: string): Promise { + const stackImages = await loadStackImages(nodeId, stackName); + if (stackImages.length === 0) { + return buildSummary(stackName, []); + } + + const docker = DockerController.getInstance(nodeId); + const deps: ComputePreviewDeps = { + getCredentials: (registry) => RegistryService.getInstance().getAuthForRegistry(registry), + getRemoteDigest, + listRegistryTags, + getLocalDigest: async (imageRef: string) => { + try { + const inspect = await docker.getDocker().getImage(imageRef).inspect(); + const repoDigests: string[] = inspect.RepoDigests ?? []; + for (const rd of repoDigests) { + if (!rd.includes('@sha256:')) continue; + const [, digest] = rd.split('@'); + return digest; + } + return null; + } catch { + return null; + } + }, + }; + + const results = await Promise.all( + stackImages.map(({ service, image }) => computeImagePreview(service, image, deps)), + ); + return buildSummary(stackName, results); + } +} diff --git a/backend/src/services/registry-api.ts b/backend/src/services/registry-api.ts new file mode 100644 index 00000000..5776436e --- /dev/null +++ b/backend/src/services/registry-api.ts @@ -0,0 +1,169 @@ +import https from 'https'; +import http from 'http'; + +export interface ParsedRef { + registry: string; + repo: string; + tag: string; +} + +export interface HttpResult { + statusCode: number; + headers: Record; + body: string; +} + +export interface RegistryCredentials { + username: string; + password: string; +} + +export function parseImageRef(imageRef: string): ParsedRef | null { + if (imageRef.startsWith('sha256:')) return null; + + const atIdx = imageRef.indexOf('@'); + if (atIdx !== -1) imageRef = imageRef.slice(0, atIdx); + + let registry = 'registry-1.docker.io'; + let rest = imageRef; + + const slashIdx = imageRef.indexOf('/'); + if (slashIdx !== -1) { + const firstPart = imageRef.slice(0, slashIdx); + if (firstPart.includes('.') || firstPart.includes(':') || firstPart === 'localhost') { + registry = firstPart; + rest = imageRef.slice(slashIdx + 1); + } + } + + let tag = 'latest'; + const colonIdx = rest.lastIndexOf(':'); + if (colonIdx > 0) { + tag = rest.slice(colonIdx + 1); + rest = rest.slice(0, colonIdx); + } + + if (registry === 'registry-1.docker.io' && !rest.includes('/')) { + rest = `library/${rest}`; + } + + return { registry, repo: rest, tag }; +} + +export function httpGet( + url: string, + headers: Record = {}, + timeoutMs = 10000, +): Promise { + return new Promise((resolve, reject) => { + const lib = url.startsWith('https:') ? https : http; + let settled = false; + const finish = (fn: () => void) => { + if (settled) return; + settled = true; + fn(); + }; + const req = lib.get(url, { headers }, (res) => { + let body = ''; + res.on('data', (chunk: Buffer) => { body += chunk.toString(); }); + res.on('end', () => finish(() => resolve({ + statusCode: res.statusCode ?? 0, + headers: res.headers as Record, + body, + }))); + res.on('error', (err) => finish(() => reject(err))); + }); + req.on('error', (err) => finish(() => reject(err))); + req.setTimeout(timeoutMs, () => { + const err = new Error('Request timed out'); + req.destroy(err); + finish(() => reject(err)); + }); + }); +} + +export async function getAuthToken( + registry: string, + repo: string, + credentials?: RegistryCredentials | null, +): Promise { + try { + const basicHeaders: Record = {}; + if (credentials) { + basicHeaders['Authorization'] = `Basic ${Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64')}`; + } + + let tokenUrl: string; + if (registry === 'registry-1.docker.io') { + tokenUrl = `https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull`; + } else { + const ping = await httpGet(`https://${registry}/v2/`, basicHeaders); + const wwwAuth = ping.headers['www-authenticate'] as string | undefined; + if (!wwwAuth) return null; + + const realmMatch = wwwAuth.match(/realm="([^"]+)"/); + const serviceMatch = wwwAuth.match(/service="([^"]+)"/); + const scopeMatch = wwwAuth.match(/scope="([^"]+)"/); + if (!realmMatch) return null; + + const params = new URLSearchParams(); + if (serviceMatch) params.set('service', serviceMatch[1]); + params.set('scope', scopeMatch ? scopeMatch[1] : `repository:${repo}:pull`); + tokenUrl = `${realmMatch[1]}?${params.toString()}`; + } + + const tokenRes = await httpGet(tokenUrl, basicHeaders); + if (tokenRes.statusCode !== 200) return null; + + const parsed = JSON.parse(tokenRes.body); + return parsed.token ?? parsed.access_token ?? null; + } catch { + return null; + } +} + +const MANIFEST_ACCEPT = [ + 'application/vnd.docker.distribution.manifest.list.v2+json', + 'application/vnd.docker.distribution.manifest.v2+json', + 'application/vnd.oci.image.index.v1+json', + 'application/vnd.oci.image.manifest.v1+json', +].join(', '); + +export async function getRemoteDigest( + registry: string, + repo: string, + tag: string, + credentials?: RegistryCredentials | null, +): Promise { + try { + const token = await getAuthToken(registry, repo, credentials); + const headers: Record = { Accept: MANIFEST_ACCEPT }; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const res = await httpGet(`https://${registry}/v2/${repo}/manifests/${tag}`, headers); + if (res.statusCode !== 200) return null; + return (res.headers['docker-content-digest'] as string) ?? null; + } catch { + return null; + } +} + +export async function listRegistryTags( + registry: string, + repo: string, + credentials?: RegistryCredentials | null, +): Promise { + try { + const token = await getAuthToken(registry, repo, credentials); + const headers: Record = { Accept: 'application/json' }; + if (token) headers['Authorization'] = `Bearer ${token}`; + + const res = await httpGet(`https://${registry}/v2/${repo}/tags/list`, headers); + if (res.statusCode !== 200) return []; + + const parsed = JSON.parse(res.body) as { tags?: string[] }; + return Array.isArray(parsed.tags) ? parsed.tags : []; + } catch { + return []; + } +} diff --git a/docs/features/auto-update-policies.mdx b/docs/features/auto-update-policies.mdx index e9a61e11..349b049e 100644 --- a/docs/features/auto-update-policies.mdx +++ b/docs/features/auto-update-policies.mdx @@ -1,194 +1,90 @@ --- -title: "Auto-Update Policies" -description: "Automatically check for and apply container image updates on a schedule." +title: "Auto-Update Readiness" +description: "Review pending container updates across your fleet, with risk tags, changelogs, and rollback targets, before applying." --- - Auto-Update Policies require a **Skipper** or **Admiral** license. + Auto-Update Readiness requires a **Skipper** or **Admiral** license. ## Overview -Auto-Update Policies let you define schedules for Sencho to automatically check your container images for updates and apply them when new versions are available. Think of it as a built-in Watchtower, integrated directly into your Sencho dashboard with full visibility into what was updated and when. +Auto-Update Readiness is the launchpad for every pending update across your stacks. Instead of a list of CRUD policies, the view surfaces one card per stack with an available update and tells you, at a glance, whether it is safe to apply. -Each policy targets a specific stack (or all stacks on a node) and runs on a cron schedule. When triggered, Sencho: +Each card shows: -1. Inspects every container in the target stack -2. Compares local image digests against the remote registry -3. If any image has a newer version, pulls the update and recreates the stack with `docker compose up -d` -4. Records the result in run history for auditability +- The current and next tag, with the new version highlighted in cyan. +- A **risk tag** derived from the version delta: `patch` (safe), `minor`, `major`, or `digest rebuild`. +- A one-line changelog preview when the registry publishes one. +- The **rollback target** (the tag Sencho will fall back to if an update is reverted). +- The next scheduled run for the matching auto-update task, if one exists. - Auto-Update Policies view showing the policies list + Readiness board with hero count, per-stack cards, and risk tags -## Creating a policy +The hero at the top counts pending updates fleet-wide and tells you how many of them are ready to apply without human review. Major version jumps and stacks with blocked registries are counted separately so they can be reviewed before the scheduler runs. -Navigate to **Auto-Update** in the sidebar and click **New Policy**. +## Workflow - - Create auto-update policy dialog - +1. Open the **Auto-Update** view from the sidebar. +2. Skim the card grid. Patch and minor bumps render with a green or amber tag; major bumps render in red and are marked as blocked. +3. For a safe update, click **Apply now** on the card to pull and recreate the stack immediately. +4. For a major bump, review the changelog preview and the rollback target before deciding. If you still want to apply it, switch to the **Schedules** view and create or edit an auto-update task for that stack. +5. Use **Recheck all** in the hero to force an immediate registry poll if you want to bypass the cached update status. -Fill in the following fields: +## Risk tags -| Field | Description | -|-------|-------------| -| **Name** | A descriptive name for the policy (e.g., "Nightly media stack update") | -| **Node** | The node where the target stack runs | -| **Stack** | The stack to monitor and update, or **All Stacks** to update every stack on the selected node | -| **Check Frequency** | A cron preset or custom cron expression defining how often to check | -| **Enabled** | Toggle to enable or disable the policy on creation | +| Tag | Meaning | Source | +|-----|---------|--------| +| **patch** | Safe automated update (e.g. `1.2.3` → `1.2.4`) | Semver comparison of tags | +| **minor** | Backwards-compatible update (e.g. `1.2.3` → `1.3.0`) | Semver comparison of tags | +| **major** | Potentially breaking change (e.g. `1.2.3` → `2.0.0`). Marked **blocked** by default | Semver comparison of tags | +| **digest rebuild** | Same tag, new image digest (e.g. `latest` pushed again) | Local vs remote digest diff | +| **unknown** | Non-semver tag (e.g. `main`, `stable`) | Fallback when tags cannot be compared | -The Stack selector becomes available after choosing a node. Selecting **All Stacks** will check and update every stack on that node during each run. +Blocked updates still schedule check runs, but the apply button is disabled until you review them manually. -### Schedule presets +## Scheduling auto-updates -For convenience, Sencho offers common schedule presets: +Auto-update is a first-class action in the Schedules view. To create a recurring check for a stack: -| Preset | Cron Expression | Description | -|--------|----------------|-------------| -| Every 6 hours | `0 */6 * * *` | Check four times per day | -| Every 12 hours | `0 */12 * * *` | Check twice per day | -| Daily at 3 AM | `0 3 * * *` | Low-traffic window for most users | -| Daily at midnight | `0 0 * * *` | Start of each day | -| Weekly (Sunday 3 AM) | `0 3 * * 0` | Minimal disruption for stable stacks | -| Custom | User-defined | Any valid cron expression | +1. Open **Schedules** in the top nav. +2. Click **New Schedule**. +3. Set **Action** to **Auto-update Stack**, choose the target node and stack, pick a cron expression, and save. -When using a preset, a human-readable description of the schedule is shown below the selector. Custom cron expressions are validated and described in real time. - -## Filtering by node - -In a multi-node environment, you can filter the policy list to show only policies targeting a specific node. Click the **calendar icon** on any node row in **Settings > Nodes** to jump directly to a filtered view. A filter badge at the top shows the active node, with a **Clear filter** button to return to the full list. - -## Managing policies - -The policy list is displayed as a table with the following columns: - -| Column | Description | -|--------|-------------| -| **Name** | The policy name | -| **Stack** | Target stack name, or "All Stacks" for wildcard policies | -| **Schedule** | Human-readable description with the raw cron expression below | -| **Status** | Last run result: **Success** (green), **Failed** (red), or "Never run" | -| **Last Run** | Timestamp of the most recent execution | -| **Next Run** | When the policy will next execute | -| **Enabled** | Toggle switch to enable or disable the policy | -| **Actions** | Action buttons (see below) | - -### Available actions - -Each policy row has four action buttons: - -- **Run Now** (play icon) - Trigger an immediate check-and-update cycle without waiting for the next scheduled run -- **Execution History** (clock icon) - Open the run history panel for this policy -- **Edit** (pencil icon) - Modify the policy name, target, or schedule -- **Delete** (trash icon) - Permanently remove the policy and all its execution history after confirmation - -## Run history - -Click the clock icon on any policy to open the run history panel. - - - Run history panel showing execution results - - -The history is displayed as a table with the following columns: - -| Column | Description | -|--------|-------------| -| **Time** | When the run started | -| **Source** | Whether the run was triggered by the **Scheduler** or **Manual** (via Run Now) | -| **Status** | Success, Failed, or Running | -| **Duration** | How long the run took (in seconds) | -| **Details** | Output summary or error message | - -Run history is paginated at 20 entries per page. You can export the full history as CSV using the download button in the panel header. +The task lives alongside restart, prune, snapshot, and scan tasks in the same timeline and table. Run history, notifications, and the Run Now button behave the same as for every other scheduled action. See [Scheduled Operations](/features/scheduled-operations) for details. ## Multi-node support -Auto-Update Policies work seamlessly across both local and remote nodes. When a policy targets a remote node, Sencho automatically proxies the update execution to the remote Sencho instance via the Distributed API. The remote node performs all image checks and compose updates locally on its own machine, then reports the results back. +The readiness board scopes to the active node selected in the sidebar, matching the scope of the auto-update schedule attached to each card. When a remote node is selected, Sencho proxies the registry checks and the apply call to the remote Sencho instance via the Distributed API. No additional configuration is needed. -No additional configuration is required. As long as your remote node is connected and reachable, auto-update policies will execute on it just like they do on the local node. +## How readiness is computed -## How it works +For each stack with a pending image update, Sencho computes a preview by: -Under the hood, Auto-Update Policies are built on the same scheduling engine as [Scheduled Operations](/features/scheduled-operations). The key difference is that auto-update policies: +1. Parsing the compose file to enumerate every pullable image reference. +2. Calling the registry with your configured credentials to fetch the current tag list and remote digest. +3. Picking the highest semver tag greater than the current tag (keeping the same prefix and suffix) or, if tags match but digests differ, treating it as a digest rebuild. +4. Scoring the overall stack by the most severe image bump. Any major bump marks the stack as blocked. +5. Deriving the rollback target from the running tag and normalizing Docker Hub library paths. -- Are available to **Skipper** tier (Scheduled Operations requires Admiral) -- Can target a single **stack** or **all stacks** on a node -- Perform a **check-then-update** flow rather than a blind restart - -### The check-then-update flow - -1. **Enumerate images** - Sencho lists all unique pullable images used by containers in the target stack -2. **Check digests** - For each image, Sencho compares the local digest against the remote registry manifest -3. **Conditional update** - Only if at least one image has a newer version does Sencho run `docker compose up -d` to pull and recreate -4. **Clear indicators** - After a successful update, the blue update indicator dot is automatically cleared -5. **Notify** - A notification is dispatched informing you which stack was updated and which images changed - -If no updates are found, the run completes with an "all images up to date" message and no containers are restarted. - -## Relationship to image update detection - -Sencho has two complementary features for keeping your images current: - -| Feature | Purpose | Tier | -|---------|---------|------| -| **Image Update Detection** | Passive: shows a blue dot on stacks with available updates | All tiers | -| **Auto-Update Policies** | Active: automatically applies updates on a schedule | Skipper+ | - -Image Update Detection runs in the background every 6 hours and highlights stacks that have newer images available. Auto-Update Policies take this a step further by automatically applying those updates based on your defined schedule. - -## Best practices - -- **Start with longer intervals** - Use "Daily at 3 AM" or "Weekly" for production stacks. Reserve shorter intervals for dev/staging environments. -- **Pin critical images** - If a stack uses `image: postgres:16.2` (pinned tag), auto-update will only detect updates to that exact tag. Use floating tags like `postgres:16` if you want minor version updates. -- **Monitor run history** - Check run history periodically to ensure updates are applying cleanly. Failed runs may indicate registry authentication issues or compose file problems. -- **Use "All Stacks" carefully** - Wildcard policies update every stack on the node. This is convenient for dev environments but may be too aggressive for production. -- **Combine with notifications** - Sencho sends alert notifications when auto-updates are applied, so you stay informed even when updates happen automatically. +The preview is recomputed each time the readiness board loads, so it reflects the live state of your registries and local images. ## Troubleshooting -### Policy reports "all images up to date" but I see an update elsewhere +### Card shows "No changelog available" -This usually means the registry check couldn't determine the remote digest. Common causes: +Sencho reads changelog metadata from the registry's manifest and OCI annotations. Registries that do not publish this metadata (most private registries and many self-hosted ones) will simply render the card without a changelog. The risk tag is still accurate because it is computed from the tag itself. -- **Private registry without credentials** - If your images are in a private registry, make sure you've added authentication credentials in **Settings > Registries**. Without valid credentials, Sencho can't query the remote manifest. -- **Network connectivity** - The Sencho instance needs outbound HTTPS access to the registry (e.g., `registry-1.docker.io`, `ghcr.io`). Firewall rules or proxy configurations may block these requests. -- **Digest-pinned images** - Images referenced by digest (`image: nginx@sha256:abc...`) are immutable by design. Sencho strips the pin for tag-based checking, but if your compose file only uses digest refs, consider switching to tag-based refs for auto-update support. +### Apply button is disabled with a "blocked" tooltip -Starting with this version, run history now distinguishes between "all images up to date" (clean check) and "image checks failed" (registry unreachable), so you can tell whether the check actually succeeded. +The stack has a major version bump. Open the cron schedule for that stack and apply manually after reviewing the upstream release notes. The block is a policy decision: major updates never auto-apply without human review. -### Policy keeps failing with "Target node offline" +### Card stays stuck on "Checking" -This means the remote Sencho instance was unreachable when the policy triggered. Verify: +The registry call is either still pending or failed. Click **Recheck all** in the hero to retry. If the stack has private-registry credentials, confirm they are still valid in **Settings > Registries**. -- The remote node's Sencho instance is running -- The API URL and token in **Settings > Nodes** are correct -- The remote node has not changed IP address or port +### "Nothing to update" but I see an update on another view -### Policy shows "no containers found" - -This warning appears when a policy targets a specific stack that has no running containers. The stack may have been: - -- Stopped or removed since the policy was created -- Renamed (stack names are directory-based) - -Check that the target stack exists and has at least one running container. For wildcard ("All Stacks") policies, empty stacks are silently skipped without a warning. - -### "Run Now" finishes instantly - -This is expected behavior. The Run Now button triggers the update check in the background and returns immediately. The actual check runs asynchronously; check the run history panel to see the result once it completes. - -### Policy was automatically disabled - -If a policy's cron expression becomes invalid after creation, the scheduler disables the policy and records the reason in the status column. To fix this: - -1. Open the policy in edit mode. -2. Re-enter a valid cron expression (or choose a preset). -3. Re-enable the policy using the toggle switch. - -### Run shows "Server restarted during execution" - -This means Sencho was restarted while this policy's update check was mid-execution. The run was marked as failed automatically on startup. The policy itself is still enabled and will run at its next scheduled time. Use **Run Now** if you want to trigger it immediately. +Image update detection runs every six hours. The readiness board uses the same cached status. Trigger **Recheck all** to force a fresh check, or see [Image update detection](/features/image-update-detection) for details on the refresh cycle. diff --git a/docs/features/scheduled-operations.mdx b/docs/features/scheduled-operations.mdx index 2a126bb4..620b07e8 100644 --- a/docs/features/scheduled-operations.mdx +++ b/docs/features/scheduled-operations.mdx @@ -4,26 +4,41 @@ description: Automate recurring Docker operations like stack restarts, fleet sna --- - Scheduled Operations requires a Sencho **Admiral** license. - Skipper and Community Edition do not include this feature. + Scheduled Operations requires a Sencho **Admiral** license. Skipper users see only the **Auto-update Stack** action; Admiral users see every action. ## Overview Scheduled Operations lets you automate recurring maintenance tasks across your infrastructure. Define a cron schedule, choose an action, and Sencho handles the rest, including a full execution history log so you always know what ran and when. +The view opens on a **Timeline** that plots the next 24 hours of scheduled work across four lanes (Restart, Update, Scan, Prune) so you can see, at a glance, what is about to fire and when. Toggle to **All tasks** for the full CRUD table. + - Scheduled operations list view showing tasks with status, schedule, and actions + Schedules timeline showing the next 24 hours across four lanes with a cyan now rail +## Timeline view + +The timeline is the default view. It shows: + +- A hero with the current 24-hour window as a date range, and the **next firing** on the right (time, task name, and relative countdown). +- Four color-coded lanes: **Restart** (cyan), **Update** (green), **Scan** (purple), and **Prune** (amber). Snapshot tasks share the Prune lane. +- One pill per firing within the window, positioned proportionally to the task's next run time. Click any pill to open that task's execution history. +- A vertical cyan **now rail** at the left edge, and six mono time ticks along the bottom axis. + +Tasks that fire more than once in the window (e.g. an hourly cron) render a pill for each firing. Disabled tasks do not appear on the timeline. + +Toggle to **All tasks** from the header to see every schedule in a table, regardless of whether it fires in the next 24 hours. + ## Supported Actions | Action | Target | Description | |--------|--------|-------------| | **Restart Stack** | A specific stack (or specific services within it) on a specific node | Restarts all or selected containers in the stack | +| **Auto-update Stack** | A specific stack on a specific node | Checks each image for updates and recreates the stack if any image has a newer version. See [Auto-Update Readiness](/features/auto-update-policies) for the companion board. Available on Skipper and Admiral. | | **Fleet Snapshot** | All nodes | Creates a fleet-wide backup of all compose files and `.env` files | | **System Prune** | The default node | Prunes selected resources, optionally filtered by Docker label | -| **Vulnerability Scan** | All images on a specific node | Runs Trivy against every image on the target node and records the results. Requires Trivy to be installed — see [Installing Trivy](/operations/trivy-setup). Available on Skipper and Admiral. | +| **Vulnerability Scan** | All images on a specific node | Runs Trivy against every image on the target node and records the results. Requires Trivy to be installed, see [Installing Trivy](/operations/trivy-setup). Available on Skipper and Admiral. | ## Creating a Scheduled Task @@ -31,9 +46,9 @@ Scheduled Operations lets you automate recurring maintenance tasks across your i 2. Click **New Schedule**. 3. Fill in the form: - **Name**: A descriptive label (e.g. "Nightly staging restart"). - - **Action**: Choose Restart Stack, Fleet Snapshot, System Prune, or Vulnerability Scan. The form fields below change based on your selection. - - **Node**: (Restart Stack and Vulnerability Scan) Select the node to run against. For Restart Stack it determines where the target stack lives; for Vulnerability Scan it determines which node's images are scanned. - - **Stack**: (Restart Stack only) Select the stack to restart. Becomes available after choosing a node. + - **Action**: Choose Restart Stack, Auto-update Stack, Fleet Snapshot, System Prune, or Vulnerability Scan. The form fields below change based on your selection. + - **Node**: (Restart Stack, Auto-update Stack, and Vulnerability Scan) Select the node to run against. For stack actions it determines where the target stack lives; for Vulnerability Scan it determines which node's images are scanned. + - **Stack**: (Restart Stack and Auto-update Stack) Select the stack to target. Becomes available after choosing a node. - **Services**: (Restart Stack only) Optionally select specific services within the stack to restart. Leave empty to restart all services. - **Prune Targets**: (System Prune only) Select which resources to prune: containers, images, networks, volumes. All are selected by default. - **Label Filter**: (System Prune only) Optionally filter prune operations to resources matching a specific Docker label (e.g. `com.docker.compose.project=mystack`). @@ -182,7 +197,7 @@ If the server restarts while a task is mid-execution, the orphaned run record is ### Task was automatically disabled -If a task's cron expression becomes invalid after creation (for example, due to a corrupt database edit or an expression that was valid in a previous version), the scheduler disables the task and records the reason in the last error column. To fix this: +If a task's cron expression becomes invalid after creation (for example, due to a corrupt database edit), the scheduler disables the task and records the reason in the last error column. To fix this: 1. Open the task in edit mode. 2. Re-enter a valid cron expression. diff --git a/docs/images/auto-update/readiness-board.png b/docs/images/auto-update/readiness-board.png new file mode 100644 index 0000000000000000000000000000000000000000..786fe4c02eeecc596f03d1a34d045f11f93bb9bc GIT binary patch literal 101378 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
    OH%r85!Ylv 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⁣q&tDhC#|R&3gPbjmljZ@P;O6TU4OX< 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 literal 0 HcmV?d00001 diff --git a/docs/images/scheduled-operations/timeline.png b/docs/images/scheduled-operations/timeline.png new file mode 100644 index 0000000000000000000000000000000000000000..95243f772238b430380c36b990154c12cc6c9155 GIT binary patch literal 66057 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 E7p9;f void; -} - -function AutoUpdatePoliciesContent({ filterNodeId, onClearFilter }: AutoUpdatePoliciesProps) { - const [policies, setPolicies] = useState([]); - const [loading, setLoading] = useState(true); - const [dialogOpen, setDialogOpen] = useState(false); - const [editingPolicy, setEditingPolicy] = useState(null); - const [deleteTarget, setDeleteTarget] = useState(null); - const [runsTask, setRunsTask] = useState(null); - const [runs, setRuns] = useState([]); - const [runsLoading, setRunsLoading] = useState(false); - - // Form state - const [formName, setFormName] = useState(''); - const [formTargetId, setFormTargetId] = useState(''); - const [formNodeId, setFormNodeId] = useState(''); - const [formCron, setFormCron] = useState('0 3 * * *'); - const [formCronPreset, setFormCronPreset] = useState('0 3 * * *'); - const [formEnabled, setFormEnabled] = useState(true); - const [saving, setSaving] = useState(false); - const [runningPolicies, setRunningPolicies] = useState>(new Set()); - const [runsPage, setRunsPage] = useState(1); - const [runsTotal, setRunsTotal] = useState(0); - const runsLimit = 20; - - // Available stacks and nodes - const [stacks, setStacks] = useState([]); - const [nodes, setNodes] = useState([]); - - const filteredPolicies = filterNodeId != null - ? policies.filter(p => p.node_id === filterNodeId) - : policies; - const filterNodeName = filterNodeId != null - ? nodes.find(n => n.id === filterNodeId)?.name - : null; - - const fetchPolicies = useCallback(async () => { - setLoading(true); - try { - const res = await apiFetch('/scheduled-tasks?action=update', { localOnly: true }); - if (res.ok) { - setPolicies(await res.json()); - } - } catch { - // Non-critical - } finally { - setLoading(false); - } - }, []); - - const fetchStacks = useCallback(async (nodeId?: string, signal?: AbortSignal) => { - try { - const res = nodeId - ? await fetchForNode('/stacks', parseInt(nodeId, 10), { signal }) - : await apiFetch('/stacks', { signal }); - if (res.ok) setStacks(await res.json()); - else setStacks([]); - } catch { - if (!signal?.aborted) setStacks([]); - } - }, []); - - const fetchNodes = useCallback(async () => { - try { - const res = await apiFetch('/nodes', { localOnly: true }); - if (res.ok) { - const data = await res.json(); - setNodes(data.map((n: { id: number; name: string }) => ({ id: n.id, name: n.name }))); - } - } catch { /* Non-critical */ } - }, []); - - useEffect(() => { - fetchPolicies(); - fetchStacks(); - fetchNodes(); - }, [fetchPolicies, fetchStacks, fetchNodes]); - - // Re-fetch stacks when selected node changes in the dialog - useEffect(() => { - if (!dialogOpen || !formNodeId) return; - const controller = new AbortController(); - fetchStacks(formNodeId, controller.signal); - setFormTargetId(''); - return () => controller.abort(); - }, [formNodeId, dialogOpen, fetchStacks]); - - const openCreate = () => { - setEditingPolicy(null); - setFormName(''); - setFormTargetId(''); - setFormNodeId(filterNodeId != null ? String(filterNodeId) : ''); - setFormCron('0 3 * * *'); - setFormCronPreset('0 3 * * *'); - setFormEnabled(true); - setDialogOpen(true); - if (filterNodeId != null) { - fetchStacks(String(filterNodeId)); - } - }; - - const openEdit = (policy: ScheduledTask) => { - setEditingPolicy(policy); - setFormName(policy.name); - setFormTargetId(policy.target_id || ''); - setFormNodeId(policy.node_id != null ? String(policy.node_id) : ''); - setFormCron(policy.cron_expression); - const matchingPreset = CRON_PRESETS.find(p => p.value === policy.cron_expression); - setFormCronPreset(matchingPreset ? matchingPreset.value : 'custom'); - setFormEnabled(policy.enabled === 1); - setDialogOpen(true); - }; - - const handleSave = async () => { - const body: Record = { - name: formName.trim(), - target_type: 'stack', - action: 'update', - target_id: formTargetId, - node_id: formNodeId ? parseInt(formNodeId, 10) : null, - cron_expression: formCron, - enabled: formEnabled, - }; - - setSaving(true); - try { - const res = editingPolicy - ? await apiFetch(`/scheduled-tasks/${editingPolicy.id}`, { method: 'PUT', body: JSON.stringify(body), localOnly: true }) - : await apiFetch('/scheduled-tasks', { method: 'POST', body: JSON.stringify(body), localOnly: true }); - - if (res.ok) { - toast.success(editingPolicy ? 'Policy updated' : 'Policy created'); - setDialogOpen(false); - fetchPolicies(); - } else { - const data = await res.json().catch(() => ({})); - toast.error(data?.error || 'Failed to save policy'); - } - } catch (error: unknown) { - const msg = error instanceof Error ? error.message : 'Something went wrong.'; - toast.error(msg); - } finally { - setSaving(false); - } - }; - - const handleToggle = async (policy: ScheduledTask) => { - try { - const res = await apiFetch(`/scheduled-tasks/${policy.id}/toggle`, { method: 'PATCH', localOnly: true }); - if (res.ok) { - fetchPolicies(); - } else { - const data = await res.json().catch(() => ({})); - toast.error(data?.error || 'Failed to toggle policy'); - } - } catch { - toast.error('Something went wrong.'); - } - }; - - const handleDelete = async () => { - if (!deleteTarget) return; - try { - const res = await apiFetch(`/scheduled-tasks/${deleteTarget.id}`, { method: 'DELETE', localOnly: true }); - if (res.ok) { - toast.success('Policy deleted'); - setDeleteTarget(null); - fetchPolicies(); - } else { - const data = await res.json().catch(() => ({})); - toast.error(data?.error || 'Failed to delete policy'); - } - } catch { - toast.error('Something went wrong.'); - } - }; - - const openRuns = async (task: ScheduledTask, page = 1) => { - setRunsTask(task); - setRunsPage(page); - setRunsLoading(true); - const offset = (page - 1) * runsLimit; - try { - const res = await apiFetch(`/scheduled-tasks/${task.id}/runs?limit=${runsLimit}&offset=${offset}`, { localOnly: true }); - if (res.ok) { - const data = await res.json(); - setRuns(data.runs); - setRunsTotal(data.total); - } - } catch { /* Non-critical */ } - finally { setRunsLoading(false); } - }; - - const handleRunNow = async (policy: ScheduledTask) => { - setRunningPolicies(prev => new Set(prev).add(policy.id)); - try { - const res = await apiFetch(`/scheduled-tasks/${policy.id}/run`, { method: 'POST', localOnly: true }); - if (res.ok) { - toast.success(`Checking for updates on "${policy.target_id}"...`); - fetchPolicies(); - } else { - const data = await res.json().catch(() => ({})); - toast.error(data?.error || 'Failed to run policy'); - } - } catch { - toast.error('Something went wrong.'); - } finally { - setRunningPolicies(prev => { - const next = new Set(prev); - next.delete(policy.id); - return next; - }); - } - }; - - const cronDescription = getCronDescription(formCron); - - return ( -
    - - -
    -
    - - Auto-Update Policies -
    -
    - - -
    -
    -

    - Automatically check for new images and update your stacks on a schedule. -

    -
    - - {filterNodeId != null && filterNodeName && ( -
    - - Filtered to node: {filterNodeName} - - -
    - )} - {loading && filteredPolicies.length === 0 ? ( -
    Loading...
    - ) : filteredPolicies.length === 0 ? ( -
    - {filterNodeId != null - ? 'No auto-update policies for this node. Create one to keep your stacks up to date automatically.' - : 'No auto-update policies yet. Create one to keep your stacks up to date automatically.'} -
    - ) : ( - - - - Name - Stack - Schedule - Status - Last Run - Next Run - Enabled - Actions - - - - {filteredPolicies.map((policy) => ( - - {policy.name} - {policy.target_id === '*' ? 'All Stacks' : policy.target_id} - -
    {getCronDescription(policy.cron_expression)}
    -
    {policy.cron_expression}
    -
    - - {policy.last_status === 'success' ? ( - Success - ) : policy.last_status === 'failure' ? ( - Failed - ) : ( - Never run - )} - - - {formatTimestamp(policy.last_run_at)} - - - {formatTimestamp(policy.next_run_at)} - - - handleToggle(policy)} - /> - - -
    - - - - -
    -
    -
    - ))} -
    -
    - )} -
    -
    - - {/* Create/Edit Dialog */} - - - - {editingPolicy ? 'Edit Auto-Update Policy' : 'New Auto-Update Policy'} - Configure an auto-update policy for a stack. - -
    -
    - - setFormName(e.target.value)} /> -
    - -
    - - ({ value: String(n.id), label: n.name }))} - value={formNodeId} - onValueChange={setFormNodeId} - placeholder="Select node..." - searchPlaceholder="Search nodes..." - emptyText="No nodes found." - /> -
    - -
    - - ({ value: s, label: s })), - ]} - value={formTargetId} - onValueChange={setFormTargetId} - placeholder={formNodeId ? "Select stack..." : "Select a node first"} - searchPlaceholder="Search stacks..." - emptyText="No stacks found." - disabled={!formNodeId} - /> -
    - -
    - - ({ value: p.value, label: p.label }))} - value={formCronPreset} - onValueChange={(val) => { - setFormCronPreset(val); - if (val !== 'custom') setFormCron(val); - }} - placeholder="Select frequency..." - searchPlaceholder="Search frequencies..." - emptyText="No matching frequency." - /> - {formCronPreset === 'custom' && ( - setFormCron(e.target.value)} - className="font-mono" - /> - )} -

    {cronDescription}

    -
    - -
    - - -
    -
    - - - - -
    -
    - - {/* Delete Confirmation */} - { if (!open) setDeleteTarget(null); }}> - - - Delete Auto-Update Policy - - Are you sure you want to delete “{deleteTarget?.name}”? This will also remove all execution history. This action cannot be undone. - - - - Cancel - - Delete - - - - - - {/* Run History Sheet */} - { if (!open) setRunsTask(null); }}> - - -
    - Update History - {runsTask?.name} - {runsTask && runs.length > 0 && ( - - )} -
    -
    - -
    - {runsLoading ? ( -
    Loading...
    - ) : runs.length === 0 ? ( -
    No executions yet.
    - ) : ( - <> - - - - Time - Source - Status - Duration - Details - - - - {runs.map((run) => { - const duration = run.completed_at && run.started_at - ? `${((run.completed_at - run.started_at) / 1000).toFixed(1)}s` - : '-'; - return ( - - - {new Date(run.started_at).toLocaleString()} - - - - {run.triggered_by === 'manual' ? 'Manual' : 'Scheduled'} - - - - {run.status === 'success' ? ( - Success - ) : run.status === 'failure' ? ( - Failed - ) : ( - Running - )} - - {duration} - - {run.error || run.output || '-'} - - - ); - })} - -
    - {Math.ceil(runsTotal / runsLimit) > 1 && runsTask && ( -
    -

    - Page {runsPage} of {Math.ceil(runsTotal / runsLimit)} -

    -
    - - -
    -
    - )} - - )} -
    -
    -
    -
    -
    - ); -} - -export default function AutoUpdatePoliciesView({ filterNodeId, onClearFilter }: AutoUpdatePoliciesProps) { - return ( - - - - ); -} diff --git a/frontend/src/components/AutoUpdateReadinessView.tsx b/frontend/src/components/AutoUpdateReadinessView.tsx new file mode 100644 index 00000000..c2fa4b51 --- /dev/null +++ b/frontend/src/components/AutoUpdateReadinessView.tsx @@ -0,0 +1,468 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card } from '@/components/ui/card'; +import { RefreshCw, Shield, AlertTriangle, ShieldAlert, Clock, Play, CalendarClock } from 'lucide-react'; +import { toast } from '@/components/ui/toast-store'; +import { apiFetch } from '@/lib/api'; +import { PaidGate } from '@/components/PaidGate'; +import { useNodes } from '@/context/NodeContext'; +import type { ScheduledTask } from '@/types/scheduling'; + +type SemverBump = 'none' | 'patch' | 'minor' | 'major' | 'unknown'; + +interface UpdatePreviewImage { + service: string; + image: string; + current_tag: string; + next_tag: string | null; + has_update: boolean; + semver_bump: SemverBump; +} + +interface UpdatePreview { + stack_name: string; + images: UpdatePreviewImage[]; + summary: { + has_update: boolean; + primary_image: string | null; + current_tag: string | null; + next_tag: string | null; + semver_bump: SemverBump; + blocked: boolean; + blocked_reason: string | null; + }; + rollback_target: string | null; + changelog: string | null; +} + +interface StackCard { + stack: string; + preview: UpdatePreview | null; + previewLoaded: boolean; + scheduledTask: ScheduledTask | null; + applying: boolean; +} + +function formatRelative(ts: number | null): string { + if (ts == null) return ''; + const delta = ts - Date.now(); + if (delta <= 0) return 'due now'; + const mins = Math.round(delta / 60_000); + if (mins < 60) return `in ${mins}m`; + const hours = Math.floor(mins / 60); + const remMins = mins % 60; + if (hours < 24) return remMins > 0 ? `in ${hours}h ${remMins}m` : `in ${hours}h`; + const days = Math.floor(hours / 24); + const remHours = hours % 24; + return remHours > 0 ? `in ${days}d ${remHours}h` : `in ${days}d`; +} + +function formatClock(ts: number | null): string { + if (ts == null) return ''; + return new Date(ts).toLocaleString(undefined, { + weekday: 'short', + hour: '2-digit', + minute: '2-digit', + }); +} + +function RiskBadge({ bump, blocked }: { bump: SemverBump; blocked: boolean }) { + if (blocked || bump === 'major') { + return ( + + + Blocked · major + + ); + } + if (bump === 'minor') { + return ( + + + Review · minor + + ); + } + if (bump === 'patch') { + return ( + + + Safe · patch + + ); + } + if (bump === 'unknown') { + return ( + + Digest rebuild + + ); + } + return ( + + None + + ); +} + +function VersionDiff({ current, next }: { current: string | null; next: string | null }) { + if (!current) return null; + const changed = next && next !== current; + return ( +
    + {current} + + + {next ?? current} + +
    + ); +} + +function StackReadinessCard({ + card, + onApply, +}: { + card: StackCard; + onApply: (stack: string) => void; +}) { + const { stack, preview, previewLoaded, scheduledTask, applying } = card; + const loading = !previewLoaded; + const failed = previewLoaded && preview === null; + const blocked = preview?.summary.blocked ?? false; + const bump = preview?.summary.semver_bump ?? 'none'; + const updatingImageCount = preview?.images.filter(i => i.has_update).length ?? 0; + const nextRun = scheduledTask?.next_run_at ?? null; + + return ( + +
    +
    + + Stack + + + {stack} + +
    + {previewLoaded && preview && } +
    + + {loading ? ( +
    Checking registry...
    + ) : failed ? ( +
    + Preview failed. Registry may be unreachable. +
    + ) : ( + (() => { + const p = preview!; + const blockedReason = p.summary.blocked_reason; + return ( + <> + + +
    + {p.summary.primary_image ?? '-'} + {updatingImageCount > 1 && ( + + · {updatingImageCount} services + + )} +
    + +
    + {p.changelog ?? 'No changelog available from the registry yet.'} +
    + + {blocked && blockedReason && ( +
    + {blockedReason} +
    + )} + +
    +
    + {nextRun ? ( + <> +
    + +
    + + ); + })() + )} +
    + ); +} + +function ReadinessHero({ + total, + ready, + refreshing, + onRefresh, +}: { + total: number; + ready: number; + refreshing: boolean; + onRefresh: () => void; +}) { + const headline = total === 0 + ? 'Everything is up to date' + : total === 1 + ? '1 update pending' + : `${total} updates pending`; + + return ( +
    +
    +
    +
    +
    + + Fleet readiness + + + {headline} + + {total > 0 && ( + + {ready} of {total} ready to apply automatically + {total - ready > 0 ? ` · ${total - ready} blocked by major bump` : ''} + + )} +
    +
    + {total > 0 && ( +
    +
    + {ready} / {total} +
    +
    + Ready +
    +
    + )} + +
    +
    +
    + ); +} + +function AutoUpdateReadinessContent() { + const { activeNode } = useNodes(); + const [cards, setCards] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const refreshTimerRef = useRef | null>(null); + // Monotonic token guards against stale setCards from older node-scoped fetches. + const loadTokenRef = useRef(0); + + const loadReadiness = useCallback(async () => { + const token = ++loadTokenRef.current; + const currentNodeId = activeNode?.id ?? null; + setLoading(true); + try { + const [statusRes, tasksRes] = await Promise.all([ + apiFetch('/image-updates'), + apiFetch('/scheduled-tasks?action=update', { localOnly: true }), + ]); + if (token !== loadTokenRef.current) return; + + if (!statusRes.ok) { + throw new Error('Failed to load image update status'); + } + const statuses = await statusRes.json() as Record; + const stacksWithUpdates = Object.entries(statuses) + .filter(([, hasUpdate]) => hasUpdate) + .map(([stack]) => stack) + .sort(); + + const tasks: ScheduledTask[] = tasksRes.ok ? await tasksRes.json() : []; + const taskByStack = new Map(); + for (const t of tasks) { + // Match tasks targeting this stack on this node. Tasks with node_id=null + // are local-node-scoped and only apply when viewing the local node. + const matchesNode = currentNodeId != null + ? (t.node_id === currentNodeId || (t.node_id == null && activeNode?.type === 'local')) + : t.node_id == null; + if (t.target_type === 'stack' && t.target_id && matchesNode) { + const existing = taskByStack.get(t.target_id); + if (!existing || (t.next_run_at ?? Infinity) < (existing.next_run_at ?? Infinity)) { + taskByStack.set(t.target_id, t); + } + } + } + + const initial: StackCard[] = stacksWithUpdates.map(stack => ({ + stack, + preview: null, + previewLoaded: false, + scheduledTask: taskByStack.get(stack) ?? null, + applying: false, + })); + if (token !== loadTokenRef.current) return; + setCards(initial); + + const previews = await Promise.all( + stacksWithUpdates.map(async (stack) => { + try { + const res = await apiFetch(`/stacks/${encodeURIComponent(stack)}/update-preview`); + if (!res.ok) return null; + return await res.json() as UpdatePreview; + } catch { + return null; + } + }), + ); + if (token !== loadTokenRef.current) return; + + setCards(stacksWithUpdates.map((stack, idx) => ({ + stack, + preview: previews[idx], + previewLoaded: true, + scheduledTask: taskByStack.get(stack) ?? null, + applying: false, + }))); + } catch (err) { + if (token !== loadTokenRef.current) return; + toast.error((err as Error)?.message || 'Failed to load readiness'); + } finally { + if (token === loadTokenRef.current) setLoading(false); + } + }, [activeNode?.id, activeNode?.type]); + + useEffect(() => { + loadReadiness(); + return () => { + // Invalidate any in-flight fetch and cancel pending refresh timers on unmount/node-change. + loadTokenRef.current++; + if (refreshTimerRef.current) { + clearTimeout(refreshTimerRef.current); + refreshTimerRef.current = null; + } + }; + }, [loadReadiness]); + + const handleRefresh = useCallback(async () => { + setRefreshing(true); + try { + const res = await apiFetch('/image-updates/refresh', { method: 'POST' }); + if (res.status === 429) { + const data = await res.json().catch(() => ({ error: 'Rate limited' })); + toast.warning(data.error ?? 'Please wait before rechecking'); + return; + } + if (!res.ok) { + toast.error('Failed to trigger refresh'); + return; + } + toast.success('Checking registries for updates...'); + if (refreshTimerRef.current) clearTimeout(refreshTimerRef.current); + refreshTimerRef.current = setTimeout(() => { + refreshTimerRef.current = null; + loadReadiness(); + }, 2500); + } catch (err) { + toast.error((err as Error)?.message || 'Failed to trigger refresh'); + } finally { + setRefreshing(false); + } + }, [loadReadiness]); + + const handleApply = useCallback(async (stack: string) => { + setCards(prev => prev.map(c => c.stack === stack ? { ...c, applying: true } : c)); + const loadingId = toast.loading(`Applying update to ${stack}...`); + try { + const res = await apiFetch(`/stacks/${encodeURIComponent(stack)}/update`, { method: 'POST' }); + if (!res.ok) { + const data = await res.json().catch(() => ({ error: 'Update failed' })); + throw new Error(data.error ?? 'Update failed'); + } + toast.success(`${stack} updated successfully`); + setCards(prev => prev.filter(c => c.stack !== stack)); + } catch (err) { + toast.error((err as Error)?.message || 'Update failed'); + setCards(prev => prev.map(c => c.stack === stack ? { ...c, applying: false } : c)); + } finally { + toast.dismiss(loadingId); + } + }, []); + + const { total, ready } = useMemo(() => { + const t = cards.length; + const r = cards.filter(c => c.previewLoaded && c.preview !== null && !c.preview.summary.blocked).length; + return { total: t, ready: r }; + }, [cards]); + + return ( +
    + + + {loading && cards.length === 0 ? ( +
    + Loading readiness... +
    + ) : cards.length === 0 ? ( +
    +
    + ) : ( +
    + {cards.map(card => ( + + ))} +
    + )} +
    + ); +} + +export default function AutoUpdateReadinessView() { + return ( + + + + ); +} diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 95c77350..5da9965c 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -54,7 +54,7 @@ import { GlobalObservabilityView } from './GlobalObservabilityView'; import { FleetView } from './FleetView'; import { AuditLogView } from './AuditLogView'; import ScheduledOperationsView from './ScheduledOperationsView'; -import AutoUpdatePoliciesView from './AutoUpdatePoliciesView'; +import AutoUpdateReadinessView from './AutoUpdateReadinessView'; import { SecurityHistoryView } from './SecurityHistoryView'; import { SENCHO_NAVIGATE_EVENT } from './NodeManager'; import type { SenchoNavigateDetail } from './NodeManager'; @@ -2779,8 +2779,8 @@ export default function EditorLayout() { ) : activeView === 'auto-updates' ? ( - - setFilterNodeId(null)} /> + + ) : activeView === 'scheduled-ops' ? ( diff --git a/frontend/src/components/ScheduledOperationsView.tsx b/frontend/src/components/ScheduledOperationsView.tsx index 1e5d443b..5823208d 100644 --- a/frontend/src/components/ScheduledOperationsView.tsx +++ b/frontend/src/components/ScheduledOperationsView.tsx @@ -11,7 +11,7 @@ import { ScrollArea } from '@/components/ui/scroll-area'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; -import { Clock, Plus, Pencil, Trash2, History, RefreshCw, Play, ChevronLeft, ChevronRight, Download } from 'lucide-react'; +import { Clock, Plus, Pencil, Trash2, History, RefreshCw, Play, ChevronLeft, ChevronRight, Download, CalendarClock, Table2 } from 'lucide-react'; import { toast } from '@/components/ui/toast-store'; import { apiFetch, fetchForNode } from '@/lib/api'; import { Combobox } from '@/components/ui/combobox'; @@ -20,11 +20,37 @@ import { getCronDescription, formatTimestamp } from '@/lib/scheduling'; const ACTION_OPTIONS = [ { value: 'restart', label: 'Restart Stack', targetType: 'stack' as const }, + { value: 'update', label: 'Auto-update Stack', targetType: 'stack' as const }, { value: 'snapshot', label: 'Fleet Snapshot', targetType: 'fleet' as const }, { value: 'prune', label: 'System Prune', targetType: 'system' as const }, { value: 'scan', label: 'Vulnerability Scan', targetType: 'system' as const }, ]; +const TIMELINE_LANES: { key: ScheduledTask['action']; label: string; color: string; bg: string; actions: ScheduledTask['action'][] }[] = [ + { key: 'restart', label: 'Restart', color: 'var(--brand)', bg: 'oklch(from var(--brand) l c h / 0.18)', actions: ['restart'] }, + { key: 'update', label: 'Update', color: 'var(--success)', bg: 'oklch(from var(--success) l c h / 0.18)', actions: ['update'] }, + { key: 'scan', label: 'Scan', color: 'var(--label-purple)', bg: 'var(--label-purple-bg)', actions: ['scan'] }, + { key: 'prune', label: 'Prune', color: 'var(--warning)', bg: 'oklch(from var(--warning) l c h / 0.18)', actions: ['prune', 'snapshot'] }, +]; + +const TIMELINE_WINDOW_HOURS = 24; +const TIMELINE_WINDOW_MS = TIMELINE_WINDOW_HOURS * 60 * 60 * 1000; + +function formatHourTick(ts: number): string { + const d = new Date(ts); + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; +} + +function formatRelative(ts: number, now: number): string { + const diff = ts - now; + if (diff <= 0) return 'now'; + const mins = Math.round(diff / 60000); + if (mins < 60) return `in ${mins}m`; + const hours = Math.floor(mins / 60); + const remMins = mins % 60; + return remMins === 0 ? `in ${hours}h` : `in ${hours}h ${remMins}m`; +} + interface ScheduledOperationsViewProps { filterNodeId?: number | null; onClearFilter?: () => void; @@ -33,6 +59,8 @@ interface ScheduledOperationsViewProps { export default function ScheduledOperationsView({ filterNodeId, onClearFilter }: ScheduledOperationsViewProps) { const [tasks, setTasks] = useState([]); const [loading, setLoading] = useState(true); + const [view, setView] = useState<'timeline' | 'table'>('timeline'); + const [now, setNow] = useState(() => Date.now()); const [dialogOpen, setDialogOpen] = useState(false); const [editingTask, setEditingTask] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); @@ -71,7 +99,7 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }: const fetchTasks = useCallback(async () => { setLoading(true); try { - const res = await apiFetch('/scheduled-tasks?exclude_action=update', { localOnly: true }); + const res = await apiFetch('/scheduled-tasks', { localOnly: true }); if (res.ok) { setTasks(await res.json()); } @@ -113,6 +141,11 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }: fetchNodes(); }, [fetchTasks, fetchStacks, fetchNodes]); + useEffect(() => { + const id = setInterval(() => setNow(Date.now()), 60_000); + return () => clearInterval(id); + }, []); + useEffect(() => { if (formAction !== 'restart' || !formTargetId) { setAvailableServices([]); @@ -301,6 +334,18 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }: const targetType = ACTION_OPTIONS.find(a => a.value === formAction)?.targetType; const cronDescription = getCronDescription(formCron); + const windowEnd = now + TIMELINE_WINDOW_MS; + const timelinePills = filteredTasks + .filter(t => t.enabled === 1 && t.next_runs && t.next_runs.length > 0) + .flatMap(task => (task.next_runs ?? []).map(runAt => ({ task, runAt }))) + .filter(p => p.runAt >= now && p.runAt <= windowEnd) + .sort((a, b) => a.runAt - b.runAt); + + const nextPill = timelinePills[0] ?? null; + const hourTicks = Array.from({ length: 6 }, (_, i) => now + (i / 5) * TIMELINE_WINDOW_MS); + const windowStartLabel = new Date(now).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); + const windowEndLabel = new Date(windowEnd).toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' }); + return (
    @@ -311,6 +356,26 @@ export default function ScheduledOperationsView({ filterNodeId, onClearFilter }: Scheduled Operations
    +
    + + +
    )} - {loading && filteredTasks.length === 0 ? ( + {view === 'timeline' ? ( +
    +
    +
    +
    + Next 24 hours +
    +
    + Next 24 hours +
    +
    + {windowStartLabel} {formatHourTick(now)} → {windowEndLabel} {formatHourTick(windowEnd)} +
    +
    + {nextPill ? ( +
    +
    + Next +
    +
    + {formatHourTick(nextPill.runAt)} +
    +
    + {nextPill.task.name} · {formatRelative(nextPill.runAt, now)} +
    +
    + ) : ( +
    +
    + Next +
    +
    + --:-- +
    +
    + Nothing scheduled +
    +
    + )} +
    + + {loading && filteredTasks.length === 0 ? ( +
    Loading...
    + ) : ( +
    +
    + {TIMELINE_LANES.map(lane => { + const lanePills = timelinePills.filter(p => lane.actions.includes(p.task.action)); + return ( +
    +
    +
    +
    + {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; + return ( + + ); + })} +
    +
    + ); + })} +
    + {/* Now rail */} + + ) : loading && filteredTasks.length === 0 ? (
    Loading...
    ) : filteredTasks.length === 0 ? (
    diff --git a/frontend/src/types/scheduling.ts b/frontend/src/types/scheduling.ts index 214d3942..65703dd5 100644 --- a/frontend/src/types/scheduling.ts +++ b/frontend/src/types/scheduling.ts @@ -17,6 +17,7 @@ export interface ScheduledTask { prune_targets: string | null; target_services: string | null; prune_label_filter: string | null; + next_runs?: number[]; } export interface TaskRun {