Files
dockhand/lib/stores/settings.ts
Jarek Krochmalski 62e3c6439e Initial commit
2025-12-28 21:16:03 +01:00

366 lines
12 KiB
TypeScript

import { writable, derived, get } from 'svelte/store';
import { browser } from '$app/environment';
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 AppSettings {
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;
}
const DEFAULT_SETTINGS: AppSettings = {
confirmDestructive: true,
showStoppedContainers: true,
highlightUpdates: true,
timeFormat: '24h',
dateFormat: 'DD.MM.YYYY',
downloadFormat: 'tar',
defaultGrypeArgs: '-o json -v {image}',
defaultTrivyArgs: 'image --format json {image}',
scheduleRetentionDays: 30,
eventRetentionDays: 30,
scheduleCleanupCron: '0 3 * * *',
eventCleanupCron: '30 3 * * *',
scheduleCleanupEnabled: true,
eventCleanupEnabled: true,
logBufferSizeKb: 500,
defaultTimezone: 'UTC'
};
// Create a writable store for app settings
function createSettingsStore() {
const { subscribe, set, update } = writable<AppSettings>(DEFAULT_SETTINGS);
let initialized = false;
// Load settings from database on initialization
async function loadSettings() {
if (!browser || initialized) return;
initialized = true;
try {
const response = await fetch('/api/settings/general');
if (response.ok) {
const settings = await response.json();
set({
confirmDestructive: settings.confirmDestructive ?? DEFAULT_SETTINGS.confirmDestructive,
showStoppedContainers: settings.showStoppedContainers ?? DEFAULT_SETTINGS.showStoppedContainers,
highlightUpdates: settings.highlightUpdates ?? DEFAULT_SETTINGS.highlightUpdates,
timeFormat: settings.timeFormat ?? DEFAULT_SETTINGS.timeFormat,
dateFormat: settings.dateFormat ?? DEFAULT_SETTINGS.dateFormat,
downloadFormat: settings.downloadFormat ?? DEFAULT_SETTINGS.downloadFormat,
defaultGrypeArgs: settings.defaultGrypeArgs ?? DEFAULT_SETTINGS.defaultGrypeArgs,
defaultTrivyArgs: settings.defaultTrivyArgs ?? DEFAULT_SETTINGS.defaultTrivyArgs,
scheduleRetentionDays: settings.scheduleRetentionDays ?? DEFAULT_SETTINGS.scheduleRetentionDays,
eventRetentionDays: settings.eventRetentionDays ?? DEFAULT_SETTINGS.eventRetentionDays,
scheduleCleanupCron: settings.scheduleCleanupCron ?? DEFAULT_SETTINGS.scheduleCleanupCron,
eventCleanupCron: settings.eventCleanupCron ?? DEFAULT_SETTINGS.eventCleanupCron,
scheduleCleanupEnabled: settings.scheduleCleanupEnabled ?? DEFAULT_SETTINGS.scheduleCleanupEnabled,
eventCleanupEnabled: settings.eventCleanupEnabled ?? DEFAULT_SETTINGS.eventCleanupEnabled,
logBufferSizeKb: settings.logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb,
defaultTimezone: settings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone
});
}
} catch {
// Silently use defaults if settings can't be loaded
}
}
// Save settings to database
async function saveSettings(settings: Partial<AppSettings>) {
if (!browser) return;
try {
const response = await fetch('/api/settings/general', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(settings)
});
if (response.ok) {
const updatedSettings = await response.json();
set({
confirmDestructive: updatedSettings.confirmDestructive ?? DEFAULT_SETTINGS.confirmDestructive,
showStoppedContainers: updatedSettings.showStoppedContainers ?? DEFAULT_SETTINGS.showStoppedContainers,
highlightUpdates: updatedSettings.highlightUpdates ?? DEFAULT_SETTINGS.highlightUpdates,
timeFormat: updatedSettings.timeFormat ?? DEFAULT_SETTINGS.timeFormat,
dateFormat: updatedSettings.dateFormat ?? DEFAULT_SETTINGS.dateFormat,
downloadFormat: updatedSettings.downloadFormat ?? DEFAULT_SETTINGS.downloadFormat,
defaultGrypeArgs: updatedSettings.defaultGrypeArgs ?? DEFAULT_SETTINGS.defaultGrypeArgs,
defaultTrivyArgs: updatedSettings.defaultTrivyArgs ?? DEFAULT_SETTINGS.defaultTrivyArgs,
scheduleRetentionDays: updatedSettings.scheduleRetentionDays ?? DEFAULT_SETTINGS.scheduleRetentionDays,
eventRetentionDays: updatedSettings.eventRetentionDays ?? DEFAULT_SETTINGS.eventRetentionDays,
scheduleCleanupCron: updatedSettings.scheduleCleanupCron ?? DEFAULT_SETTINGS.scheduleCleanupCron,
eventCleanupCron: updatedSettings.eventCleanupCron ?? DEFAULT_SETTINGS.eventCleanupCron,
scheduleCleanupEnabled: updatedSettings.scheduleCleanupEnabled ?? DEFAULT_SETTINGS.scheduleCleanupEnabled,
eventCleanupEnabled: updatedSettings.eventCleanupEnabled ?? DEFAULT_SETTINGS.eventCleanupEnabled,
logBufferSizeKb: updatedSettings.logBufferSizeKb ?? DEFAULT_SETTINGS.logBufferSizeKb,
defaultTimezone: updatedSettings.defaultTimezone ?? DEFAULT_SETTINGS.defaultTimezone
});
}
} catch (error) {
console.error('Failed to save settings:', error);
}
}
// Load settings on store creation
if (browser) {
loadSettings();
}
return {
subscribe,
set: (value: AppSettings) => {
set(value);
saveSettings(value);
},
update: (fn: (settings: AppSettings) => AppSettings) => {
update((current) => {
const newSettings = fn(current);
saveSettings(newSettings);
return newSettings;
});
},
// Convenience methods for individual settings
setConfirmDestructive: (value: boolean) => {
update((current) => {
const newSettings = { ...current, confirmDestructive: value };
saveSettings({ confirmDestructive: value });
return newSettings;
});
},
setShowStoppedContainers: (value: boolean) => {
update((current) => {
const newSettings = { ...current, showStoppedContainers: value };
saveSettings({ showStoppedContainers: value });
return newSettings;
});
},
setHighlightUpdates: (value: boolean) => {
update((current) => {
const newSettings = { ...current, highlightUpdates: value };
saveSettings({ highlightUpdates: value });
return newSettings;
});
},
setTimeFormat: (value: TimeFormat) => {
update((current) => {
const newSettings = { ...current, timeFormat: value };
saveSettings({ timeFormat: value });
return newSettings;
});
},
setDateFormat: (value: DateFormat) => {
update((current) => {
const newSettings = { ...current, dateFormat: value };
saveSettings({ dateFormat: value });
return newSettings;
});
},
setDownloadFormat: (value: DownloadFormat) => {
update((current) => {
const newSettings = { ...current, downloadFormat: value };
saveSettings({ downloadFormat: value });
return newSettings;
});
},
setDefaultGrypeArgs: (value: string) => {
update((current) => {
const newSettings = { ...current, defaultGrypeArgs: value };
saveSettings({ defaultGrypeArgs: value });
return newSettings;
});
},
setDefaultTrivyArgs: (value: string) => {
update((current) => {
const newSettings = { ...current, defaultTrivyArgs: value };
saveSettings({ defaultTrivyArgs: value });
return newSettings;
});
},
setScheduleRetentionDays: (value: number) => {
update((current) => {
const newSettings = { ...current, scheduleRetentionDays: value };
saveSettings({ scheduleRetentionDays: value });
return newSettings;
});
},
setEventRetentionDays: (value: number) => {
update((current) => {
const newSettings = { ...current, eventRetentionDays: value };
saveSettings({ eventRetentionDays: value });
return newSettings;
});
},
setScheduleCleanupCron: (value: string) => {
update((current) => {
const newSettings = { ...current, scheduleCleanupCron: value };
saveSettings({ scheduleCleanupCron: value });
return newSettings;
});
},
setEventCleanupCron: (value: string) => {
update((current) => {
const newSettings = { ...current, eventCleanupCron: value };
saveSettings({ eventCleanupCron: value });
return newSettings;
});
},
setScheduleCleanupEnabled: (value: boolean) => {
update((current) => {
const newSettings = { ...current, scheduleCleanupEnabled: value };
saveSettings({ scheduleCleanupEnabled: value });
return newSettings;
});
},
setEventCleanupEnabled: (value: boolean) => {
update((current) => {
const newSettings = { ...current, eventCleanupEnabled: value };
saveSettings({ eventCleanupEnabled: value });
return newSettings;
});
},
setLogBufferSizeKb: (value: number) => {
update((current) => {
const newSettings = { ...current, logBufferSizeKb: value };
saveSettings({ logBufferSizeKb: value });
return newSettings;
});
},
setDefaultTimezone: (value: string) => {
update((current) => {
const newSettings = { ...current, defaultTimezone: value };
saveSettings({ defaultTimezone: value });
return newSettings;
});
},
// Manual refresh from database
refresh: loadSettings
};
}
export const appSettings = createSettingsStore();
// Cache current settings for synchronous access (updated reactively)
let cachedTimeFormat: TimeFormat = DEFAULT_SETTINGS.timeFormat;
let cachedDateFormat: DateFormat = DEFAULT_SETTINGS.dateFormat;
// Subscribe once to keep cache updated
if (browser) {
appSettings.subscribe((s) => {
cachedTimeFormat = s.timeFormat;
cachedDateFormat = s.dateFormat;
});
}
/**
* Format a date part according to user's date format preference.
* This is a low-level helper - prefer formatDateTime for most uses.
*/
function formatDatePart(d: Date): string {
const day = d.getDate().toString().padStart(2, '0');
const month = (d.getMonth() + 1).toString().padStart(2, '0');
const year = d.getFullYear();
switch (cachedDateFormat) {
case 'MM/DD/YYYY':
return `${month}/${day}/${year}`;
case 'DD/MM/YYYY':
return `${day}/${month}/${year}`;
case 'YYYY-MM-DD':
return `${year}-${month}-${day}`;
case 'DD.MM.YYYY':
default:
return `${day}.${month}.${year}`;
}
}
/**
* Format a time part according to user's time format preference.
* This is a low-level helper - prefer formatDateTime for most uses.
*/
function formatTimePart(d: Date, includeSeconds = false): string {
const hours = d.getHours();
const minutes = d.getMinutes().toString().padStart(2, '0');
const seconds = d.getSeconds().toString().padStart(2, '0');
if (cachedTimeFormat === '12h') {
const hour12 = hours === 0 ? 12 : hours > 12 ? hours - 12 : hours;
const ampm = hours >= 12 ? 'PM' : 'AM';
return includeSeconds
? `${hour12}:${minutes}:${seconds} ${ampm}`
: `${hour12}:${minutes} ${ampm}`;
} else {
const hour24 = hours.toString().padStart(2, '0');
return includeSeconds
? `${hour24}:${minutes}:${seconds}`
: `${hour24}:${minutes}`;
}
}
/**
* Format a timestamp according to user's time and date format preferences.
* Performant: uses cached settings, no store subscription per call.
*
* @param date - Date object, ISO string, or timestamp
* @param options - Formatting options
* @returns Formatted string
*/
export function formatTime(
date: Date | string | number,
options: { includeDate?: boolean; includeSeconds?: boolean } = {}
): string {
const d = date instanceof Date ? date : new Date(date);
const { includeDate = false, includeSeconds = false } = options;
if (includeDate) {
return `${formatDatePart(d)} ${formatTimePart(d, includeSeconds)}`;
}
return formatTimePart(d, includeSeconds);
}
/**
* Format a timestamp with date according to user's preferences.
* Convenience wrapper around formatTime.
*/
export function formatDateTime(date: Date | string | number, includeSeconds = false): string {
return formatTime(date, { includeDate: true, includeSeconds });
}
/**
* Format just the date part according to user's preferences.
*/
export function formatDate(date: Date | string | number): string {
const d = date instanceof Date ? date : new Date(date);
return formatDatePart(d);
}
/**
* Get the current time format setting (for components that need it).
*/
export function getTimeFormat(): TimeFormat {
return cachedTimeFormat;
}
/**
* Get the current date format setting (for components that need it).
*/
export function getDateFormat(): DateFormat {
return cachedDateFormat;
}