1.0.36
This commit is contained in:
63 files changed
+1324
-281
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user