mirror of
https://github.com/khoaliber/dockhand.git
synced 2026-03-07 21:29:06 +00:00
Initial commit
This commit is contained in:
207
routes/api/schedules/+server.ts
Normal file
207
routes/api/schedules/+server.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Schedules API - List all active schedules
|
||||
*
|
||||
* GET /api/schedules - Returns all enabled schedules (container auto-updates, git stack syncs, and system jobs)
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import {
|
||||
getEnabledAutoUpdateSettings,
|
||||
getEnabledAutoUpdateGitStacks,
|
||||
getAllAutoUpdateSettings,
|
||||
getAllAutoUpdateGitStacks,
|
||||
getAllEnvUpdateCheckSettings,
|
||||
getLastExecutionForSchedule,
|
||||
getRecentExecutionsForSchedule,
|
||||
getEnvironment,
|
||||
getEnvironmentTimezone,
|
||||
type ScheduleExecutionData,
|
||||
type VulnerabilityCriteria
|
||||
} from '$lib/server/db';
|
||||
import { getNextRun, getSystemSchedules } from '$lib/server/scheduler';
|
||||
import { getGlobalScannerDefaults, getScannerSettingsWithDefaults } from '$lib/server/scanner';
|
||||
|
||||
export interface ScheduleInfo {
|
||||
id: number;
|
||||
type: 'container_update' | 'git_stack_sync' | 'system_cleanup' | 'env_update_check';
|
||||
name: string;
|
||||
entityName: string;
|
||||
description?: string;
|
||||
environmentId: number | null;
|
||||
environmentName: string | null;
|
||||
enabled: boolean;
|
||||
scheduleType: string;
|
||||
cronExpression: string | null;
|
||||
nextRun: string | null;
|
||||
lastExecution: ScheduleExecutionData | null;
|
||||
recentExecutions: ScheduleExecutionData[];
|
||||
isSystem: boolean;
|
||||
// Container update specific fields
|
||||
envHasScanning?: boolean;
|
||||
vulnerabilityCriteria?: VulnerabilityCriteria | null;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
try {
|
||||
const schedules: ScheduleInfo[] = [];
|
||||
|
||||
// Pre-fetch global scanner defaults ONCE (CLI args are global, not per-environment)
|
||||
const globalScannerDefaults = await getGlobalScannerDefaults();
|
||||
|
||||
// Get container auto-update schedules (get all, not just enabled, so we can show them in the grid)
|
||||
const containerSettings = await getAllAutoUpdateSettings();
|
||||
const containerSchedules = await Promise.all(
|
||||
containerSettings.map(async (setting) => {
|
||||
const [env, lastExecution, recentExecutions, scannerSettings, timezone] = await Promise.all([
|
||||
setting.environmentId ? getEnvironment(setting.environmentId) : null,
|
||||
getLastExecutionForSchedule('container_update', setting.id),
|
||||
getRecentExecutionsForSchedule('container_update', setting.id, 5),
|
||||
getScannerSettingsWithDefaults(setting.environmentId ?? undefined, globalScannerDefaults),
|
||||
setting.environmentId ? getEnvironmentTimezone(setting.environmentId) : 'UTC'
|
||||
]);
|
||||
const isEnabled = setting.enabled ?? false;
|
||||
const nextRun = isEnabled && setting.cronExpression ? getNextRun(setting.cronExpression, timezone) : null;
|
||||
// Check if env has scanning enabled
|
||||
const envHasScanning = scannerSettings.scanner !== 'none';
|
||||
|
||||
return {
|
||||
id: setting.id,
|
||||
type: 'container_update' as const,
|
||||
name: `Update container: ${setting.containerName}`,
|
||||
entityName: setting.containerName,
|
||||
environmentId: setting.environmentId ?? null,
|
||||
environmentName: env?.name ?? null,
|
||||
enabled: isEnabled,
|
||||
scheduleType: setting.scheduleType ?? 'daily',
|
||||
cronExpression: setting.cronExpression ?? null,
|
||||
nextRun: nextRun?.toISOString() ?? null,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: false,
|
||||
envHasScanning,
|
||||
vulnerabilityCriteria: setting.vulnerabilityCriteria ?? null
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...containerSchedules);
|
||||
|
||||
// Get git stack auto-sync schedules
|
||||
const gitStacks = await getAllAutoUpdateGitStacks();
|
||||
const gitSchedules = await Promise.all(
|
||||
gitStacks.map(async (stack) => {
|
||||
const [env, lastExecution, recentExecutions, timezone] = await Promise.all([
|
||||
stack.environmentId ? getEnvironment(stack.environmentId) : null,
|
||||
getLastExecutionForSchedule('git_stack_sync', stack.id),
|
||||
getRecentExecutionsForSchedule('git_stack_sync', stack.id, 5),
|
||||
stack.environmentId ? getEnvironmentTimezone(stack.environmentId) : 'UTC'
|
||||
]);
|
||||
const isEnabled = stack.autoUpdate ?? false;
|
||||
const nextRun = isEnabled && stack.autoUpdateCron ? getNextRun(stack.autoUpdateCron, timezone) : null;
|
||||
|
||||
return {
|
||||
id: stack.id,
|
||||
type: 'git_stack_sync' as const,
|
||||
name: `Git sync: ${stack.stackName}`,
|
||||
entityName: stack.stackName,
|
||||
environmentId: stack.environmentId ?? null,
|
||||
environmentName: env?.name ?? null,
|
||||
enabled: isEnabled,
|
||||
scheduleType: stack.autoUpdateSchedule ?? 'daily',
|
||||
cronExpression: stack.autoUpdateCron ?? null,
|
||||
nextRun: nextRun?.toISOString() ?? null,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: false
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...gitSchedules);
|
||||
|
||||
// Get environment update check schedules
|
||||
const envUpdateCheckConfigs = await getAllEnvUpdateCheckSettings();
|
||||
const envUpdateCheckSchedules = await Promise.all(
|
||||
envUpdateCheckConfigs.map(async ({ envId, settings }) => {
|
||||
const [env, lastExecution, recentExecutions, scannerSettings, timezone] = await Promise.all([
|
||||
getEnvironment(envId),
|
||||
getLastExecutionForSchedule('env_update_check', envId),
|
||||
getRecentExecutionsForSchedule('env_update_check', envId, 5),
|
||||
getScannerSettingsWithDefaults(envId, globalScannerDefaults),
|
||||
getEnvironmentTimezone(envId)
|
||||
]);
|
||||
const isEnabled = settings.enabled ?? false;
|
||||
const nextRun = isEnabled && settings.cron ? getNextRun(settings.cron, timezone) : null;
|
||||
const envHasScanning = scannerSettings.scanner !== 'none';
|
||||
|
||||
// Build description based on autoUpdate and scanning status
|
||||
let description: string;
|
||||
if (settings.autoUpdate) {
|
||||
description = envHasScanning ? 'Check, scan & auto-update containers' : 'Check & auto-update containers';
|
||||
} else {
|
||||
description = 'Check containers for updates (notify only)';
|
||||
}
|
||||
|
||||
return {
|
||||
id: envId,
|
||||
type: 'env_update_check' as const,
|
||||
name: `Update environment: ${env?.name || 'Unknown'}`,
|
||||
entityName: env?.name || 'Unknown',
|
||||
description,
|
||||
environmentId: envId,
|
||||
environmentName: env?.name ?? null,
|
||||
enabled: isEnabled,
|
||||
scheduleType: 'custom',
|
||||
cronExpression: settings.cron ?? null,
|
||||
nextRun: nextRun?.toISOString() ?? null,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: false,
|
||||
autoUpdate: settings.autoUpdate,
|
||||
envHasScanning,
|
||||
vulnerabilityCriteria: settings.autoUpdate ? (settings.vulnerabilityCriteria ?? null) : null
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...envUpdateCheckSchedules);
|
||||
|
||||
// Get system schedules
|
||||
const systemSchedules = await getSystemSchedules();
|
||||
const sysSchedules = await Promise.all(
|
||||
systemSchedules.map(async (sys) => {
|
||||
const [lastExecution, recentExecutions] = await Promise.all([
|
||||
getLastExecutionForSchedule(sys.type, sys.id),
|
||||
getRecentExecutionsForSchedule(sys.type, sys.id, 5)
|
||||
]);
|
||||
|
||||
return {
|
||||
id: sys.id,
|
||||
type: sys.type,
|
||||
name: sys.name,
|
||||
entityName: sys.name,
|
||||
description: sys.description,
|
||||
environmentId: null,
|
||||
environmentName: null,
|
||||
enabled: sys.enabled,
|
||||
scheduleType: 'custom',
|
||||
cronExpression: sys.cronExpression,
|
||||
nextRun: sys.nextRun,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: true
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...sysSchedules);
|
||||
|
||||
// Sort: system jobs last, then by name
|
||||
schedules.sort((a, b) => {
|
||||
if (a.isSystem !== b.isSystem) return a.isSystem ? 1 : -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return json({ schedules });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get schedules:', error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
62
routes/api/schedules/[type]/[id]/+server.ts
Normal file
62
routes/api/schedules/[type]/[id]/+server.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Delete schedule
|
||||
* DELETE /api/schedules/:type/:id
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import {
|
||||
getAutoUpdateSettingById,
|
||||
deleteAutoUpdateSchedule,
|
||||
updateGitStack,
|
||||
deleteEnvUpdateCheckSettings
|
||||
} from '$lib/server/db';
|
||||
import { unregisterSchedule } from '$lib/server/scheduler';
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params }) => {
|
||||
try {
|
||||
const { type, id } = params;
|
||||
const scheduleId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(scheduleId)) {
|
||||
return json({ error: 'Invalid schedule ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (type === 'container_update') {
|
||||
// Hard delete container schedule
|
||||
const schedule = await getAutoUpdateSettingById(scheduleId);
|
||||
if (schedule) {
|
||||
await deleteAutoUpdateSchedule(schedule.containerName, schedule.environmentId ?? undefined);
|
||||
// Unregister from croner
|
||||
unregisterSchedule(scheduleId, 'container_update');
|
||||
}
|
||||
return json({ success: true });
|
||||
|
||||
} else if (type === 'git_stack_sync') {
|
||||
// Disable auto-update for git stack (don't delete the stack itself)
|
||||
await updateGitStack(scheduleId, {
|
||||
autoUpdate: false,
|
||||
autoUpdateSchedule: null,
|
||||
autoUpdateCron: null
|
||||
});
|
||||
// Unregister from croner
|
||||
unregisterSchedule(scheduleId, 'git_stack_sync');
|
||||
return json({ success: true });
|
||||
|
||||
} else if (type === 'env_update_check') {
|
||||
// Delete env update check settings (scheduleId is environmentId)
|
||||
await deleteEnvUpdateCheckSettings(scheduleId);
|
||||
unregisterSchedule(scheduleId, 'env_update_check');
|
||||
return json({ success: true });
|
||||
|
||||
} else if (type === 'system_cleanup') {
|
||||
return json({ error: 'System schedules cannot be removed' }, { status: 400 });
|
||||
|
||||
} else {
|
||||
return json({ error: 'Invalid schedule type' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete schedule:', error);
|
||||
return json({ error: 'Failed to delete schedule' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
52
routes/api/schedules/[type]/[id]/run/+server.ts
Normal file
52
routes/api/schedules/[type]/[id]/run/+server.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Manual Schedule Trigger API - Manually run a schedule
|
||||
*
|
||||
* POST /api/schedules/[type]/[id]/run - Trigger a manual execution
|
||||
*
|
||||
* Path params:
|
||||
* - type: 'container_update' | 'git_stack_sync' | 'system_cleanup'
|
||||
* - id: schedule ID
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { triggerContainerUpdate, triggerGitStackSync, triggerSystemJob, triggerEnvUpdateCheck } from '$lib/server/scheduler';
|
||||
|
||||
export const POST: RequestHandler = async ({ params }) => {
|
||||
try {
|
||||
const { type, id } = params;
|
||||
const scheduleId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(scheduleId)) {
|
||||
return json({ error: 'Invalid schedule ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
let result: { success: boolean; executionId?: number; error?: string };
|
||||
|
||||
switch (type) {
|
||||
case 'container_update':
|
||||
result = await triggerContainerUpdate(scheduleId);
|
||||
break;
|
||||
case 'git_stack_sync':
|
||||
result = await triggerGitStackSync(scheduleId);
|
||||
break;
|
||||
case 'system_cleanup':
|
||||
result = await triggerSystemJob(id);
|
||||
break;
|
||||
case 'env_update_check':
|
||||
result = await triggerEnvUpdateCheck(scheduleId);
|
||||
break;
|
||||
default:
|
||||
return json({ error: 'Invalid schedule type' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
return json({ error: result.error }, { status: 400 });
|
||||
}
|
||||
|
||||
return json({ success: true, message: 'Schedule triggered successfully' });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to trigger schedule:', error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
88
routes/api/schedules/[type]/[id]/toggle/+server.ts
Normal file
88
routes/api/schedules/[type]/[id]/toggle/+server.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Toggle schedule enabled/disabled
|
||||
* POST /api/schedules/:type/:id/toggle
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getAutoUpdateSettingById, updateAutoUpdateSettingById, getGitStack, updateGitStack, getEnvUpdateCheckSettings, setEnvUpdateCheckSettings } from '$lib/server/db';
|
||||
import { registerSchedule, unregisterSchedule } from '$lib/server/scheduler';
|
||||
|
||||
export const POST: RequestHandler = async ({ params }) => {
|
||||
try {
|
||||
const { type, id } = params;
|
||||
const scheduleId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(scheduleId)) {
|
||||
return json({ error: 'Invalid schedule ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (type === 'container_update') {
|
||||
const setting = await getAutoUpdateSettingById(scheduleId);
|
||||
if (!setting) {
|
||||
return json({ error: 'Schedule not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const newEnabled = !setting.enabled;
|
||||
await updateAutoUpdateSettingById(scheduleId, {
|
||||
enabled: newEnabled
|
||||
});
|
||||
|
||||
// Register or unregister schedule with croner
|
||||
if (newEnabled && setting.cronExpression) {
|
||||
await registerSchedule(scheduleId, 'container_update', setting.environmentId);
|
||||
} else {
|
||||
unregisterSchedule(scheduleId, 'container_update');
|
||||
}
|
||||
|
||||
return json({ success: true, enabled: newEnabled });
|
||||
} else if (type === 'git_stack_sync') {
|
||||
const stack = await getGitStack(scheduleId);
|
||||
if (!stack) {
|
||||
return json({ error: 'Schedule not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const newEnabled = !stack.autoUpdate;
|
||||
await updateGitStack(scheduleId, {
|
||||
autoUpdate: newEnabled
|
||||
});
|
||||
|
||||
// Register or unregister schedule with croner
|
||||
if (newEnabled && stack.autoUpdateCron) {
|
||||
await registerSchedule(scheduleId, 'git_stack_sync', stack.environmentId);
|
||||
} else {
|
||||
unregisterSchedule(scheduleId, 'git_stack_sync');
|
||||
}
|
||||
|
||||
return json({ success: true, enabled: newEnabled });
|
||||
} else if (type === 'env_update_check') {
|
||||
// scheduleId is environmentId for env update check
|
||||
const config = await getEnvUpdateCheckSettings(scheduleId);
|
||||
if (!config) {
|
||||
return json({ error: 'Schedule not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const newEnabled = !config.enabled;
|
||||
await setEnvUpdateCheckSettings(scheduleId, {
|
||||
...config,
|
||||
enabled: newEnabled
|
||||
});
|
||||
|
||||
// Register or unregister schedule with croner
|
||||
if (newEnabled && config.cron) {
|
||||
await registerSchedule(scheduleId, 'env_update_check', scheduleId);
|
||||
} else {
|
||||
unregisterSchedule(scheduleId, 'env_update_check');
|
||||
}
|
||||
|
||||
return json({ success: true, enabled: newEnabled });
|
||||
} else if (type === 'system_cleanup') {
|
||||
return json({ error: 'System schedules cannot be paused' }, { status: 400 });
|
||||
} else {
|
||||
return json({ error: 'Invalid schedule type' }, { status: 400 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle schedule:', error);
|
||||
return json({ error: 'Failed to toggle schedule' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
62
routes/api/schedules/executions/+server.ts
Normal file
62
routes/api/schedules/executions/+server.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Schedule Executions API - List execution history
|
||||
*
|
||||
* GET /api/schedules/executions - Returns paginated execution history
|
||||
*
|
||||
* Query params:
|
||||
* - scheduleType: 'container_update' | 'git_stack_sync'
|
||||
* - scheduleId: number
|
||||
* - environmentId: number
|
||||
* - status: 'queued' | 'running' | 'success' | 'failed' | 'skipped'
|
||||
* - triggeredBy: 'cron' | 'webhook' | 'manual'
|
||||
* - fromDate: ISO date string
|
||||
* - toDate: ISO date string
|
||||
* - limit: number (default 50)
|
||||
* - offset: number (default 0)
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import {
|
||||
getScheduleExecutions,
|
||||
type ScheduleType,
|
||||
type ScheduleTrigger,
|
||||
type ScheduleStatus
|
||||
} from '$lib/server/db';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
try {
|
||||
const scheduleType = url.searchParams.get('scheduleType') as ScheduleType | null;
|
||||
const scheduleIdParam = url.searchParams.get('scheduleId');
|
||||
const environmentIdParam = url.searchParams.get('environmentId');
|
||||
const status = url.searchParams.get('status') as ScheduleStatus | null;
|
||||
const statusesParam = url.searchParams.get('statuses');
|
||||
const triggeredBy = url.searchParams.get('triggeredBy') as ScheduleTrigger | null;
|
||||
const fromDate = url.searchParams.get('fromDate');
|
||||
const toDate = url.searchParams.get('toDate');
|
||||
const limitParam = url.searchParams.get('limit');
|
||||
const offsetParam = url.searchParams.get('offset');
|
||||
|
||||
const filters: any = {};
|
||||
|
||||
if (scheduleType) filters.scheduleType = scheduleType;
|
||||
if (scheduleIdParam) filters.scheduleId = parseInt(scheduleIdParam, 10);
|
||||
if (environmentIdParam) {
|
||||
filters.environmentId = environmentIdParam === 'null' ? null : parseInt(environmentIdParam, 10);
|
||||
}
|
||||
if (status) filters.status = status;
|
||||
if (statusesParam) filters.statuses = statusesParam.split(',') as ScheduleStatus[];
|
||||
if (triggeredBy) filters.triggeredBy = triggeredBy;
|
||||
if (fromDate) filters.fromDate = fromDate;
|
||||
if (toDate) filters.toDate = toDate;
|
||||
if (limitParam) filters.limit = parseInt(limitParam, 10);
|
||||
if (offsetParam) filters.offset = parseInt(offsetParam, 10);
|
||||
|
||||
const result = await getScheduleExecutions(filters);
|
||||
|
||||
return json(result);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get schedule executions:', error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
45
routes/api/schedules/executions/[id]/+server.ts
Normal file
45
routes/api/schedules/executions/[id]/+server.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Schedule Execution Detail API
|
||||
*
|
||||
* GET /api/schedules/executions/[id] - Returns execution details including logs
|
||||
* DELETE /api/schedules/executions/[id] - Delete a schedule execution
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getScheduleExecution, deleteScheduleExecution } from '$lib/server/db';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
try {
|
||||
const id = parseInt(params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid execution ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
const execution = await getScheduleExecution(id);
|
||||
if (!execution) {
|
||||
return json({ error: 'Execution not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return json(execution);
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get schedule execution:', error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params }) => {
|
||||
try {
|
||||
const id = parseInt(params.id, 10);
|
||||
if (isNaN(id)) {
|
||||
return json({ error: 'Invalid execution ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
await deleteScheduleExecution(id);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to delete schedule execution:', error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
61
routes/api/schedules/settings/+server.ts
Normal file
61
routes/api/schedules/settings/+server.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Schedule Settings API - Get/set schedule display preferences
|
||||
*
|
||||
* GET /api/schedules/settings - Get current display settings
|
||||
* PUT /api/schedules/settings - Update display settings
|
||||
*
|
||||
* Note: Data retention settings are now managed in /api/settings/general
|
||||
*/
|
||||
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { getSetting, setSetting } from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
// Setting key for hide system jobs preference
|
||||
const getHideSystemJobsKey = (userId?: number) =>
|
||||
userId ? `user_${userId}_schedules_hide_system_jobs` : 'schedules_hide_system_jobs';
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
try {
|
||||
const auth = await authorize(cookies);
|
||||
const userId = auth.isAuthenticated ? auth.user?.id : undefined;
|
||||
|
||||
// Get user-specific setting, fallback to global
|
||||
let hideSystemJobs = await getSetting(getHideSystemJobsKey(userId));
|
||||
if (hideSystemJobs === null && userId) {
|
||||
hideSystemJobs = await getSetting(getHideSystemJobsKey());
|
||||
}
|
||||
if (hideSystemJobs === null) {
|
||||
hideSystemJobs = false; // Default to visible
|
||||
}
|
||||
|
||||
return json({ hideSystemJobs });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to get schedule settings:', error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async ({ request, cookies }) => {
|
||||
try {
|
||||
const auth = await authorize(cookies);
|
||||
const userId = auth.isAuthenticated ? auth.user?.id : undefined;
|
||||
|
||||
const body = await request.json();
|
||||
const { hideSystemJobs } = body;
|
||||
|
||||
if (hideSystemJobs !== undefined) {
|
||||
if (typeof hideSystemJobs !== 'boolean') {
|
||||
return json({ error: 'Invalid hideSystemJobs (must be boolean)' }, { status: 400 });
|
||||
}
|
||||
// Save user-specific preference
|
||||
await setSetting(getHideSystemJobsKey(userId), hideSystemJobs);
|
||||
}
|
||||
|
||||
return json({ success: true, hideSystemJobs });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to update schedule settings:', error);
|
||||
return json({ error: error.message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
306
routes/api/schedules/stream/+server.ts
Normal file
306
routes/api/schedules/stream/+server.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
/**
|
||||
* Schedules Stream API - Real-time schedule updates via SSE
|
||||
*
|
||||
* GET /api/schedules/stream - Server-Sent Events stream for schedule updates
|
||||
*/
|
||||
|
||||
import type { RequestHandler } from './$types';
|
||||
import {
|
||||
getAllAutoUpdateSettings,
|
||||
getAllAutoUpdateGitStacks,
|
||||
getAllEnvUpdateCheckSettings,
|
||||
getLastExecutionForSchedule,
|
||||
getRecentExecutionsForSchedule,
|
||||
getEnvironment,
|
||||
getEnvironmentTimezone,
|
||||
type VulnerabilityCriteria
|
||||
} from '$lib/server/db';
|
||||
import { getNextRun, getSystemSchedules } from '$lib/server/scheduler';
|
||||
import { getGlobalScannerDefaults, getScannerSettingsWithDefaults } from '$lib/server/scanner';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import type { ScheduleInfo } from '../+server';
|
||||
|
||||
async function getSchedulesData(): Promise<ScheduleInfo[]> {
|
||||
const schedules: ScheduleInfo[] = [];
|
||||
|
||||
// Pre-fetch global scanner defaults ONCE (CLI args are global, not per-environment)
|
||||
const globalScannerDefaults = await getGlobalScannerDefaults();
|
||||
|
||||
// Get container auto-update schedules
|
||||
const containerSettings = await getAllAutoUpdateSettings();
|
||||
const containerSchedules = await Promise.all(
|
||||
containerSettings.map(async (setting) => {
|
||||
const [env, lastExecution, recentExecutions, scannerSettings, timezone] = await Promise.all([
|
||||
setting.environmentId ? getEnvironment(setting.environmentId) : null,
|
||||
getLastExecutionForSchedule('container_update', setting.id),
|
||||
getRecentExecutionsForSchedule('container_update', setting.id, 5),
|
||||
getScannerSettingsWithDefaults(setting.environmentId ?? undefined, globalScannerDefaults),
|
||||
setting.environmentId ? getEnvironmentTimezone(setting.environmentId) : 'UTC'
|
||||
]);
|
||||
const isEnabled = setting.enabled ?? false;
|
||||
const nextRun = isEnabled && setting.cronExpression ? getNextRun(setting.cronExpression, timezone) : null;
|
||||
const envHasScanning = scannerSettings.scanner !== 'none';
|
||||
|
||||
return {
|
||||
id: setting.id,
|
||||
type: 'container_update' as const,
|
||||
name: `Update container: ${setting.containerName}`,
|
||||
entityName: setting.containerName,
|
||||
environmentId: setting.environmentId ?? null,
|
||||
environmentName: env?.name ?? null,
|
||||
enabled: isEnabled,
|
||||
scheduleType: setting.scheduleType ?? 'daily',
|
||||
cronExpression: setting.cronExpression ?? null,
|
||||
nextRun: nextRun?.toISOString() ?? null,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: false,
|
||||
envHasScanning,
|
||||
vulnerabilityCriteria: setting.vulnerabilityCriteria ?? null
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...containerSchedules);
|
||||
|
||||
// Get git stack auto-sync schedules
|
||||
const gitStacks = await getAllAutoUpdateGitStacks();
|
||||
const gitSchedules = await Promise.all(
|
||||
gitStacks.map(async (stack) => {
|
||||
const [env, lastExecution, recentExecutions, timezone] = await Promise.all([
|
||||
stack.environmentId ? getEnvironment(stack.environmentId) : null,
|
||||
getLastExecutionForSchedule('git_stack_sync', stack.id),
|
||||
getRecentExecutionsForSchedule('git_stack_sync', stack.id, 5),
|
||||
stack.environmentId ? getEnvironmentTimezone(stack.environmentId) : 'UTC'
|
||||
]);
|
||||
const isEnabled = stack.autoUpdate ?? false;
|
||||
const nextRun = isEnabled && stack.autoUpdateCron ? getNextRun(stack.autoUpdateCron, timezone) : null;
|
||||
|
||||
return {
|
||||
id: stack.id,
|
||||
type: 'git_stack_sync' as const,
|
||||
name: `Git sync: ${stack.stackName}`,
|
||||
entityName: stack.stackName,
|
||||
environmentId: stack.environmentId ?? null,
|
||||
environmentName: env?.name ?? null,
|
||||
enabled: isEnabled,
|
||||
scheduleType: stack.autoUpdateSchedule ?? 'daily',
|
||||
cronExpression: stack.autoUpdateCron ?? null,
|
||||
nextRun: nextRun?.toISOString() ?? null,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: false
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...gitSchedules);
|
||||
|
||||
// Get environment update check schedules
|
||||
const envUpdateCheckConfigs = await getAllEnvUpdateCheckSettings();
|
||||
const envUpdateCheckSchedules = await Promise.all(
|
||||
envUpdateCheckConfigs.map(async ({ envId, settings }) => {
|
||||
const [env, lastExecution, recentExecutions, scannerSettings, timezone] = await Promise.all([
|
||||
getEnvironment(envId),
|
||||
getLastExecutionForSchedule('env_update_check', envId),
|
||||
getRecentExecutionsForSchedule('env_update_check', envId, 5),
|
||||
getScannerSettingsWithDefaults(envId, globalScannerDefaults),
|
||||
getEnvironmentTimezone(envId)
|
||||
]);
|
||||
const isEnabled = settings.enabled ?? false;
|
||||
const nextRun = isEnabled && settings.cron ? getNextRun(settings.cron, timezone) : null;
|
||||
const envHasScanning = scannerSettings.scanner !== 'none';
|
||||
|
||||
// Build description based on autoUpdate and scanning status
|
||||
let description: string;
|
||||
if (settings.autoUpdate) {
|
||||
description = envHasScanning ? 'Check, scan & auto-update containers' : 'Check & auto-update containers';
|
||||
} else {
|
||||
description = 'Check containers for updates (notify only)';
|
||||
}
|
||||
|
||||
return {
|
||||
id: envId,
|
||||
type: 'env_update_check' as const,
|
||||
name: `Update environment: ${env?.name || 'Unknown'}`,
|
||||
entityName: env?.name || 'Unknown',
|
||||
description,
|
||||
environmentId: envId,
|
||||
environmentName: env?.name ?? null,
|
||||
enabled: isEnabled,
|
||||
scheduleType: 'custom',
|
||||
cronExpression: settings.cron ?? null,
|
||||
nextRun: nextRun?.toISOString() ?? null,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: false,
|
||||
autoUpdate: settings.autoUpdate,
|
||||
envHasScanning,
|
||||
vulnerabilityCriteria: settings.autoUpdate ? (settings.vulnerabilityCriteria ?? null) : null
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...envUpdateCheckSchedules);
|
||||
|
||||
// Get system schedules
|
||||
const systemSchedules = await getSystemSchedules();
|
||||
const sysSchedules = await Promise.all(
|
||||
systemSchedules.map(async (sys) => {
|
||||
const [lastExecution, recentExecutions] = await Promise.all([
|
||||
getLastExecutionForSchedule(sys.type, sys.id),
|
||||
getRecentExecutionsForSchedule(sys.type, sys.id, 5)
|
||||
]);
|
||||
|
||||
return {
|
||||
id: sys.id,
|
||||
type: sys.type,
|
||||
name: sys.name,
|
||||
entityName: sys.name,
|
||||
description: sys.description,
|
||||
environmentId: null,
|
||||
environmentName: null,
|
||||
enabled: sys.enabled,
|
||||
scheduleType: 'custom',
|
||||
cronExpression: sys.cronExpression,
|
||||
nextRun: sys.nextRun,
|
||||
lastExecution: lastExecution ?? null,
|
||||
recentExecutions,
|
||||
isSystem: true
|
||||
};
|
||||
})
|
||||
);
|
||||
schedules.push(...sysSchedules);
|
||||
|
||||
// Sort: system jobs last, then by name
|
||||
schedules.sort((a, b) => {
|
||||
if (a.isSystem !== b.isSystem) return a.isSystem ? 1 : -1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return schedules;
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('schedules', 'view')) {
|
||||
return new Response(JSON.stringify({ error: 'Permission denied' }), {
|
||||
status: 403,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
}
|
||||
|
||||
let controllerClosed = false;
|
||||
let intervalId: ReturnType<typeof setInterval> | null = null;
|
||||
let isPolling = false; // Guard against concurrent poll executions
|
||||
let initialDataSent = false; // Track if initial data was successfully sent
|
||||
|
||||
const stream = new ReadableStream({
|
||||
async start(controller) {
|
||||
const encoder = new TextEncoder();
|
||||
console.log('[Schedules Stream] New connection opened');
|
||||
|
||||
// Returns true if data was successfully enqueued
|
||||
const safeEnqueue = (data: string): boolean => {
|
||||
if (controllerClosed) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
controller.enqueue(encoder.encode(data));
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.log('[Schedules Stream] Controller closed during enqueue, cleaning up');
|
||||
controllerClosed = true;
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Send immediate connection confirmation so client knows stream is alive
|
||||
if (!safeEnqueue(`event: connected\ndata: {}\n\n`)) {
|
||||
return; // Connection already closed, abort
|
||||
}
|
||||
|
||||
// Send initial data - this is critical, retry logic if needed
|
||||
let retryCount = 0;
|
||||
const maxRetries = 2;
|
||||
|
||||
while (!initialDataSent && retryCount <= maxRetries && !controllerClosed) {
|
||||
try {
|
||||
const schedules = await getSchedulesData();
|
||||
|
||||
// Check if still connected before sending
|
||||
if (controllerClosed) {
|
||||
console.log('[Schedules Stream] Connection closed before initial data could be sent');
|
||||
return;
|
||||
}
|
||||
|
||||
if (safeEnqueue(`event: schedules\ndata: ${JSON.stringify({ schedules })}\n\n`)) {
|
||||
initialDataSent = true;
|
||||
console.log('[Schedules Stream] Initial data sent successfully');
|
||||
} else {
|
||||
console.log('[Schedules Stream] Failed to enqueue initial data, connection closed');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[Schedules Stream] Failed to get initial schedules (attempt ${retryCount + 1}):`, error);
|
||||
retryCount++;
|
||||
|
||||
if (retryCount > maxRetries) {
|
||||
// Send error event to client so they can show an error state
|
||||
safeEnqueue(`event: error\ndata: ${JSON.stringify({ error: String(error), fatal: true })}\n\n`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Brief delay before retry
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
|
||||
// Only start polling if initial data was sent successfully
|
||||
if (!initialDataSent) {
|
||||
console.log('[Schedules Stream] Initial data was never sent, not starting polling');
|
||||
return;
|
||||
}
|
||||
|
||||
// Poll every 2 seconds for updates (with guard against concurrent executions)
|
||||
intervalId = setInterval(async () => {
|
||||
// Skip if already polling or controller closed
|
||||
if (isPolling || controllerClosed) {
|
||||
if (controllerClosed && intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
isPolling = true;
|
||||
try {
|
||||
const schedules = await getSchedulesData();
|
||||
safeEnqueue(`event: schedules\ndata: ${JSON.stringify({ schedules })}\n\n`);
|
||||
} catch (error) {
|
||||
console.error('[Schedules Stream] Failed to get schedules during poll:', error);
|
||||
// Don't send error event for poll failures, just log
|
||||
} finally {
|
||||
isPolling = false;
|
||||
}
|
||||
}, 2000);
|
||||
},
|
||||
cancel() {
|
||||
console.log('[Schedules Stream] Connection cancelled, cleaning up');
|
||||
controllerClosed = true;
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
intervalId = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive'
|
||||
}
|
||||
});
|
||||
};
|
||||
42
routes/api/schedules/system/[id]/toggle/+server.ts
Normal file
42
routes/api/schedules/system/[id]/toggle/+server.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { json, type RequestHandler } from '@sveltejs/kit';
|
||||
import {
|
||||
setScheduleCleanupEnabled,
|
||||
setEventCleanupEnabled,
|
||||
getScheduleCleanupEnabled,
|
||||
getEventCleanupEnabled
|
||||
} from '$lib/server/db';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
|
||||
const SYSTEM_SCHEDULE_CLEANUP_ID = 1;
|
||||
const SYSTEM_EVENT_CLEANUP_ID = 2;
|
||||
|
||||
export const POST: RequestHandler = async ({ params, cookies }) => {
|
||||
const auth = await authorize(cookies);
|
||||
if (auth.authEnabled && !await auth.can('settings', 'edit')) {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
const { id } = params;
|
||||
const systemId = parseInt(id, 10);
|
||||
|
||||
if (isNaN(systemId)) {
|
||||
return json({ error: 'Invalid system schedule ID' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (systemId === SYSTEM_SCHEDULE_CLEANUP_ID) {
|
||||
const currentEnabled = await getScheduleCleanupEnabled();
|
||||
await setScheduleCleanupEnabled(!currentEnabled);
|
||||
return json({ success: true, enabled: !currentEnabled });
|
||||
} else if (systemId === SYSTEM_EVENT_CLEANUP_ID) {
|
||||
const currentEnabled = await getEventCleanupEnabled();
|
||||
await setEventCleanupEnabled(!currentEnabled);
|
||||
return json({ success: true, enabled: !currentEnabled });
|
||||
} else {
|
||||
return json({ error: 'Unknown system schedule' }, { status: 400 });
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Failed to toggle system schedule:', error);
|
||||
return json({ error: 'Failed to toggle system schedule' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user