1.0.13
This commit is contained in:
1 parent
a88d3d5788
commit
8f3a7eb435
96 files changed
+5235
-1870
No files matched your search
@@ -72,8 +72,11 @@
|
||||
import { focusFirstInput } from '$lib/utils';
|
||||
import { authStore, canAccess } from '$lib/stores/auth';
|
||||
import { licenseStore } from '$lib/stores/license';
|
||||
import { formatDateTime } from '$lib/stores/settings';
|
||||
import { getLabelColor, getLabelBgColor, parseLabels, MAX_LABELS } from '$lib/utils/label-colors';
|
||||
import EventTypesEditor from './EventTypesEditor.svelte';
|
||||
import UpdatesTab from './tabs/UpdatesTab.svelte';
|
||||
import ActivityTab from './tabs/ActivityTab.svelte';
|
||||
|
||||
// Scanner options for ToggleGroup
|
||||
const scannerOptions = [
|
||||
@@ -366,6 +369,14 @@
|
||||
let updateCheckVulnerabilityCriteria = $state<VulnerabilityCriteria>('never');
|
||||
let updateCheckLoading = $state(false);
|
||||
|
||||
// Image prune settings state
|
||||
let imagePruneEnabled = $state(false);
|
||||
let imagePruneCron = $state('0 3 * * 0'); // Default: 3 AM Sunday
|
||||
let imagePruneMode = $state<'dangling' | 'all'>('dangling');
|
||||
let imagePruneLastPruned = $state<string | undefined>(undefined);
|
||||
let imagePruneLastResult = $state<{ spaceReclaimed: number; imagesRemoved: number } | undefined>(undefined);
|
||||
let imagePruneLoading = $state(false);
|
||||
|
||||
// === Validation Functions ===
|
||||
function isValidHost(host: string): boolean {
|
||||
if (!host) return false;
|
||||
@@ -419,10 +430,11 @@
|
||||
hawserToken = null;
|
||||
generatedToken = null;
|
||||
pendingToken = null;
|
||||
// Load scanner settings, notifications, update check settings, and timezone
|
||||
// Load scanner settings, notifications, update check settings, image prune settings, and timezone
|
||||
loadScannerSettings(environment.id);
|
||||
loadEnvNotifications(environment.id);
|
||||
loadUpdateCheckSettings(environment.id);
|
||||
loadImagePruneSettings(environment.id);
|
||||
loadTimezone(environment.id);
|
||||
// Load Hawser token if edge mode
|
||||
if (formConnectionType === 'hawser-edge') {
|
||||
@@ -461,6 +473,12 @@
|
||||
updateCheckEnabled = false;
|
||||
updateCheckCron = '0 4 * * *';
|
||||
updateCheckAutoUpdate = false;
|
||||
// Reset image prune settings
|
||||
imagePruneEnabled = false;
|
||||
imagePruneCron = '0 3 * * 0';
|
||||
imagePruneMode = 'dangling';
|
||||
imagePruneLastPruned = undefined;
|
||||
imagePruneLastResult = undefined;
|
||||
// Load default timezone from global settings
|
||||
loadDefaultTimezone();
|
||||
}
|
||||
@@ -664,6 +682,10 @@
|
||||
if (updateCheckEnabled && newEnv?.id) {
|
||||
await saveUpdateCheckSettings(newEnv.id);
|
||||
}
|
||||
// Save image prune settings if enabled
|
||||
if (imagePruneEnabled && newEnv?.id) {
|
||||
await saveImagePruneSettings(newEnv.id);
|
||||
}
|
||||
// Save timezone if not default
|
||||
if (newEnv?.id) {
|
||||
await saveTimezone(newEnv.id);
|
||||
@@ -735,6 +757,7 @@
|
||||
if (response.ok) {
|
||||
await saveScannerSettings(environment.id);
|
||||
await saveUpdateCheckSettings(environment.id);
|
||||
await saveImagePruneSettings(environment.id);
|
||||
await saveTimezone(environment.id);
|
||||
toast.success(`Updated environment: ${formName}`);
|
||||
onSaved();
|
||||
@@ -897,6 +920,51 @@
|
||||
}
|
||||
}
|
||||
|
||||
// === Image Prune Settings Functions ===
|
||||
async function loadImagePruneSettings(envId: number) {
|
||||
imagePruneLoading = true;
|
||||
try {
|
||||
const response = await fetch(`/api/environments/${envId}/image-prune`);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.settings) {
|
||||
imagePruneEnabled = data.settings.enabled ?? false;
|
||||
imagePruneCron = data.settings.cronExpression || '0 3 * * 0';
|
||||
imagePruneMode = data.settings.pruneMode || 'dangling';
|
||||
imagePruneLastPruned = data.settings.lastPruned;
|
||||
imagePruneLastResult = data.settings.lastResult;
|
||||
} else {
|
||||
// No settings found - use defaults
|
||||
imagePruneEnabled = false;
|
||||
imagePruneCron = '0 3 * * 0';
|
||||
imagePruneMode = 'dangling';
|
||||
imagePruneLastPruned = undefined;
|
||||
imagePruneLastResult = undefined;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load image prune settings:', error);
|
||||
} finally {
|
||||
imagePruneLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveImagePruneSettings(envId: number) {
|
||||
try {
|
||||
await fetch(`/api/environments/${envId}/image-prune`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
enabled: imagePruneEnabled,
|
||||
cronExpression: imagePruneCron,
|
||||
pruneMode: imagePruneMode
|
||||
})
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save image prune settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeGrype(envId?: number) {
|
||||
removingGrype = true;
|
||||
try {
|
||||
@@ -1929,117 +1997,30 @@
|
||||
|
||||
<!-- Updates Tab -->
|
||||
<Tabs.Content value="updates" class="space-y-4 mt-0 h-full">
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm font-medium">
|
||||
Scheduled update check
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Periodically check all containers in this environment for available image updates.
|
||||
</p>
|
||||
|
||||
{#if updateCheckLoading}
|
||||
<div class="flex items-center justify-center py-4">
|
||||
<RefreshCw class="w-5 h-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-start gap-2">
|
||||
<CircleFadingArrowUp class="w-4 h-4 text-green-500 glow-green mt-0.5 shrink-0" />
|
||||
<div class="flex-1">
|
||||
<Label>Enable scheduled update check</Label>
|
||||
<p class="text-xs text-muted-foreground">Automatically check for container updates on a schedule</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={updateCheckEnabled} />
|
||||
</div>
|
||||
|
||||
{#if updateCheckEnabled}
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-4 shrink-0"></div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<Label>Schedule</Label>
|
||||
<CronEditor value={updateCheckCron} onchange={(cron) => updateCheckCron = cron} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2">
|
||||
<CircleArrowUp class="w-4 h-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div class="flex-1">
|
||||
<Label>Automatically update containers</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
When enabled, containers will be updated automatically when new images are found.
|
||||
When disabled, only sends notifications about available updates.
|
||||
</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={updateCheckAutoUpdate} />
|
||||
</div>
|
||||
|
||||
{#if updateCheckAutoUpdate && scannerEnabled}
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-4 shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<Label>Block updates with vulnerabilities</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Block auto-updates if the new image has vulnerabilities exceeding this criteria
|
||||
</p>
|
||||
</div>
|
||||
<VulnerabilityCriteriaSelector
|
||||
bind:value={updateCheckVulnerabilityCriteria}
|
||||
class="w-[200px]"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text-xs text-muted-foreground bg-muted/50 rounded-md p-2 flex items-start gap-2">
|
||||
<Info class="w-3 h-3 mt-0.5 shrink-0" />
|
||||
{#if updateCheckAutoUpdate}
|
||||
{#if scannerEnabled && updateCheckVulnerabilityCriteria !== 'never'}
|
||||
<span>New images are pulled to a temporary tag, scanned, then deployed if they pass the vulnerability check. Blocked images are deleted automatically.</span>
|
||||
{:else}
|
||||
<span>Containers will be updated automatically when new images are available.</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span>You'll receive notifications when updates are available. Containers won't be modified.</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Timezone selector -->
|
||||
<div class="space-y-2">
|
||||
<Label>Timezone</Label>
|
||||
<TimezoneSelector
|
||||
bind:value={formTimezone}
|
||||
id="edit-env-timezone"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Used for scheduling auto-updates and git syncs
|
||||
</p>
|
||||
</div>
|
||||
<UpdatesTab
|
||||
updateCheckLoading={updateCheckLoading}
|
||||
bind:updateCheckEnabled={updateCheckEnabled}
|
||||
bind:updateCheckCron={updateCheckCron}
|
||||
bind:updateCheckAutoUpdate={updateCheckAutoUpdate}
|
||||
bind:updateCheckVulnerabilityCriteria={updateCheckVulnerabilityCriteria}
|
||||
scannerEnabled={scannerEnabled}
|
||||
imagePruneLoading={imagePruneLoading}
|
||||
bind:imagePruneEnabled={imagePruneEnabled}
|
||||
bind:imagePruneCron={imagePruneCron}
|
||||
bind:imagePruneMode={imagePruneMode}
|
||||
imagePruneLastPruned={imagePruneLastPruned}
|
||||
imagePruneLastResult={imagePruneLastResult}
|
||||
bind:timezone={formTimezone}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Activity Tab -->
|
||||
<Tabs.Content value="activity" class="space-y-4 mt-0 h-full">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<Label>Collect container activity</Label>
|
||||
<p class="text-xs text-muted-foreground">Track container events (start, stop, restart, etc.) from this environment in real-time</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={formCollectActivity} />
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<Label>Collect system metrics</Label>
|
||||
<p class="text-xs text-muted-foreground">Collect CPU and memory usage statistics from this environment</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={formCollectMetrics} />
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<Label>Highlight value changes</Label>
|
||||
<p class="text-xs text-muted-foreground">Show amber glow when container values change in the containers list</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={formHighlightChanges} />
|
||||
</div>
|
||||
<ActivityTab
|
||||
bind:collectActivity={formCollectActivity}
|
||||
bind:collectMetrics={formCollectMetrics}
|
||||
bind:highlightChanges={formHighlightChanges}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- Security Tab -->
|
||||
|
||||
@@ -63,6 +63,7 @@
|
||||
updatedAt: string;
|
||||
updateCheckEnabled?: boolean;
|
||||
updateCheckAutoUpdate?: boolean;
|
||||
imagePruneEnabled?: boolean;
|
||||
timezone?: string;
|
||||
hawserVersion?: string;
|
||||
}
|
||||
@@ -479,7 +480,12 @@
|
||||
<Cpu class="w-4 h-4 text-sky-400 glow-sky" />
|
||||
</span>
|
||||
{/if}
|
||||
{#if !env.updateCheckEnabled && !hasScannerEnabled && !env.collectActivity && !env.collectMetrics}
|
||||
{#if env.imagePruneEnabled}
|
||||
<span title="Automatic image pruning enabled">
|
||||
<Trash2 class="w-4 h-4 text-amber-500 glow-amber" />
|
||||
</span>
|
||||
{/if}
|
||||
{#if !env.updateCheckEnabled && !hasScannerEnabled && !env.collectActivity && !env.collectMetrics && !env.imagePruneEnabled}
|
||||
<span class="text-muted-foreground text-xs">—</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { TogglePill } from '$lib/components/ui/toggle-pill';
|
||||
|
||||
interface Props {
|
||||
collectActivity: boolean;
|
||||
collectMetrics: boolean;
|
||||
highlightChanges: boolean;
|
||||
}
|
||||
|
||||
let {
|
||||
collectActivity = $bindable(),
|
||||
collectMetrics = $bindable(),
|
||||
highlightChanges = $bindable()
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<Label>Collect container activity</Label>
|
||||
<p class="text-xs text-muted-foreground">Track container events (start, stop, restart, etc.) from this environment in real-time</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={collectActivity} />
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<Label>Collect system metrics</Label>
|
||||
<p class="text-xs text-muted-foreground">Collect CPU and memory usage statistics from this environment</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={collectMetrics} />
|
||||
</div>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-1">
|
||||
<Label>Highlight value changes</Label>
|
||||
<p class="text-xs text-muted-foreground">Show amber glow when container values change in the containers list</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={highlightChanges} />
|
||||
</div>
|
||||
@@ -0,0 +1,219 @@
|
||||
<script lang="ts">
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import * as Select from '$lib/components/ui/select';
|
||||
import { TogglePill } from '$lib/components/ui/toggle-pill';
|
||||
import CronEditor from '$lib/components/cron-editor.svelte';
|
||||
import TimezoneSelector from '$lib/components/TimezoneSelector.svelte';
|
||||
import VulnerabilityCriteriaSelector, { type VulnerabilityCriteria } from '$lib/components/VulnerabilityCriteriaSelector.svelte';
|
||||
import { CircleFadingArrowUp, CircleArrowUp, RefreshCw, Info, Trash2 } from 'lucide-svelte';
|
||||
import { formatDateTime } from '$lib/stores/settings';
|
||||
|
||||
interface Props {
|
||||
// Update check settings
|
||||
updateCheckLoading: boolean;
|
||||
updateCheckEnabled: boolean;
|
||||
updateCheckCron: string;
|
||||
updateCheckAutoUpdate: boolean;
|
||||
updateCheckVulnerabilityCriteria: VulnerabilityCriteria;
|
||||
scannerEnabled: boolean;
|
||||
// Image prune settings
|
||||
imagePruneLoading: boolean;
|
||||
imagePruneEnabled: boolean;
|
||||
imagePruneCron: string;
|
||||
imagePruneMode: 'dangling' | 'all';
|
||||
imagePruneLastPruned?: string;
|
||||
imagePruneLastResult?: { spaceReclaimed: number; imagesRemoved: number };
|
||||
// Timezone
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
let {
|
||||
updateCheckLoading,
|
||||
updateCheckEnabled = $bindable(),
|
||||
updateCheckCron = $bindable(),
|
||||
updateCheckAutoUpdate = $bindable(),
|
||||
updateCheckVulnerabilityCriteria = $bindable(),
|
||||
scannerEnabled,
|
||||
imagePruneLoading,
|
||||
imagePruneEnabled = $bindable(),
|
||||
imagePruneCron = $bindable(),
|
||||
imagePruneMode = $bindable(),
|
||||
imagePruneLastPruned,
|
||||
imagePruneLastResult,
|
||||
timezone = $bindable()
|
||||
}: Props = $props();
|
||||
|
||||
// Format bytes to human-readable string
|
||||
function formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Scheduled Update Check Section -->
|
||||
<div class="space-y-4">
|
||||
<div class="text-sm font-medium">
|
||||
Scheduled update check
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Periodically check all containers in this environment for available image updates.
|
||||
</p>
|
||||
|
||||
{#if updateCheckLoading}
|
||||
<div class="flex items-center justify-center py-4">
|
||||
<RefreshCw class="w-5 h-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-start gap-2">
|
||||
<CircleFadingArrowUp class="w-4 h-4 text-green-500 glow-green mt-0.5 shrink-0" />
|
||||
<div class="flex-1">
|
||||
<Label>Enable scheduled update check</Label>
|
||||
<p class="text-xs text-muted-foreground">Automatically check for container updates on a schedule</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={updateCheckEnabled} />
|
||||
</div>
|
||||
|
||||
{#if updateCheckEnabled}
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-4 shrink-0"></div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<Label>Schedule</Label>
|
||||
<CronEditor value={updateCheckCron} onchange={(cron) => updateCheckCron = cron} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2">
|
||||
<CircleArrowUp class="w-4 h-4 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div class="flex-1">
|
||||
<Label>Automatically update containers</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
When enabled, containers will be updated automatically when new images are found.
|
||||
When disabled, only sends notifications about available updates.
|
||||
</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={updateCheckAutoUpdate} />
|
||||
</div>
|
||||
|
||||
{#if updateCheckAutoUpdate && scannerEnabled}
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-4 shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<Label>Block updates with vulnerabilities</Label>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Block auto-updates if the new image has vulnerabilities exceeding this criteria
|
||||
</p>
|
||||
</div>
|
||||
<VulnerabilityCriteriaSelector
|
||||
bind:value={updateCheckVulnerabilityCriteria}
|
||||
class="w-[200px]"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text-xs text-muted-foreground bg-muted/50 rounded-md p-2 flex items-start gap-2">
|
||||
<Info class="w-3 h-3 mt-0.5 shrink-0" />
|
||||
{#if updateCheckAutoUpdate}
|
||||
{#if scannerEnabled && updateCheckVulnerabilityCriteria !== 'never'}
|
||||
<span>New images are pulled to a temporary tag, scanned, then deployed if they pass the vulnerability check. Blocked images are deleted automatically.</span>
|
||||
{:else}
|
||||
<span>Containers will be updated automatically when new images are available.</span>
|
||||
{/if}
|
||||
{:else}
|
||||
<span>You'll receive notifications when updates are available. Containers won't be modified.</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Image Pruning Section -->
|
||||
<div class="space-y-4 pt-4 border-t">
|
||||
<div class="text-sm font-medium">
|
||||
Automatic image pruning
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Automatically remove unused Docker images on a schedule to free up disk space.
|
||||
</p>
|
||||
|
||||
{#if imagePruneLoading}
|
||||
<div class="flex items-center justify-center py-4">
|
||||
<RefreshCw class="w-5 h-5 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex items-start gap-2">
|
||||
<Trash2 class="w-4 h-4 text-amber-500 glow-amber mt-0.5 shrink-0" />
|
||||
<div class="flex-1">
|
||||
<Label>Enable automatic image pruning</Label>
|
||||
<p class="text-xs text-muted-foreground">Automatically remove unused images on a schedule</p>
|
||||
</div>
|
||||
<TogglePill bind:checked={imagePruneEnabled} />
|
||||
</div>
|
||||
|
||||
{#if imagePruneEnabled}
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-4 shrink-0"></div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<Label>Schedule</Label>
|
||||
<CronEditor value={imagePruneCron} onchange={(cron) => imagePruneCron = cron} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-4 shrink-0"></div>
|
||||
<div class="flex-1 space-y-2">
|
||||
<Label>Prune mode</Label>
|
||||
<Select.Root type="single" bind:value={imagePruneMode}>
|
||||
<Select.Trigger class="w-full">
|
||||
{imagePruneMode === 'dangling' ? 'Dangling images only' : 'All unused images'}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
<Select.Item value="dangling">Dangling images only</Select.Item>
|
||||
<Select.Item value="all">All unused images</Select.Item>
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
{#if imagePruneMode === 'dangling'}
|
||||
Only removes untagged image layers (safest option)
|
||||
{:else}
|
||||
Removes all images not used by any container (more aggressive)
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if imagePruneLastPruned}
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="w-4 shrink-0"></div>
|
||||
<div class="flex-1">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Last pruned: {formatDateTime(imagePruneLastPruned)}
|
||||
{#if imagePruneLastResult}
|
||||
- {imagePruneLastResult.imagesRemoved} images removed, {formatBytes(imagePruneLastResult.spaceReclaimed)} reclaimed
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="text-xs text-muted-foreground bg-muted/50 rounded-md p-2 flex items-start gap-2">
|
||||
<Info class="w-3 h-3 mt-0.5 shrink-0" />
|
||||
<span>Images in use by running or stopped containers will never be removed.</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Timezone selector -->
|
||||
<div class="space-y-2">
|
||||
<Label>Timezone</Label>
|
||||
<TimezoneSelector
|
||||
bind:value={timezone}
|
||||
id="edit-env-timezone"
|
||||
/>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Used for scheduling auto-updates, git syncs, and image pruning
|
||||
</p>
|
||||
</div>
|
||||
@@ -128,20 +128,22 @@
|
||||
<Card.Title class="text-sm font-medium flex items-center gap-2">
|
||||
<Eye class="w-4 h-4" />
|
||||
Appearance
|
||||
{#if !$authStore.authEnabled}
|
||||
<Tooltip.Provider delayDuration={100}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<HelpCircle class="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right" sideOffset={8} class="!w-80">
|
||||
Theme and font settings are global when authentication is disabled. When auth is enabled, users can customize their appearance in their profile.
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
{/if}
|
||||
<Tooltip.Provider delayDuration={100}>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger>
|
||||
<HelpCircle class="w-4 h-4 text-muted-foreground cursor-help" />
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="right" sideOffset={8} class="!w-80">
|
||||
{#if $authStore.authEnabled}
|
||||
These settings apply to the login page and as defaults. Personal preferences can be configured in your profile.
|
||||
{:else}
|
||||
Theme and font settings are global when authentication is disabled.
|
||||
{/if}
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Tooltip.Provider>
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
@@ -225,20 +227,18 @@
|
||||
<p class="text-xs text-muted-foreground">How dates are displayed throughout the app</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Right column: Theme settings (only when auth disabled) -->
|
||||
{#if !$authStore.authEnabled}
|
||||
<div class="space-y-4">
|
||||
<ThemeSelector />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="text-xs text-muted-foreground flex items-start gap-1.5">
|
||||
<HelpCircle class="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p>Appearance settings (theme, fonts) are personal when auth is enabled.</p>
|
||||
<a href="/profile" class="text-primary hover:underline">Configure in your profile</a>
|
||||
<!-- Right column: Theme settings (always shown, with hint when auth enabled) -->
|
||||
<div class="space-y-4">
|
||||
<ThemeSelector />
|
||||
{#if $authStore.authEnabled}
|
||||
<div class="text-xs text-muted-foreground flex items-start gap-1.5 mt-2 p-2 bg-muted/50 rounded-md">
|
||||
<HelpCircle class="w-3.5 h-3.5 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p>Personal theme preferences can be configured in your <a href="/profile" class="text-primary hover:underline">profile</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
Reference in new issue
Block a user