diff --git a/cmd/pulse-agent/main.go b/cmd/pulse-agent/main.go
index b9c90f6..7a20cb8 100644
--- a/cmd/pulse-agent/main.go
+++ b/cmd/pulse-agent/main.go
@@ -166,13 +166,25 @@ func main() {
dockerAgent, err = dockeragent.New(dockerCfg)
if err != nil {
- logger.Fatal().Err(err).Msg("Failed to initialize docker agent")
- }
+ // Docker isn't available yet - start retry loop in background
+ logger.Warn().Err(err).Msg("Docker not available, will retry with exponential backoff")
- g.Go(func() error {
- logger.Info().Msg("Docker agent module started")
- return dockerAgent.Run(ctx)
- })
+ g.Go(func() error {
+ agent := initDockerWithRetry(ctx, dockerCfg, &logger)
+ if agent != nil {
+ dockerAgent = agent
+ logger.Info().Msg("Docker agent module started (after retry)")
+ return agent.Run(ctx)
+ }
+ // Docker never became available, continue without it
+ return nil
+ })
+ } else {
+ g.Go(func() error {
+ logger.Info().Msg("Docker agent module started")
+ return dockerAgent.Run(ctx)
+ })
+ }
}
// Mark as ready after all agents started
@@ -416,3 +428,59 @@ func defaultLogLevel(envValue string) string {
}
return envValue
}
+
+// initDockerWithRetry attempts to initialize the Docker agent with exponential backoff.
+// It returns the agent when Docker becomes available, or nil if the context is cancelled.
+// Retry intervals: 5s, 10s, 20s, 40s, 80s, 160s, then cap at 5 minutes.
+func initDockerWithRetry(ctx context.Context, cfg dockeragent.Config, logger *zerolog.Logger) *dockeragent.Agent {
+ const (
+ initialDelay = 5 * time.Second
+ maxDelay = 5 * time.Minute
+ multiplier = 2.0
+ )
+
+ delay := initialDelay
+ attempt := 0
+
+ for {
+ select {
+ case <-ctx.Done():
+ logger.Info().Msg("Docker retry cancelled, context done")
+ return nil
+ default:
+ }
+
+ attempt++
+ logger.Info().
+ Int("attempt", attempt).
+ Str("next_retry", delay.String()).
+ Msg("Waiting to retry Docker connection")
+
+ select {
+ case <-ctx.Done():
+ logger.Info().Msg("Docker retry cancelled during wait")
+ return nil
+ case <-time.After(delay):
+ }
+
+ agent, err := dockeragent.New(cfg)
+ if err == nil {
+ logger.Info().
+ Int("attempts", attempt).
+ Msg("Successfully connected to Docker after retry")
+ return agent
+ }
+
+ logger.Warn().
+ Err(err).
+ Int("attempt", attempt).
+ Str("next_retry", (time.Duration(float64(delay) * multiplier)).String()).
+ Msg("Docker still not available, will retry")
+
+ // Calculate next delay with exponential backoff, capped at maxDelay
+ delay = time.Duration(float64(delay) * multiplier)
+ if delay > maxDelay {
+ delay = maxDelay
+ }
+ }
+}
diff --git a/frontend-modern/src/components/Backups/UnifiedBackups.tsx b/frontend-modern/src/components/Backups/UnifiedBackups.tsx
index e8f4bf2..64e18da 100644
--- a/frontend-modern/src/components/Backups/UnifiedBackups.tsx
+++ b/frontend-modern/src/components/Backups/UnifiedBackups.tsx
@@ -2364,7 +2364,7 @@ const UnifiedBackups: Component = () => {
{
const details = [];
diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx
index d4c60ed..6d9e07e 100644
--- a/frontend-modern/src/components/Dashboard/Dashboard.tsx
+++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx
@@ -278,9 +278,11 @@ export function Dashboard(props: DashboardProps) {
const [sortDirection, setSortDirection] = createSignal<'asc' | 'desc'>('asc');
// Column visibility management
+ // OS and IP columns are hidden by default since they require guest agent and may show dashes
const columnVisibility = useColumnVisibility(
STORAGE_KEYS.DASHBOARD_HIDDEN_COLUMNS,
- GUEST_COLUMNS as GuestColumnDef[]
+ GUEST_COLUMNS as GuestColumnDef[],
+ ['os', 'ip'] // Default hidden columns for cleaner first-run experience
);
const visibleColumns = columnVisibility.visibleColumns;
const visibleColumnIds = createMemo(() => visibleColumns().map(c => c.id));
diff --git a/frontend-modern/src/components/Dashboard/DiskList.tsx b/frontend-modern/src/components/Dashboard/DiskList.tsx
index 042d8d1..1ae5cc3 100644
--- a/frontend-modern/src/components/Dashboard/DiskList.tsx
+++ b/frontend-modern/src/components/Dashboard/DiskList.tsx
@@ -48,7 +48,7 @@ export function DiskList(props: DiskListProps) {
0}
fallback={
-
+
-
}
diff --git a/frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx b/frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx
index 3606625..4b9fe2c 100644
--- a/frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx
+++ b/frontend-modern/src/components/Dashboard/EnhancedCPUBar.tsx
@@ -53,7 +53,7 @@ export function EnhancedCPUBar(props: EnhancedCPUBarProps) {
// Progress bar mode - full width, flex centered like stacked bars
diff --git a/frontend-modern/src/components/Dashboard/GuestRow.tsx b/frontend-modern/src/components/Dashboard/GuestRow.tsx
index adf4bdd..73fc590 100644
--- a/frontend-modern/src/components/Dashboard/GuestRow.tsx
+++ b/frontend-modern/src/components/Dashboard/GuestRow.tsx
@@ -123,7 +123,7 @@ function NetworkInfoCell(props: { ipAddresses: string[]; networkInterfaces: Gues
return (
<>
@@ -186,12 +186,18 @@ function NetworkInfoCell(props: { ipAddresses: string[]; networkInterfaces: Gues
{/* Fallback: just show IP list if no interface details */}
0}>
-
-
- {(ip) => (
- {ip}
- )}
-
+
+
+ IP Addresses
+ No agent data
+
+
+
+ {(ip) => (
+ {ip}
+ )}
+
+
@@ -267,7 +273,7 @@ function OSInfoCell(props: { osName: string; osVersion: string; agentVersion: st
return (
<>
@@ -328,7 +334,7 @@ function BackupStatusCell(props: { lastBackup: string | number | null | undefine
return (
<>
@@ -991,7 +997,7 @@ export function GuestRow(props: GuestRowProps) {
when={hasDiskUsage()}
fallback={
-
+
-
diff --git a/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx b/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx
index 1593d5f..36e2d81 100644
--- a/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx
+++ b/frontend-modern/src/components/Dashboard/StackedMemoryBar.tsx
@@ -141,7 +141,7 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
fallback={
diff --git a/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx b/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx
index 87dad0f..fd8d862 100644
--- a/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx
+++ b/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx
@@ -128,7 +128,7 @@ interface DockerColumnDef extends ColumnConfig {
export const DOCKER_COLUMNS: DockerColumnDef[] = [
{ id: 'resource', label: 'Resource', priority: 'essential', minWidth: 'auto', flex: 1, sortKey: 'resource' },
{ id: 'type', label: 'Type', priority: 'essential', minWidth: 'auto', maxWidth: 'auto', sortKey: 'type' },
- { id: 'image', label: 'Image / Stack', priority: 'essential', minWidth: 'auto', maxWidth: 'auto', sortKey: 'image' },
+ { id: 'image', label: 'Image / Stack', priority: 'essential', minWidth: '80px', maxWidth: '200px', sortKey: 'image' },
{ id: 'status', label: 'Status', priority: 'essential', minWidth: 'auto', maxWidth: 'auto', sortKey: 'status' },
// Metric columns - need fixed width to match progress bar max-width (140px + padding)
// Note: Disk column removed - Docker API rarely provides this data
@@ -1289,7 +1289,10 @@ const DockerContainerRow: Component<{
);
case 'image':
return (
-
+
{container.image || '—'}
);
@@ -1403,7 +1406,7 @@ const DockerContainerRow: Component<{
"min-width": column.id === 'cpu' || column.id === 'memory' ? '140px' : undefined,
// Force resource column to respect truncation for very long names (issue #789)
"max-width": column.id === 'resource' ? '0' : undefined,
- "width": column.id === 'resource' ? '40%' : undefined,
+ "width": column.id === 'resource' ? '25%' : undefined,
}}
>
{renderCell(column)}
@@ -2173,7 +2176,10 @@ const DockerServiceRow: Component<{
);
case 'image':
return (
-
+
{service.image || '—'}
);
@@ -2230,7 +2236,7 @@ const DockerServiceRow: Component<{
"min-width": column.id === 'cpu' || column.id === 'memory' ? '140px' : undefined,
// Force resource column to respect truncation for very long names (issue #789)
"max-width": column.id === 'resource' ? '0' : undefined,
- "width": column.id === 'resource' ? '40%' : undefined,
+ "width": column.id === 'resource' ? '25%' : undefined,
}}
>
{renderCell(column)}
diff --git a/frontend-modern/src/components/Docker/StackedContainerBar.tsx b/frontend-modern/src/components/Docker/StackedContainerBar.tsx
index 690abf5..fbedf3a 100644
--- a/frontend-modern/src/components/Docker/StackedContainerBar.tsx
+++ b/frontend-modern/src/components/Docker/StackedContainerBar.tsx
@@ -48,7 +48,7 @@ export function StackedContainerBar(props: StackedContainerBarProps) {
return (
diff --git a/frontend-modern/src/components/Hosts/HostsOverview.tsx b/frontend-modern/src/components/Hosts/HostsOverview.tsx
index eb08cf4..844d4bf 100644
--- a/frontend-modern/src/components/Hosts/HostsOverview.tsx
+++ b/frontend-modern/src/components/Hosts/HostsOverview.tsx
@@ -89,7 +89,7 @@ function HostNetworkInfoCell(props: { networkInterfaces: NetworkInterface[] }) {
return (
<>
@@ -220,7 +220,7 @@ function HostTemperatureCell(props: { sensors: Record | null | u
return (
<>
@@ -342,7 +342,7 @@ function HostRAIDStatusCell(props: HostRAIDStatusCellProps) {
return (
<>
diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx
index 316c98d..d8fddc1 100644
--- a/frontend-modern/src/components/Settings/Settings.tsx
+++ b/frontend-modern/src/components/Settings/Settings.tsx
@@ -4033,7 +4033,7 @@ const Settings: Component = (props) => {
Discovery subnet
|