import { json, type RequestHandler } from '@sveltejs/kit'; import { getSetting, setSetting, getScheduleRetentionDays, setScheduleRetentionDays, getEventRetentionDays, setEventRetentionDays, getScheduleCleanupCron, setScheduleCleanupCron, getEventCleanupCron, setEventCleanupCron, getScheduleCleanupEnabled, setScheduleCleanupEnabled, getEventCleanupEnabled, setEventCleanupEnabled, getScannerCleanupCron, setScannerCleanupCron, getScannerCleanupEnabled, setScannerCleanupEnabled, getDefaultTimezone, setDefaultTimezone, getEventCollectionMode, setEventCollectionMode, getEventPollInterval, setEventPollInterval, getMetricsCollectionInterval, setMetricsCollectionInterval, getExternalStackPaths, setExternalStackPaths, getPrimaryStackLocation, setPrimaryStackLocation } from '$lib/server/db'; import { authorize } from '$lib/server/authorize'; import { refreshSystemJobs } from '$lib/server/scheduler'; import { sendToEventSubprocess, sendToMetricsSubprocess } from '$lib/server/subprocess-manager'; import { DEFAULT_GRYPE_IMAGE, DEFAULT_TRIVY_IMAGE } from '$lib/server/scanner'; export type TimeFormat = '12h' | '24h'; export type DateFormat = 'MM/DD/YYYY' | 'DD/MM/YYYY' | 'YYYY-MM-DD' | 'DD.MM.YYYY'; export type DownloadFormat = 'tar' | 'tar.gz'; export type EventCollectionMode = 'stream' | 'poll'; export interface GeneralSettings { confirmDestructive: boolean; showStoppedContainers: boolean; highlightUpdates: boolean; timeFormat: TimeFormat; dateFormat: DateFormat; downloadFormat: DownloadFormat; defaultGrypeArgs: string; defaultTrivyArgs: string; scheduleRetentionDays: number; eventRetentionDays: number; scheduleCleanupCron: string; eventCleanupCron: string; scheduleCleanupEnabled: boolean; eventCleanupEnabled: boolean; scannerCleanupCron: string; scannerCleanupEnabled: boolean; logBufferSizeKb: number; defaultTimezone: string; // Background monitoring settings eventCollectionMode: EventCollectionMode; eventPollInterval: number; metricsCollectionInterval: number; // Theme settings (for when auth is disabled) lightTheme: string; darkTheme: string; font: string; fontSize: string; gridFontSize: string; terminalFont: string; editorFont: string; // Compact ports compactPorts: boolean; // Log timestamp formatting formatLogTimestamps: boolean; // External stack paths externalStackPaths: string[]; // Primary stack location primaryStackLocation: string | null; // Scanner images defaultGrypeImage: string; defaultTrivyImage: string; // Compose template defaultComposeTemplate: string; // Label filter mode labelFilterMode: 'any' | 'all'; } const DEFAULT_SETTINGS: Omit = { confirmDestructive: true, showStoppedContainers: true, highlightUpdates: true, timeFormat: '24h', dateFormat: 'DD.MM.YYYY', downloadFormat: 'tar', defaultGrypeArgs: '-o json -v {image}', defaultTrivyArgs: 'image --format json {image}', logBufferSizeKb: 500, defaultTimezone: 'UTC', eventCollectionMode: 'stream', eventPollInterval: 60000, metricsCollectionInterval: 30000, compactPorts: false, formatLogTimestamps: false, lightTheme: 'default', darkTheme: 'default', font: 'system', fontSize: 'normal', gridFontSize: 'normal', terminalFont: 'system-mono', editorFont: 'system-mono', externalStackPaths: [], primaryStackLocation: null, defaultGrypeImage: DEFAULT_GRYPE_IMAGE, defaultTrivyImage: DEFAULT_TRIVY_IMAGE, labelFilterMode: 'any' as const, defaultComposeTemplate: `version: "3.8" services: app: image: nginx:alpine ports: - "8080:80" environment: - APP_ENV=\${APP_ENV:-production} volumes: - ./html:/usr/share/nginx/html:ro restart: unless-stopped # Add more services as needed # networks: # default: # driver: bridge ` }; const VALID_LIGHT_THEMES = ['default', 'catppuccin', 'rose-pine', 'nord', 'solarized', 'gruvbox', 'alucard', 'github', 'material', 'atom-one']; const VALID_DARK_THEMES = ['default', 'catppuccin', 'dracula', 'rose-pine', 'rose-pine-moon', 'tokyo-night', 'nord', 'one-dark', 'gruvbox', 'solarized', 'everforest', 'kanagawa', 'monokai', 'monokai-pro', 'material', 'palenight', 'github']; const VALID_FONTS = ['system', 'geist', 'inter', 'plus-jakarta', 'dm-sans', 'outfit', 'space-grotesk', 'sofia-sans', 'nunito', 'poppins', 'montserrat', 'raleway', 'manrope', 'roboto', 'open-sans', 'lato', 'source-sans', 'work-sans', 'fira-sans', 'jetbrains-mono', 'fira-code', 'quicksand', 'comfortaa']; const VALID_FONT_SIZES = ['xsmall', 'small', 'normal', 'medium', 'large', 'xlarge']; const VALID_TERMINAL_FONTS = ['system-mono', 'jetbrains-mono', 'fira-code', 'source-code-pro', 'cascadia-code', 'ibm-plex-mono', 'roboto-mono', 'ubuntu-mono', 'space-mono', 'inconsolata', 'hack', 'anonymous-pro', 'dm-mono', 'red-hat-mono', 'menlo', 'consolas', 'sf-mono']; const VALID_EDITOR_FONTS = VALID_TERMINAL_FONTS; const VALID_DATE_FORMATS: DateFormat[] = ['MM/DD/YYYY', 'DD/MM/YYYY', 'YYYY-MM-DD', 'DD.MM.YYYY']; export const GET: RequestHandler = async ({ cookies }) => { const auth = await authorize(cookies); // UI preferences (time format, date format) should be available to all authenticated users // This doesn't expose sensitive data and is needed for proper UI rendering if (auth.authEnabled && !auth.isAuthenticated) { return json({ error: 'Authentication required' }, { status: 401 }); } try { // Fetch all settings in parallel for better performance const [ confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode ] = await Promise.all([ getSetting('confirm_destructive'), getSetting('show_stopped_containers'), getSetting('highlight_updates'), getSetting('time_format'), getSetting('date_format'), getSetting('download_format'), getSetting('default_grype_args'), getSetting('default_trivy_args'), getScheduleRetentionDays(), getEventRetentionDays(), getScheduleCleanupCron(), getEventCleanupCron(), getScheduleCleanupEnabled(), getEventCleanupEnabled(), getScannerCleanupCron(), getScannerCleanupEnabled(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), getEventCollectionMode(), getEventPollInterval(), getMetricsCollectionInterval(), getSetting('theme_light'), getSetting('theme_dark'), getSetting('theme_font'), getSetting('theme_font_size'), getSetting('theme_grid_font_size'), getSetting('theme_terminal_font'), getSetting('theme_editor_font'), getSetting('compact_ports'), getSetting('format_log_timestamps'), getExternalStackPaths(), getPrimaryStackLocation(), getSetting('default_grype_image'), getSetting('default_trivy_image'), getSetting('default_compose_template'), getSetting('label_filter_mode') ]); const settings: GeneralSettings = { confirmDestructive: confirmDestructive ?? DEFAULT_SETTINGS.confirmDestructive, showStoppedContainers: showStoppedContainers ?? DEFAULT_SETTINGS.showStoppedContainers, highlightUpdates: highlightUpdates ?? DEFAULT_SETTINGS.highlightUpdates, timeFormat: timeFormat ?? DEFAULT_SETTINGS.timeFormat, dateFormat: dateFormat ?? DEFAULT_SETTINGS.dateFormat, downloadFormat: downloadFormat ?? DEFAULT_SETTINGS.downloadFormat, defaultGrypeArgs: defaultGrypeArgs ?? DEFAULT_SETTINGS.defaultGrypeArgs, defaultTrivyArgs: defaultTrivyArgs ?? DEFAULT_SETTINGS.defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb: logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, eventCollectionMode: (eventCollectionMode ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode, eventPollInterval: eventPollInterval ?? DEFAULT_SETTINGS.eventPollInterval, metricsCollectionInterval: metricsCollectionInterval ?? DEFAULT_SETTINGS.metricsCollectionInterval, lightTheme: lightTheme ?? DEFAULT_SETTINGS.lightTheme, darkTheme: darkTheme ?? DEFAULT_SETTINGS.darkTheme, font: font ?? DEFAULT_SETTINGS.font, fontSize: fontSize ?? DEFAULT_SETTINGS.fontSize, gridFontSize: gridFontSize ?? DEFAULT_SETTINGS.gridFontSize, terminalFont: terminalFont ?? DEFAULT_SETTINGS.terminalFont, editorFont: editorFont ?? DEFAULT_SETTINGS.editorFont, compactPorts: compactPorts ?? DEFAULT_SETTINGS.compactPorts, formatLogTimestamps: formatLogTimestamps ?? DEFAULT_SETTINGS.formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage: defaultGrypeImage ?? DEFAULT_GRYPE_IMAGE, defaultTrivyImage: defaultTrivyImage ?? DEFAULT_TRIVY_IMAGE, defaultComposeTemplate: defaultComposeTemplate ?? DEFAULT_SETTINGS.defaultComposeTemplate, labelFilterMode: labelFilterMode ?? DEFAULT_SETTINGS.labelFilterMode }; return json(settings); } catch (error) { console.error('Failed to get general settings:', error); return json({ error: 'Failed to get general settings' }, { status: 500 }); } }; export const POST: RequestHandler = async ({ request, cookies }) => { const auth = await authorize(cookies); if (auth.authEnabled && !await auth.can('settings', 'edit')) { return json({ error: 'Permission denied' }, { status: 403 }); } try { const body = await request.json(); const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode } = body; if (confirmDestructive !== undefined) { await setSetting('confirm_destructive', confirmDestructive); } if (showStoppedContainers !== undefined) { await setSetting('show_stopped_containers', showStoppedContainers); } if (highlightUpdates !== undefined) { await setSetting('highlight_updates', highlightUpdates); } if (timeFormat !== undefined && (timeFormat === '12h' || timeFormat === '24h')) { await setSetting('time_format', timeFormat); } if (dateFormat !== undefined && VALID_DATE_FORMATS.includes(dateFormat)) { await setSetting('date_format', dateFormat); } if (downloadFormat !== undefined && (downloadFormat === 'tar' || downloadFormat === 'tar.gz')) { await setSetting('download_format', downloadFormat); } if (defaultGrypeArgs !== undefined && typeof defaultGrypeArgs === 'string') { await setSetting('default_grype_args', defaultGrypeArgs); } if (defaultTrivyArgs !== undefined && typeof defaultTrivyArgs === 'string') { await setSetting('default_trivy_args', defaultTrivyArgs); } if (scheduleRetentionDays !== undefined && typeof scheduleRetentionDays === 'number') { await setScheduleRetentionDays(Math.max(1, Math.min(365, scheduleRetentionDays))); } if (eventRetentionDays !== undefined && typeof eventRetentionDays === 'number') { await setEventRetentionDays(Math.max(1, Math.min(365, eventRetentionDays))); } if (scheduleCleanupCron !== undefined && typeof scheduleCleanupCron === 'string') { await setScheduleCleanupCron(scheduleCleanupCron); } if (eventCleanupCron !== undefined && typeof eventCleanupCron === 'string') { await setEventCleanupCron(eventCleanupCron); } if (scheduleCleanupEnabled !== undefined && typeof scheduleCleanupEnabled === 'boolean') { await setScheduleCleanupEnabled(scheduleCleanupEnabled); } if (eventCleanupEnabled !== undefined && typeof eventCleanupEnabled === 'boolean') { await setEventCleanupEnabled(eventCleanupEnabled); } if (scannerCleanupCron !== undefined && typeof scannerCleanupCron === 'string') { await setScannerCleanupCron(scannerCleanupCron); await refreshSystemJobs(); } if (scannerCleanupEnabled !== undefined && typeof scannerCleanupEnabled === 'boolean') { await setScannerCleanupEnabled(scannerCleanupEnabled); await refreshSystemJobs(); } if (logBufferSizeKb !== undefined && typeof logBufferSizeKb === 'number') { // Clamp to reasonable range: 100KB - 5000KB (5MB) await setSetting('log_buffer_size_kb', Math.max(100, Math.min(5000, logBufferSizeKb))); } if (defaultTimezone !== undefined && typeof defaultTimezone === 'string') { await setDefaultTimezone(defaultTimezone); // Refresh system jobs to use the new timezone await refreshSystemJobs(); } if (eventCollectionMode !== undefined && (eventCollectionMode === 'stream' || eventCollectionMode === 'poll')) { await setEventCollectionMode(eventCollectionMode); // Notify event subprocess to refresh collectors with new mode sendToEventSubprocess({ type: 'refresh_environments' }); } if (eventPollInterval !== undefined && typeof eventPollInterval === 'number') { // Validate: 30s - 300s (30 seconds to 5 minutes) const validatedInterval = Math.max(30000, Math.min(300000, eventPollInterval)); await setEventPollInterval(validatedInterval); // Notify event subprocess to refresh collectors with new interval sendToEventSubprocess({ type: 'refresh_environments' }); } if (metricsCollectionInterval !== undefined && typeof metricsCollectionInterval === 'number') { // Validate: 10s - 300s (10 seconds to 5 minutes) const validatedInterval = Math.max(10000, Math.min(300000, metricsCollectionInterval)); await setMetricsCollectionInterval(validatedInterval); // Notify metrics subprocess to update its collection interval sendToMetricsSubprocess({ type: 'update_interval', intervalMs: validatedInterval }); } if (lightTheme !== undefined && VALID_LIGHT_THEMES.includes(lightTheme)) { await setSetting('theme_light', lightTheme); } if (darkTheme !== undefined && VALID_DARK_THEMES.includes(darkTheme)) { await setSetting('theme_dark', darkTheme); } if (font !== undefined && VALID_FONTS.includes(font)) { await setSetting('theme_font', font); } if (fontSize !== undefined && VALID_FONT_SIZES.includes(fontSize)) { await setSetting('theme_font_size', fontSize); } if (gridFontSize !== undefined && VALID_FONT_SIZES.includes(gridFontSize)) { await setSetting('theme_grid_font_size', gridFontSize); } if (terminalFont !== undefined && VALID_TERMINAL_FONTS.includes(terminalFont)) { await setSetting('theme_terminal_font', terminalFont); } if (editorFont !== undefined && VALID_EDITOR_FONTS.includes(editorFont)) { await setSetting('theme_editor_font', editorFont); } if (compactPorts !== undefined) { await setSetting('compact_ports', compactPorts); } if (formatLogTimestamps !== undefined) { await setSetting('format_log_timestamps', formatLogTimestamps); } if (externalStackPaths !== undefined && Array.isArray(externalStackPaths)) { // Filter to valid non-empty strings const validPaths = externalStackPaths.filter((p: unknown) => typeof p === 'string' && p.trim()); await setExternalStackPaths(validPaths); } if (primaryStackLocation !== undefined) { // Accept string or null if (primaryStackLocation === null || (typeof primaryStackLocation === 'string' && primaryStackLocation.trim())) { await setPrimaryStackLocation(primaryStackLocation); } else if (primaryStackLocation === '') { // Empty string means clear the setting await setPrimaryStackLocation(null); } } if (defaultGrypeImage !== undefined && typeof defaultGrypeImage === 'string') { await setSetting('default_grype_image', defaultGrypeImage); } if (defaultTrivyImage !== undefined && typeof defaultTrivyImage === 'string') { await setSetting('default_trivy_image', defaultTrivyImage); } if (defaultComposeTemplate !== undefined && typeof defaultComposeTemplate === 'string') { await setSetting('default_compose_template', defaultComposeTemplate); } if (labelFilterMode !== undefined && (labelFilterMode === 'any' || labelFilterMode === 'all')) { await setSetting('label_filter_mode', labelFilterMode); } // Fetch all settings in parallel for the response const [ confirmDestructiveVal, showStoppedContainersVal, highlightUpdatesVal, timeFormatVal, dateFormatVal, downloadFormatVal, defaultGrypeArgsVal, defaultTrivyArgsVal, scheduleRetentionDaysVal, eventRetentionDaysVal, scheduleCleanupCronVal, eventCleanupCronVal, scheduleCleanupEnabledVal, eventCleanupEnabledVal, scannerCleanupCronVal, scannerCleanupEnabledVal, logBufferSizeKbVal, defaultTimezoneVal, eventCollectionModeVal, eventPollIntervalVal, metricsCollectionIntervalVal, lightThemeVal, darkThemeVal, fontVal, fontSizeVal, gridFontSizeVal, terminalFontVal, editorFontVal, compactPortsVal, formatLogTimestampsVal, externalStackPathsVal, primaryStackLocationVal, defaultGrypeImageVal, defaultTrivyImageVal, defaultComposeTemplateVal, labelFilterModeVal ] = await Promise.all([ getSetting('confirm_destructive'), getSetting('show_stopped_containers'), getSetting('highlight_updates'), getSetting('time_format'), getSetting('date_format'), getSetting('download_format'), getSetting('default_grype_args'), getSetting('default_trivy_args'), getScheduleRetentionDays(), getEventRetentionDays(), getScheduleCleanupCron(), getEventCleanupCron(), getScheduleCleanupEnabled(), getEventCleanupEnabled(), getScannerCleanupCron(), getScannerCleanupEnabled(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), getEventCollectionMode(), getEventPollInterval(), getMetricsCollectionInterval(), getSetting('theme_light'), getSetting('theme_dark'), getSetting('theme_font'), getSetting('theme_font_size'), getSetting('theme_grid_font_size'), getSetting('theme_terminal_font'), getSetting('theme_editor_font'), getSetting('compact_ports'), getSetting('format_log_timestamps'), getExternalStackPaths(), getPrimaryStackLocation(), getSetting('default_grype_image'), getSetting('default_trivy_image'), getSetting('default_compose_template'), getSetting('label_filter_mode') ]); const settings: GeneralSettings = { confirmDestructive: confirmDestructiveVal ?? DEFAULT_SETTINGS.confirmDestructive, showStoppedContainers: showStoppedContainersVal ?? DEFAULT_SETTINGS.showStoppedContainers, highlightUpdates: highlightUpdatesVal ?? DEFAULT_SETTINGS.highlightUpdates, timeFormat: timeFormatVal ?? DEFAULT_SETTINGS.timeFormat, dateFormat: dateFormatVal ?? DEFAULT_SETTINGS.dateFormat, downloadFormat: downloadFormatVal ?? DEFAULT_SETTINGS.downloadFormat, defaultGrypeArgs: defaultGrypeArgsVal ?? DEFAULT_SETTINGS.defaultGrypeArgs, defaultTrivyArgs: defaultTrivyArgsVal ?? DEFAULT_SETTINGS.defaultTrivyArgs, scheduleRetentionDays: scheduleRetentionDaysVal, eventRetentionDays: eventRetentionDaysVal, scheduleCleanupCron: scheduleCleanupCronVal, eventCleanupCron: eventCleanupCronVal, scheduleCleanupEnabled: scheduleCleanupEnabledVal, eventCleanupEnabled: eventCleanupEnabledVal, scannerCleanupCron: scannerCleanupCronVal, scannerCleanupEnabled: scannerCleanupEnabledVal, logBufferSizeKb: logBufferSizeKbVal ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezoneVal ?? DEFAULT_SETTINGS.defaultTimezone, eventCollectionMode: (eventCollectionModeVal ?? DEFAULT_SETTINGS.eventCollectionMode) as EventCollectionMode, eventPollInterval: eventPollIntervalVal ?? DEFAULT_SETTINGS.eventPollInterval, metricsCollectionInterval: metricsCollectionIntervalVal ?? DEFAULT_SETTINGS.metricsCollectionInterval, lightTheme: lightThemeVal ?? DEFAULT_SETTINGS.lightTheme, darkTheme: darkThemeVal ?? DEFAULT_SETTINGS.darkTheme, font: fontVal ?? DEFAULT_SETTINGS.font, fontSize: fontSizeVal ?? DEFAULT_SETTINGS.fontSize, gridFontSize: gridFontSizeVal ?? DEFAULT_SETTINGS.gridFontSize, terminalFont: terminalFontVal ?? DEFAULT_SETTINGS.terminalFont, editorFont: editorFontVal ?? DEFAULT_SETTINGS.editorFont, compactPorts: compactPortsVal ?? DEFAULT_SETTINGS.compactPorts, formatLogTimestamps: formatLogTimestampsVal ?? DEFAULT_SETTINGS.formatLogTimestamps, externalStackPaths: externalStackPathsVal, primaryStackLocation: primaryStackLocationVal, defaultGrypeImage: defaultGrypeImageVal ?? DEFAULT_GRYPE_IMAGE, defaultTrivyImage: defaultTrivyImageVal ?? DEFAULT_TRIVY_IMAGE, defaultComposeTemplate: defaultComposeTemplateVal ?? DEFAULT_SETTINGS.defaultComposeTemplate, labelFilterMode: labelFilterModeVal ?? DEFAULT_SETTINGS.labelFilterMode }; return json(settings); } catch (error) { console.error('Failed to save general settings:', error); return json({ error: 'Failed to save general settings' }, { status: 500 }); } };