This commit is contained in:
jarek committed 2026-06-27 15:15:54 +02:00
1 parent ce12d7ae9f
commit 685132ff89
63 files changed
+1324 -281

No files matched your search

+81 -7
View File
@@ -40,11 +40,16 @@ 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' | 'raw';
export type EventCollectionMode = 'stream' | 'poll';
export type ActionIconSize = 'small' | 'normal' | 'large' | 'xlarge';
const VALID_ACTION_ICON_SIZES: ActionIconSize[] = ['small', 'normal', 'large', 'xlarge'];
export interface GeneralSettings {
confirmDestructive: boolean;
showStoppedContainers: boolean;
highlightUpdates: boolean;
coloredActionButtons: boolean;
actionIconSize: ActionIconSize;
timeFormat: TimeFormat;
dateFormat: DateFormat;
downloadFormat: DownloadFormat;
@@ -102,12 +107,17 @@ export interface GeneralSettings {
animateIcons: boolean;
// Skip Dockhand's scanner images (grype, trivy) during 'prune all unused' (#625)
protectScannerImages: boolean;
// Scanner Advanced settings (#1219). Empty = use auto-detection.
defaultScannerNetworkMode: string;
defaultScannerDns: string[];
}
const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRetentionDays' | 'scheduleCleanupCron' | 'eventCleanupCron' | 'scheduleCleanupEnabled' | 'eventCleanupEnabled' | 'scannerCleanupCron' | 'scannerCleanupEnabled'> = {
confirmDestructive: true,
showStoppedContainers: true,
highlightUpdates: true,
coloredActionButtons: false,
actionIconSize: 'normal' as const,
timeFormat: '24h',
dateFormat: 'DD.MM.YYYY',
downloadFormat: 'tar',
@@ -138,6 +148,8 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
showImageChangelogLinks: true,
animateIcons: true,
protectScannerImages: true,
defaultScannerNetworkMode: '',
defaultScannerDns: [],
defaultComposeTemplate: `version: "3.8"
services:
@@ -167,6 +179,24 @@ const VALID_EDITOR_FONTS = VALID_TERMINAL_FONTS;
const VALID_DATE_FORMATS: DateFormat[] = ['MM/DD/YYYY', 'DD/MM/YYYY', 'YYYY-MM-DD', 'DD.MM.YYYY'];
// Scanner DNS is stored as a JSON-stringified array in the settings KV store
// (consistent with externalStackPaths). Tolerate the legacy/hand-edited
// comma-separated form too.
function parseScannerDnsStorage(raw: string | null | undefined): string[] {
if (!raw) return [];
const trimmed = raw.trim();
if (!trimmed) return [];
if (trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed);
return Array.isArray(parsed) ? parsed.filter((v) => typeof v === 'string') : [];
} catch {
return [];
}
}
return trimmed.split(',').map((s) => s.trim()).filter(Boolean);
}
export const GET: RequestHandler = async ({ cookies }) => {
const auth = await authorize(cookies);
// UI preferences (time format, date format) should be available to all authenticated users
@@ -181,6 +211,8 @@ export const GET: RequestHandler = async ({ cookies }) => {
confirmDestructive,
showStoppedContainers,
highlightUpdates,
coloredActionButtons,
actionIconSize,
timeFormat,
dateFormat,
downloadFormat,
@@ -219,11 +251,15 @@ export const GET: RequestHandler = async ({ cookies }) => {
honorProxyLabels,
showImageChangelogLinks,
animateIcons,
protectScannerImages
protectScannerImages,
defaultScannerNetworkMode,
defaultScannerDnsRaw
] = await Promise.all([
getSetting('confirm_destructive'),
getSetting('show_stopped_containers'),
getSetting('highlight_updates'),
getSetting('colored_action_buttons'),
getSetting('action_icon_size'),
getSetting('time_format'),
getSetting('date_format'),
getSetting('download_format'),
@@ -262,13 +298,17 @@ export const GET: RequestHandler = async ({ cookies }) => {
getSetting('honor_proxy_labels'),
getSetting('show_image_changelog_links'),
getSetting('animate_icons'),
getSetting('protect_scanner_images')
getSetting('protect_scanner_images'),
getSetting('default_scanner_network_mode'),
getSetting('default_scanner_dns')
]);
const settings: GeneralSettings = {
confirmDestructive: confirmDestructive ?? DEFAULT_SETTINGS.confirmDestructive,
showStoppedContainers: showStoppedContainers ?? DEFAULT_SETTINGS.showStoppedContainers,
highlightUpdates: highlightUpdates ?? DEFAULT_SETTINGS.highlightUpdates,
coloredActionButtons: coloredActionButtons ?? DEFAULT_SETTINGS.coloredActionButtons,
actionIconSize: (VALID_ACTION_ICON_SIZES.includes(actionIconSize as ActionIconSize) ? actionIconSize : DEFAULT_SETTINGS.actionIconSize) as ActionIconSize,
timeFormat: timeFormat ?? DEFAULT_SETTINGS.timeFormat,
dateFormat: dateFormat ?? DEFAULT_SETTINGS.dateFormat,
downloadFormat: downloadFormat ?? DEFAULT_SETTINGS.downloadFormat,
@@ -309,7 +349,9 @@ export const GET: RequestHandler = async ({ cookies }) => {
honorProxyLabels: honorProxyLabels ?? DEFAULT_SETTINGS.honorProxyLabels,
showImageChangelogLinks: showImageChangelogLinks ?? DEFAULT_SETTINGS.showImageChangelogLinks,
animateIcons: animateIcons ?? DEFAULT_SETTINGS.animateIcons,
protectScannerImages: protectScannerImages ?? DEFAULT_SETTINGS.protectScannerImages
protectScannerImages: protectScannerImages ?? DEFAULT_SETTINGS.protectScannerImages,
defaultScannerNetworkMode: defaultScannerNetworkMode ?? DEFAULT_SETTINGS.defaultScannerNetworkMode,
defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRaw)
};
return json(settings);
@@ -327,7 +369,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
try {
const body = await request.json();
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, honorProxyLabels, showImageChangelogLinks, animateIcons, protectScannerImages } = body;
const { confirmDestructive, showStoppedContainers, highlightUpdates, coloredActionButtons, actionIconSize, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, scannerCleanupCron, scannerCleanupEnabled, logBufferSizeKb, logMaxLines, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, showExposedPorts, formatLogTimestamps, externalStackPaths, primaryStackLocation, defaultGrypeImage, defaultTrivyImage, defaultComposeTemplate, labelFilterMode, honorProxyLabels, showImageChangelogLinks, animateIcons, protectScannerImages, defaultScannerNetworkMode, defaultScannerDns } = body;
if (confirmDestructive !== undefined) {
await setSetting('confirm_destructive', confirmDestructive);
@@ -338,6 +380,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
if (highlightUpdates !== undefined) {
await setSetting('highlight_updates', highlightUpdates);
}
if (coloredActionButtons !== undefined) {
await setSetting('colored_action_buttons', coloredActionButtons);
}
if (actionIconSize !== undefined && VALID_ACTION_ICON_SIZES.includes(actionIconSize)) {
await setSetting('action_icon_size', actionIconSize);
}
if (timeFormat !== undefined && (timeFormat === '12h' || timeFormat === '24h')) {
await setSetting('time_format', timeFormat);
}
@@ -479,12 +527,28 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
if (protectScannerImages !== undefined && typeof protectScannerImages === 'boolean') {
await setSetting('protect_scanner_images', protectScannerImages);
}
if (defaultScannerNetworkMode !== undefined && typeof defaultScannerNetworkMode === 'string') {
// Free-form network mode (host / bridge / none / custom-network-name / '').
// Trim and cap length to avoid pathological values from a corrupted UI.
const trimmed = defaultScannerNetworkMode.trim().slice(0, 200);
await setSetting('default_scanner_network_mode', trimmed);
}
if (defaultScannerDns !== undefined && Array.isArray(defaultScannerDns)) {
const cleaned = defaultScannerDns
.filter((d) => typeof d === 'string')
.map((d: string) => d.trim())
.filter((d) => d.length > 0)
.slice(0, 10); // sane upper bound; Docker accepts more but no one needs it
await setSetting('default_scanner_dns', JSON.stringify(cleaned));
}
// Fetch all settings in parallel for the response
const [
confirmDestructiveVal,
showStoppedContainersVal,
highlightUpdatesVal,
coloredActionButtonsVal,
actionIconSizeVal,
timeFormatVal,
dateFormatVal,
downloadFormatVal,
@@ -523,11 +587,15 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
honorProxyLabelsVal,
showImageChangelogLinksVal,
animateIconsVal,
protectScannerImagesVal
protectScannerImagesVal,
defaultScannerNetworkModeVal,
defaultScannerDnsRawVal
] = await Promise.all([
getSetting('confirm_destructive'),
getSetting('show_stopped_containers'),
getSetting('highlight_updates'),
getSetting('colored_action_buttons'),
getSetting('action_icon_size'),
getSetting('time_format'),
getSetting('date_format'),
getSetting('download_format'),
@@ -566,13 +634,17 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
getSetting('honor_proxy_labels'),
getSetting('show_image_changelog_links'),
getSetting('animate_icons'),
getSetting('protect_scanner_images')
getSetting('protect_scanner_images'),
getSetting('default_scanner_network_mode'),
getSetting('default_scanner_dns')
]);
const settings: GeneralSettings = {
confirmDestructive: confirmDestructiveVal ?? DEFAULT_SETTINGS.confirmDestructive,
showStoppedContainers: showStoppedContainersVal ?? DEFAULT_SETTINGS.showStoppedContainers,
highlightUpdates: highlightUpdatesVal ?? DEFAULT_SETTINGS.highlightUpdates,
coloredActionButtons: coloredActionButtonsVal ?? DEFAULT_SETTINGS.coloredActionButtons,
actionIconSize: (VALID_ACTION_ICON_SIZES.includes(actionIconSizeVal as ActionIconSize) ? actionIconSizeVal : DEFAULT_SETTINGS.actionIconSize) as ActionIconSize,
timeFormat: timeFormatVal ?? DEFAULT_SETTINGS.timeFormat,
dateFormat: dateFormatVal ?? DEFAULT_SETTINGS.dateFormat,
downloadFormat: downloadFormatVal ?? DEFAULT_SETTINGS.downloadFormat,
@@ -613,7 +685,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
honorProxyLabels: honorProxyLabelsVal ?? DEFAULT_SETTINGS.honorProxyLabels,
protectScannerImages: protectScannerImagesVal ?? DEFAULT_SETTINGS.protectScannerImages,
showImageChangelogLinks: showImageChangelogLinksVal ?? DEFAULT_SETTINGS.showImageChangelogLinks,
animateIcons: animateIconsVal ?? DEFAULT_SETTINGS.animateIcons
animateIcons: animateIconsVal ?? DEFAULT_SETTINGS.animateIcons,
defaultScannerNetworkMode: defaultScannerNetworkModeVal ?? DEFAULT_SETTINGS.defaultScannerNetworkMode,
defaultScannerDns: parseScannerDnsStorage(defaultScannerDnsRawVal)
};
return json(settings);
+12 -4
View File
@@ -14,7 +14,9 @@ const DEFAULT_THEME_SETTINGS = {
gridFontSize: 'normal',
terminalFont: 'system-mono',
editorFont: 'system-mono',
animateIcons: true
animateIcons: true,
coloredActionButtons: false,
actionIconSize: 'normal'
};
export const GET: RequestHandler = async () => {
@@ -27,7 +29,9 @@ export const GET: RequestHandler = async () => {
gridFontSize,
terminalFont,
editorFont,
animateIcons
animateIcons,
coloredActionButtons,
actionIconSize
] = await Promise.all([
getSetting('theme_light'),
getSetting('theme_dark'),
@@ -36,7 +40,9 @@ export const GET: RequestHandler = async () => {
getSetting('theme_grid_font_size'),
getSetting('theme_terminal_font'),
getSetting('theme_editor_font'),
getSetting('animate_icons')
getSetting('animate_icons'),
getSetting('colored_action_buttons'),
getSetting('action_icon_size')
]);
return json({
@@ -47,7 +53,9 @@ export const GET: RequestHandler = async () => {
gridFontSize: gridFontSize ?? DEFAULT_THEME_SETTINGS.gridFontSize,
terminalFont: terminalFont ?? DEFAULT_THEME_SETTINGS.terminalFont,
editorFont: editorFont ?? DEFAULT_THEME_SETTINGS.editorFont,
animateIcons: animateIcons ?? DEFAULT_THEME_SETTINGS.animateIcons
animateIcons: animateIcons ?? DEFAULT_THEME_SETTINGS.animateIcons,
coloredActionButtons: coloredActionButtons ?? DEFAULT_THEME_SETTINGS.coloredActionButtons,
actionIconSize: actionIconSize ?? DEFAULT_THEME_SETTINGS.actionIconSize
});
} catch (error) {
console.error('Failed to get theme settings:', error);