import { json, type RequestHandler } from '@sveltejs/kit'; import { getSetting, setSetting, getScheduleRetentionDays, setScheduleRetentionDays, getEventRetentionDays, setEventRetentionDays, getScheduleCleanupCron, setScheduleCleanupCron, getEventCleanupCron, setEventCleanupCron, getScheduleCleanupEnabled, setScheduleCleanupEnabled, getEventCleanupEnabled, setEventCleanupEnabled, getDefaultTimezone, setDefaultTimezone } from '$lib/server/db'; import { authorize } from '$lib/server/authorize'; import { refreshSystemJobs } from '$lib/server/scheduler'; 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 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; logBufferSizeKb: number; defaultTimezone: string; // Theme settings (for when auth is disabled) lightTheme: string; darkTheme: string; font: string; fontSize: string; gridFontSize: string; terminalFont: string; } 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', lightTheme: 'default', darkTheme: 'default', font: 'system', fontSize: 'normal', gridFontSize: 'normal', terminalFont: 'system-mono' }; 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', 'menlo', 'consolas', 'sf-mono']; 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, logBufferSizeKb, defaultTimezone, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont ] = 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(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), getSetting('theme_light'), getSetting('theme_dark'), getSetting('theme_font'), getSetting('theme_font_size'), getSetting('theme_grid_font_size'), getSetting('theme_terminal_font') ]); 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, logBufferSizeKb: logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone, 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 }; 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, logBufferSizeKb, defaultTimezone, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont } = 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 (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 (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); } // Fetch all settings in parallel for the response const [ confirmDestructiveVal, showStoppedContainersVal, highlightUpdatesVal, timeFormatVal, dateFormatVal, downloadFormatVal, defaultGrypeArgsVal, defaultTrivyArgsVal, scheduleRetentionDaysVal, eventRetentionDaysVal, scheduleCleanupCronVal, eventCleanupCronVal, scheduleCleanupEnabledVal, eventCleanupEnabledVal, logBufferSizeKbVal, defaultTimezoneVal, lightThemeVal, darkThemeVal, fontVal, fontSizeVal, gridFontSizeVal, terminalFontVal ] = 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(), getSetting('log_buffer_size_kb'), getDefaultTimezone(), getSetting('theme_light'), getSetting('theme_dark'), getSetting('theme_font'), getSetting('theme_font_size'), getSetting('theme_grid_font_size'), getSetting('theme_terminal_font') ]); 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, logBufferSizeKb: logBufferSizeKbVal ?? DEFAULT_SETTINGS.logBufferSizeKb, defaultTimezone: defaultTimezoneVal ?? DEFAULT_SETTINGS.defaultTimezone, 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 }; return json(settings); } catch (error) { console.error('Failed to save general settings:', error); return json({ error: 'Failed to save general settings' }, { status: 500 }); } };