-
- {#if $licenseStore.isEnterprise && total > 0}
-
- Showing {visibleStart}-{visibleEnd} of {total}
-
- {/if}
-
- {#if $licenseStore.isEnterprise}
-
-
-
-
- {$auditSseConnected ? 'Live' : 'Connecting'}
-
-
-
-
- {#if showExportMenu}
-
-
-
-
-
- {/if}
-
-
- {/if}
+
+
+
0 ? `${visibleStart}-${visibleEnd}` : undefined} total={total > 0 ? total : undefined} countClass="min-w-32" />
-
- {#if $licenseStore.loading}
-
-
- {:else if !$licenseStore.isEnterprise}
-
-
-
-
-
-
Enterprise feature
-
- Audit logging is an enterprise feature that tracks all user actions for compliance and security monitoring.
-
-
-
- {:else}
-
-
-
-
-
- Filters
-
-
-
-
-
-
- {#if filterUsernames.length === 0}
- All users
- {:else if filterUsernames.length === 1}
- {filterUsernames[0]}
- {:else}
- {filterUsernames.length} users
- {/if}
-
-
-
- {#if filterUsernames.length > 0}
-
+ {#if $licenseStore.isEnterprise}
+
+
+
+
+
+
+ {#if filterUsernames.length === 0}
+ User
+ {:else if filterUsernames.length === 1}
+ {filterUsernames[0]}
+ {:else}
+ {filterUsernames.length} users
{/if}
- {#each users as user}
-
-
- {user}
-
- {/each}
-
-
+
+
+
+ {#if filterUsernames.length > 0}
+
+ {/if}
+ {#each users as user}
+
+
+ {user}
+
+ {/each}
+
+
-
-
-
-
-
- {#if filterEntityTypes.length === 0}
- All entities
- {:else if filterEntityTypes.length === 1}
- {entityTypes.find(e => e.value === filterEntityTypes[0])?.label || filterEntityTypes[0]}
- {:else}
- {filterEntityTypes.length} entities
- {/if}
-
-
-
- {#if filterEntityTypes.length > 0}
-
+
+
+
+
+
+ {#if filterEntityTypes.length === 0}
+ Entity
+ {:else if filterEntityTypes.length === 1}
+ {entityTypes.find(e => e.value === filterEntityTypes[0])?.label || filterEntityTypes[0]}
+ {:else}
+ {filterEntityTypes.length} entities
{/if}
- {#each entityTypes as type}
-
-
- {type.label}
-
- {/each}
-
-
+
+
+
+ {#if filterEntityTypes.length > 0}
+
+ {/if}
+ {#each entityTypes as type}
+
+
+ {type.label}
+
+ {/each}
+
+
-
-
-
-
-
- {#if filterActions.length === 0}
- All actions
- {:else if filterActions.length === 1}
- {actionTypes.find(a => a.value === filterActions[0])?.label || filterActions[0]}
- {:else}
- {filterActions.length} actions
- {/if}
-
-
-
- {#if filterActions.length > 0}
-
+
+
+
+
+
+ {#if filterActions.length === 0}
+ Action
+ {:else if filterActions.length === 1}
+ {actionTypes.find(a => a.value === filterActions[0])?.label || filterActions[0]}
+ {:else}
+ {filterActions.length} actions
{/if}
- {#each actionTypes as action}
-
-
- {action.label}
-
- {/each}
-
-
+
+
+
+ {#if filterActions.length > 0}
+
+ {/if}
+ {#each actionTypes as action}
+
+
+ {action.label}
+
+ {/each}
+
+
-
- {#if environments.length > 0}
- {@const selectedEnv = environments.find(e => e.id === filterEnvironmentId)}
- {@const SelectedEnvIcon = selectedEnv ? getIconComponent(selectedEnv.icon || 'globe') : Server}
-
filterEnvironmentId = v ? parseInt(v) : null}
- >
-
-
-
- {#if filterEnvironmentId === null}
- All environments
- {:else}
- {selectedEnv?.name || 'Environment'}
- {/if}
-
-
-
-
-
- All environments
-
- {#each environments as env}
- {@const EnvIcon = getIconComponent(env.icon || 'globe')}
-
-
- {env.name}
-
- {/each}
-
-
- {/if}
-
-
+
+ {#if environments.length > 0}
+ {@const selectedEnv = environments.find(e => e.id === filterEnvironmentId)}
+ {@const SelectedEnvIcon = selectedEnv ? getIconComponent(selectedEnv.icon || 'globe') : Server}
{
- selectedDatePreset = v || '';
- if (v !== 'custom') {
- applyDatePreset(v || '');
- }
- }}
+ value={filterEnvironmentId !== null ? String(filterEnvironmentId) : undefined}
+ onValueChange={(v) => filterEnvironmentId = v ? parseInt(v) : null}
>
-
-
+
+
- {#if selectedDatePreset === 'custom'}
- Custom
- {:else if selectedDatePreset}
- {datePresets.find(d => d.value === selectedDatePreset)?.label || 'All time'}
+ {#if filterEnvironmentId === null}
+ Environment
{:else}
- All time
+ {selectedEnv?.name || 'Environment'}
{/if}
- All time
- {#each datePresets as preset}
- {preset.label}
+
+
+ All environments
+
+ {#each environments as env}
+ {@const EnvIcon = getIconComponent(env.icon || 'globe')}
+
+
+ {env.name}
+
{/each}
- Custom range...
+ {/if}
-
- {#if selectedDatePreset === 'custom'}
-
-
- {/if}
-
-
- {#if filterUsernames.length > 0 || filterEntityTypes.length > 0 || filterActions.length > 0 || filterEnvironmentId !== null || selectedDatePreset}
-
- {/if}
-
-
-
-
-
-
-
-
-
-
Timestamp
-
Environment
-
User
-
Action
-
Entity
-
Name
-
IP address
-
-
-
-
-
-
+
{
+ selectedDatePreset = v || '';
+ if (v !== 'custom') {
+ applyDatePreset(v || '');
+ }
+ }}
>
- {#if loading || !initialized}
-
-
- Loading...
-
- {:else if logs.length === 0}
-
-
-
No audit log entries found
-
- {:else}
-
-
-
- {#each visibleLogs as log (log.id)}
-
showDetails(log)}
- role="button"
- tabindex="0"
- onkeydown={(e) => e.key === 'Enter' && showDetails(log)}
- >
-
- {formatTimestamp(log.createdAt)}
-
-
- {#if log.environmentName}
- {@const LogEnvIcon = getIconComponent(log.environmentIcon || 'globe')}
-
-
- {log.environmentName}
-
- {:else}
-
-
- {/if}
-
-
-
-
-
-
-
-
-
-
- {log.entityType}
-
-
-
-
- {log.entityName || log.entityId || '-'}
-
-
-
- {log.ipAddress || '-'}
-
-
-
-
-
- {/each}
-
-
+
+
+
+ {#if selectedDatePreset === 'custom'}
+ Custom
+ {:else if selectedDatePreset}
+ {datePresets.find(d => d.value === selectedDatePreset)?.label || 'All time'}
+ {:else}
+ All time
+ {/if}
+
+
+
+ All time
+ {#each datePresets as preset}
+ {preset.label}
+ {/each}
+ Custom range...
+
+
-
- {#if loadingMore}
-
-
- Loading more...
-
- {/if}
+
+ {#if selectedDatePreset === 'custom'}
+
+
+ {/if}
-
- {#if !hasMore && logs.length > 0}
-
- End of results ({total.toLocaleString()} entries)
-
- {/if}
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if showExportMenu}
+
+
+
+
+
{/if}
{/if}
+
+
+ {#if $licenseStore.loading}
+
+ {:else if !$licenseStore.isEnterprise}
+
+
+
+
+
Enterprise feature
+
+ Audit logging is an enterprise feature that tracks all user actions for compliance and security monitoring.
+
+
+
+ {:else}
+
showDetails(log)}
+ class="border-none"
+ wrapperClass="border rounded-lg"
+ >
+ {#snippet cell(column, log, rowState)}
+ {#if column.id === 'timestamp'}
+ {formatTimestamp(log.createdAt)}
+ {:else if column.id === 'environment'}
+ {#if log.environmentName}
+ {@const LogEnvIcon = getIconComponent(log.environmentIcon || 'globe')}
+
+
+ {log.environmentName}
+
+ {:else}
+ -
+ {/if}
+ {:else if column.id === 'user'}
+
+
+ {log.username}
+
+ {:else if column.id === 'action'}
+
+
+
+
+
+ {:else if column.id === 'entity'}
+
+
+ {log.entityType}
+
+ {:else if column.id === 'name'}
+
+ {log.entityName || log.entityId || '-'}
+
+ {:else if column.id === 'ip'}
+
+ {log.ipAddress || '-'}
+
+ {:else if column.id === 'actions'}
+
+
+
+ {/if}
+ {/snippet}
+
+ {#snippet emptyState()}
+
+
+
No audit log entries found
+
+ {/snippet}
+
+ {#snippet loadingState()}
+
+
+ Loading...
+
+ {/snippet}
+
+ {#snippet footer()}
+ {#if loadingMore}
+
+
+ Loading more...
+
+ {:else if !hasMore && logs.length > 0}
+
+ End of results ({total.toLocaleString()} entries)
+
+ {/if}
+ {/snippet}
+
+ {/if}
@@ -1125,7 +1017,12 @@
{/if}
- {#if selectedLog.details}
+ {#if selectedLog.details?.changes}
+
+
+
+
+ {:else if selectedLog.details}
{JSON.stringify(selectedLog.details, null, 2)}
diff --git a/src/routes/containers/+page.svelte b/src/routes/containers/+page.svelte
index 9c8e94b..d6042a4 100644
--- a/src/routes/containers/+page.svelte
+++ b/src/routes/containers/+page.svelte
@@ -98,7 +98,7 @@
return parseFloat((bytes / Math.pow(k, i)).toFixed(decimals)) + sizes[i];
}
- type SortField = 'name' | 'image' | 'state' | 'uptime' | 'stack' | 'ip' | 'cpu' | 'memory';
+ type SortField = 'name' | 'image' | 'state' | 'health' | 'uptime' | 'stack' | 'ip' | 'cpu' | 'memory';
type SortDirection = 'asc' | 'desc';
let containers = $state
([]);
@@ -701,6 +701,12 @@
cmp = (stateOrder[a.state.toLowerCase() as keyof typeof stateOrder] ?? 4) -
(stateOrder[b.state.toLowerCase() as keyof typeof stateOrder] ?? 4);
break;
+ case 'health':
+ const healthOrder: Record = { unhealthy: 0, starting: 1, healthy: 2 };
+ const healthA = a.health ? (healthOrder[a.health] ?? 1) : 3;
+ const healthB = b.health ? (healthOrder[b.health] ?? 1) : 3;
+ cmp = healthA - healthB;
+ break;
case 'uptime':
cmp = parseUptimeToSeconds(a.status) - parseUptimeToSeconds(b.status);
break;
diff --git a/src/routes/containers/ContainerInspectModal.svelte b/src/routes/containers/ContainerInspectModal.svelte
index abaf737..5fb9b0a 100644
--- a/src/routes/containers/ContainerInspectModal.svelte
+++ b/src/routes/containers/ContainerInspectModal.svelte
@@ -4,7 +4,7 @@
import * as Tabs from '$lib/components/ui/tabs';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
- import { Loader2, Box, Info, Layers, Cpu, MemoryStick, HardDrive, Network, Shield, Settings2, Code, Copy, Check, Activity, Wifi, Pencil, RefreshCw, X, FolderOpen, Moon, Tags, ExternalLink } from 'lucide-svelte';
+ import { Loader2, Box, Info, Layers, Cpu, MemoryStick, HardDrive, Network, Shield, Settings2, Code, Copy, Check, Activity, Wifi, Pencil, RefreshCw, X, FolderOpen, Moon, Tags, ExternalLink, Gpu } from 'lucide-svelte';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { currentEnvironment, appendEnvParam, environments } from '$lib/stores/environment';
@@ -1193,6 +1193,57 @@
{/if}
+
+ {#if containerData.HostConfig?.DeviceRequests?.length > 0 || (containerData.HostConfig?.Runtime && containerData.HostConfig.Runtime !== 'runc')}
+
+
+
+ GPU
+
+
+ {#if containerData.HostConfig?.Runtime}
+
+
Runtime
+
{containerData.HostConfig.Runtime}
+
+ {/if}
+ {#if containerData.HostConfig?.DeviceRequests?.length > 0}
+ {@const req = containerData.HostConfig.DeviceRequests[0]}
+
+
Count
+
{req.Count === -1 ? 'All' : req.Count}
+
+ {#if req.Driver}
+
+
Driver
+
{req.Driver}
+
+ {/if}
+ {#if req.DeviceIDs?.length > 0}
+
+
Device IDs
+
+ {#each req.DeviceIDs as id}
+ {id}
+ {/each}
+
+
+ {/if}
+ {#if req.Capabilities?.length > 0}
+
+
Capabilities
+
+ {#each req.Capabilities.flat() as cap}
+ {cap}
+ {/each}
+
+
+ {/if}
+ {/if}
+
+
+ {/if}
+
Cgroup settings
diff --git a/src/routes/containers/ContainerSettingsTab.svelte b/src/routes/containers/ContainerSettingsTab.svelte
index a0e3e0f..94c6702 100644
--- a/src/routes/containers/ContainerSettingsTab.svelte
+++ b/src/routes/containers/ContainerSettingsTab.svelte
@@ -5,7 +5,7 @@
import { Button } from '$lib/components/ui/button';
import { Checkbox } from '$lib/components/ui/checkbox';
import { TogglePill, ToggleGroup } from '$lib/components/ui/toggle-pill';
- import { Plus, Trash2, Settings2, RefreshCw, Network, X, Ban, RotateCw, AlertTriangle, PauseCircle, Share2, Server, CircleOff, ChevronDown, ChevronRight, Cpu, Shield, HeartPulse, Wifi, HardDrive, Lock, Loader2, CheckCircle2, Package } from 'lucide-svelte';
+ import { Plus, Trash2, Settings2, RefreshCw, Network, X, Ban, RotateCw, AlertTriangle, PauseCircle, Share2, Server, CircleOff, ChevronDown, ChevronRight, Cpu, Shield, HeartPulse, Wifi, HardDrive, Lock, Loader2, CheckCircle2, Package, Gpu } from 'lucide-svelte';
import { Badge } from '$lib/components/ui/badge';
import AutoUpdateSettings from './AutoUpdateSettings.svelte';
import type { VulnerabilityCriteria } from '$lib/components/VulnerabilityCriteriaSelector.svelte';
@@ -40,6 +40,8 @@
const commonUlimits = ['nofile', 'nproc', 'core', 'memlock', 'stack', 'cpu', 'fsize', 'locks'];
+ const commonGpuCapabilities = ['gpu', 'compute', 'utility', 'graphics', 'video', 'display'];
+
interface ConfigSet {
id: number;
name: string;
@@ -104,6 +106,14 @@
securityOptions: string[];
// Devices
deviceMappings: { hostPath: string; containerPath: string; permissions: string }[];
+ // GPU settings
+ gpuEnabled: boolean;
+ gpuMode: 'all' | 'count' | 'specific';
+ gpuCount: number;
+ gpuDeviceIds: string[];
+ gpuDriver: string;
+ gpuCapabilities: string[];
+ runtime: string;
// DNS settings
dnsServers: string[];
dnsSearch: string[];
@@ -166,6 +176,13 @@
capDrop = $bindable(),
securityOptions = $bindable(),
deviceMappings = $bindable(),
+ gpuEnabled = $bindable(),
+ gpuMode = $bindable(),
+ gpuCount = $bindable(),
+ gpuDeviceIds = $bindable(),
+ gpuDriver = $bindable(),
+ gpuCapabilities = $bindable(),
+ runtime = $bindable(),
dnsServers = $bindable(),
dnsSearch = $bindable(),
dnsOptions = $bindable(),
@@ -187,6 +204,7 @@
let showHealth = $state(false);
let showDns = $state(false);
let showDevices = $state(false);
+ let showGpu = $state(false);
let showUlimits = $state(false);
// DNS input fields
@@ -197,6 +215,10 @@
// Security options input
let securityOptionInput = $state('');
+ // GPU device ID input
+ let gpuDeviceIdInput = $state('');
+ let customRuntimeInput = $state('');
+
// Helper functions for form
function addPortMapping() {
portMappings = [...portMappings, { hostPort: '', containerPort: '', protocol: 'tcp' }];
@@ -256,6 +278,27 @@
ulimits = ulimits.filter((_, i) => i !== index);
}
+ function addGpuDeviceId() {
+ if (gpuDeviceIdInput.trim() && !gpuDeviceIds.includes(gpuDeviceIdInput.trim())) {
+ gpuDeviceIds = [...gpuDeviceIds, gpuDeviceIdInput.trim()];
+ gpuDeviceIdInput = '';
+ }
+ }
+
+ function removeGpuDeviceId(id: string) {
+ gpuDeviceIds = gpuDeviceIds.filter(d => d !== id);
+ }
+
+ function addGpuCapability(cap: string) {
+ if (cap && !gpuCapabilities.includes(cap)) {
+ gpuCapabilities = [...gpuCapabilities, cap];
+ }
+ }
+
+ function removeGpuCapability(cap: string) {
+ gpuCapabilities = gpuCapabilities.filter(c => c !== cap);
+ }
+
function addCapability(type: 'add' | 'drop', cap: string) {
if (!cap) return;
const capUpper = cap.toUpperCase();
@@ -1210,6 +1253,146 @@
{/if}
+
+
+
+ {#if showGpu}
+
+
+
+
+
+
+
+
+
+ {
+ if (v === '') runtime = '';
+ else if (v === 'nvidia') runtime = 'nvidia';
+ else if (v === 'custom') runtime = customRuntimeInput || '';
+ }}>
+
+ {runtime === '' ? 'Default (runc)' : runtime === 'nvidia' ? 'NVIDIA' : `Custom: ${runtime}`}
+
+
+
+
+
+
+
+ {#if runtime !== '' && runtime !== 'nvidia'}
+ { runtime = customRuntimeInput; }}
+ />
+ {/if}
+
+
+
+ {#if gpuEnabled}
+
+
+ { gpuMode = v as 'all' | 'count' | 'specific'; }}
+ />
+
+
+ {#if gpuMode === 'count'}
+
+
+
+
+ {/if}
+
+ {#if gpuMode === 'specific'}
+
+
+
+
{ if (e.key === 'Enter') { e.preventDefault(); addGpuDeviceId(); } }}
+ />
+
+
+ {#if gpuDeviceIds.length > 0}
+
+ {#each gpuDeviceIds as id}
+
+ {id}
+
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
+
+
+
+
+
+
+
+
{ addGpuCapability(v); }}>
+
+ Add capability...
+
+
+ {#each commonGpuCapabilities.filter(c => !gpuCapabilities.includes(c)) as cap}
+
+ {/each}
+
+
+ {#if gpuCapabilities.length > 0}
+
+ {#each gpuCapabilities as cap}
+
+ {cap}
+
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
+ {/if}
+
+