diff --git a/frontend-modern/src/api/nodes.ts b/frontend-modern/src/api/nodes.ts
index 499bf74..c5bd37a 100644
--- a/frontend-modern/src/api/nodes.ts
+++ b/frontend-modern/src/api/nodes.ts
@@ -40,6 +40,7 @@ export class NodesAPI {
nodeCount?: number;
clusterNodeCount?: number;
datastoreCount?: number;
+ warnings?: string[];
}> {
return apiFetchJSON(`${this.baseUrl}/test-connection`, {
method: 'POST',
@@ -51,6 +52,7 @@ export class NodesAPI {
status: string;
message?: string;
latency?: number;
+ warnings?: string[];
}> {
return apiFetchJSON(`${this.baseUrl}/${nodeId}/test`, {
method: 'POST',
diff --git a/frontend-modern/src/components/Dashboard/GuestRow.tsx b/frontend-modern/src/components/Dashboard/GuestRow.tsx
index 5b06862..408aa2b 100644
--- a/frontend-modern/src/components/Dashboard/GuestRow.tsx
+++ b/frontend-modern/src/components/Dashboard/GuestRow.tsx
@@ -1,4 +1,4 @@
-import { createMemo, createSignal, createEffect, on, Show, For } from 'solid-js';
+import { createMemo, createSignal, createEffect, Show, For } from 'solid-js';
import type { VM, Container } from '@/types/api';
import { formatBytes, formatPercent, formatUptime } from '@/utils/format';
import { MetricBar } from './MetricBar';
@@ -60,7 +60,6 @@ interface GuestRowProps {
}
export function GuestRow(props: GuestRowProps) {
- const initialGuestId = buildGuestId(props.guest);
const guestId = createMemo(() => buildGuestId(props.guest));
const isEditingUrl = createMemo(() => currentlyEditingGuestId() === guestId());
diff --git a/frontend-modern/src/components/ErrorBoundary.tsx b/frontend-modern/src/components/ErrorBoundary.tsx
index efe7880..58ae219 100644
--- a/frontend-modern/src/components/ErrorBoundary.tsx
+++ b/frontend-modern/src/components/ErrorBoundary.tsx
@@ -1,4 +1,4 @@
-import { Component, JSX, createSignal, ErrorBoundary as SolidErrorBoundary } from 'solid-js';
+import { Component, JSX, ErrorBoundary as SolidErrorBoundary } from 'solid-js';
import { logError } from '@/utils/logger';
import { SectionHeader } from '@/components/shared/SectionHeader';
@@ -9,8 +9,6 @@ interface ErrorBoundaryProps {
}
const DefaultErrorFallback: Component<{ error: Error; reset: () => void }> = (props) => {
- const [details, setDetails] = createSignal(false);
-
return (
diff --git a/frontend-modern/src/components/FirstRunSetup.tsx b/frontend-modern/src/components/FirstRunSetup.tsx
index 34c771e..665b374 100644
--- a/frontend-modern/src/components/FirstRunSetup.tsx
+++ b/frontend-modern/src/components/FirstRunSetup.tsx
@@ -23,7 +23,6 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool
const [copied, setCopied] = createSignal<'password' | 'token' | null>(null);
const [themeMode, setThemeMode] = createSignal<'system' | 'light' | 'dark'>('system');
const [bootstrapToken, setBootstrapToken] = createSignal('');
- const [isUnlocking, setIsUnlocking] = createSignal(false);
const [isUnlocked, setIsUnlocked] = createSignal(false);
const [bootstrapTokenPath, setBootstrapTokenPath] = createSignal('');
const [isDocker, setIsDocker] = createSignal(false);
@@ -371,10 +370,10 @@ IMPORTANT: Keep these credentials secure!
- {isUnlocking() ? 'Unlocking...' : 'Unlock Wizard'}
+ Unlock Wizard
diff --git a/frontend-modern/src/components/Settings/NodeModal.tsx b/frontend-modern/src/components/Settings/NodeModal.tsx
index a623f90..e54cc77 100644
--- a/frontend-modern/src/components/Settings/NodeModal.tsx
+++ b/frontend-modern/src/components/Settings/NodeModal.tsx
@@ -85,6 +85,9 @@ export const NodeModal: Component = (props) => {
const [quickSetupCommand, setQuickSetupCommand] = createSignal('');
const [quickSetupToken, setQuickSetupToken] = createSignal('');
const [quickSetupExpiry, setQuickSetupExpiry] = createSignal(null);
+ const showTemperatureMonitoringSection = () =>
+ typeof props.temperatureMonitoringEnabled === 'boolean';
+ const temperatureMonitoringEnabledValue = () => props.temperatureMonitoringEnabled ?? true;
const quickSetupExpiryLabel = () => {
const expiry = quickSetupExpiry();
if (!expiry) {
@@ -1764,40 +1767,39 @@ export const NodeModal: Component = (props) => {
-
- {() => {
- const enabled = props.temperatureMonitoringEnabled ?? true;
- return (
-
-
-
-
Temperature monitoring
-
- Uses the Pulse sensors key or proxy to read CPU/NVMe temperatures for this node. Disable if you don't need temperature data or haven't deployed the proxy yet.
-
-
-
{
- props.onToggleTemperatureMonitoring?.(event.currentTarget.checked);
- }}
- disabled={props.temperatureMonitoringLocked || props.savingTemperatureSetting}
- ariaLabel={enabled ? 'Disable temperature monitoring' : 'Enable temperature monitoring'}
- />
-
-
-
- Pulse will skip SSH temperature polling for this node. Existing dashboard readings will stop refreshing.
-
-
-
-
- Locked by environment variables. Remove the override (ENABLE_TEMPERATURE_MONITORING) and restart Pulse to manage it in the UI.
-
-
+
+
+
+
+
Temperature monitoring
+
+ Uses the Pulse sensors key or proxy to read CPU/NVMe temperatures for this node. Disable if you don't need temperature data or haven't deployed the proxy yet.
+
- );
- }}
+
{
+ props.onToggleTemperatureMonitoring?.(event.currentTarget.checked);
+ }}
+ disabled={props.temperatureMonitoringLocked || props.savingTemperatureSetting}
+ ariaLabel={
+ temperatureMonitoringEnabledValue()
+ ? 'Disable temperature monitoring'
+ : 'Enable temperature monitoring'
+ }
+ />
+
+
+
+ Pulse will skip SSH temperature polling for this node. Existing dashboard readings will stop refreshing.
+
+
+
+
+ Locked by environment variables. Remove the override (ENABLE_TEMPERATURE_MONITORING) and restart Pulse to manage it in the UI.
+
+
+
diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx
index e39570d..9c9f77b 100644
--- a/frontend-modern/src/components/Settings/Settings.tsx
+++ b/frontend-modern/src/components/Settings/Settings.tsx
@@ -9,7 +9,6 @@ import {
onCleanup,
on,
} from 'solid-js';
-import type { JSX } from 'solid-js';
import { useNavigate, useLocation } from '@solidjs/router';
import { useWebSocket } from '@/App';
import { showSuccess, showError, showWarning } from '@/utils/toast';
@@ -535,6 +534,14 @@ const Settings: Component = (props) => {
const [showDeleteNodeModal, setShowDeleteNodeModal] = createSignal(false);
const [nodePendingDelete, setNodePendingDelete] = createSignal(null);
const [deleteNodeLoading, setDeleteNodeLoading] = createSignal(false);
+ const isNodeModalVisible = (type: 'pve' | 'pbs' | 'pmg') =>
+ Boolean(showNodeModal() && currentNodeType() === type);
+ const resolveTemperatureMonitoringEnabled = (node?: NodeConfigWithStatus | null) => {
+ if (node && typeof node.temperatureMonitoringEnabled === 'boolean') {
+ return node.temperatureMonitoringEnabled;
+ }
+ return temperatureMonitoringEnabled();
+ };
const [initialLoadComplete, setInitialLoadComplete] = createSignal(false);
const [discoveryScanStatus, setDiscoveryScanStatus] = createSignal({
scanning: false,
@@ -6714,7 +6721,7 @@ const Settings: Component = (props) => {
{/* Node Modal - Use separate modals for PVE and PBS to ensure clean state */}
-
+
= (props) => {
nodeType="pve"
editingNode={editingNode()?.type === 'pve' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined}
- temperatureMonitoringEnabled={
- editingNode()?.temperatureMonitoringEnabled !== undefined
- ? editingNode()!.temperatureMonitoringEnabled
- : temperatureMonitoringEnabled()
- }
+ temperatureMonitoringEnabled={resolveTemperatureMonitoringEnabled(
+ editingNode()?.type === 'pve' ? editingNode() : null,
+ )}
temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={
@@ -6788,7 +6793,7 @@ const Settings: Component = (props) => {
{/* PBS Node Modal - Separate instance to prevent contamination */}
-
+
= (props) => {
nodeType="pbs"
editingNode={editingNode()?.type === 'pbs' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined}
- temperatureMonitoringEnabled={
- editingNode()?.temperatureMonitoringEnabled !== undefined
- ? editingNode()!.temperatureMonitoringEnabled
- : temperatureMonitoringEnabled()
- }
+ temperatureMonitoringEnabled={resolveTemperatureMonitoringEnabled(
+ editingNode()?.type === 'pbs' ? editingNode() : null,
+ )}
temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={
@@ -6861,7 +6864,7 @@ const Settings: Component = (props) => {
{/* PMG Node Modal */}
-
+
= (props) => {
nodeType="pmg"
editingNode={editingNode()?.type === 'pmg' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined}
- temperatureMonitoringEnabled={
- editingNode()?.temperatureMonitoringEnabled !== undefined
- ? editingNode()!.temperatureMonitoringEnabled
- : temperatureMonitoringEnabled()
- }
+ temperatureMonitoringEnabled={resolveTemperatureMonitoringEnabled(
+ editingNode()?.type === 'pmg' ? editingNode() : null,
+ )}
temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={
diff --git a/frontend-modern/src/components/UpdateProgressModal.tsx b/frontend-modern/src/components/UpdateProgressModal.tsx
index 26a5024..f6cceef 100644
--- a/frontend-modern/src/components/UpdateProgressModal.tsx
+++ b/frontend-modern/src/components/UpdateProgressModal.tsx
@@ -1,5 +1,6 @@
-import { createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js';
+import { createSignal, Show, onCleanup, createEffect } from 'solid-js';
import { UpdatesAPI, type UpdateStatus } from '@/api/updates';
+import { apiFetch } from '@/utils/apiClient';
import { logger } from '@/utils/logger';
interface UpdateProgressModalProps {
@@ -16,7 +17,6 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
const [hasError, setHasError] = createSignal(false);
const [isRestarting, setIsRestarting] = createSignal(false);
const [wsDisconnected, setWsDisconnected] = createSignal(false);
- const [usingSSE, setUsingSSE] = createSignal(false);
let pollInterval: number | undefined;
let healthCheckTimer: number | undefined;
let healthCheckAttempts = 0;
@@ -30,12 +30,12 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
};
const closeSSE = () => {
- if (eventSource) {
- eventSource.close();
- eventSource = undefined;
- setUsingSSE(false);
- logger.info('SSE connection closed');
+ if (!eventSource) {
+ return;
}
+ eventSource.close();
+ eventSource = undefined;
+ logger.info('SSE connection closed');
};
const setupSSE = () => {
@@ -45,7 +45,6 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
try {
// Create EventSource connection to SSE endpoint
eventSource = new EventSource('/api/updates/stream');
- setUsingSSE(true);
eventSource.onopen = () => {
logger.info('SSE connection established');
@@ -73,7 +72,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
if (updateStatus.status === 'completed' && !updateStatus.error) {
closeSSE();
// Verify backend health and reload
- fetch('/api/health', { cache: 'no-store' })
+ apiFetch('/api/health', { cache: 'no-store' })
.then((healthCheck) => {
if (healthCheck.ok) {
logger.info('Update completed, backend healthy, reloading...');
@@ -157,7 +156,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
}
// Verify backend is healthy and reload
try {
- const healthCheck = await fetch('/api/health', { cache: 'no-store' });
+ const healthCheck = await apiFetch('/api/health', { cache: 'no-store' });
if (healthCheck.ok) {
logger.info('Update completed, backend healthy, reloading...');
window.location.reload();
@@ -214,7 +213,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
let isHealthy = false;
try {
- const response = await fetch('/api/health', { cache: 'no-store' });
+ const response = await apiFetch('/api/health', { cache: 'no-store' });
if (response.ok) {
isHealthy = true;
}
@@ -258,7 +257,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
// Give it a moment for the backend to fully initialize
setTimeout(async () => {
try {
- const response = await fetch('/api/health', { cache: 'no-store' });
+ const response = await apiFetch('/api/health', { cache: 'no-store' });
if (response.ok) {
logger.info('Backend healthy after websocket reconnect, reloading...');
window.location.reload();
diff --git a/frontend-modern/src/components/shared/NodeSummaryTable.tsx b/frontend-modern/src/components/shared/NodeSummaryTable.tsx
index ca0d2a1..1818711 100644
--- a/frontend-modern/src/components/shared/NodeSummaryTable.tsx
+++ b/frontend-modern/src/components/shared/NodeSummaryTable.tsx
@@ -651,20 +651,20 @@ export const NodeSummaryTable: Component = (props) => {
: 'text-green-600 dark:text-green-400';
const temp = node!.temperature;
- const cpuMin = temp?.cpuMin;
- const cpuMax = temp?.cpuMaxRecord;
- const hasMinMax =
- typeof cpuMin === 'number' &&
- cpuMin > 0 &&
- typeof cpuMax === 'number' &&
- cpuMax > 0;
+ const cpuMinValue =
+ typeof temp?.cpuMin === 'number' && temp.cpuMin > 0 ? temp.cpuMin : null;
+ const cpuMaxValue =
+ typeof temp?.cpuMaxRecord === 'number' && temp.cpuMaxRecord > 0
+ ? temp.cpuMaxRecord
+ : null;
+ const hasMinMax = cpuMinValue !== null && cpuMaxValue !== null;
const gpus = temp?.gpu ?? [];
const hasGPU = gpus.length > 0;
if (hasMinMax || hasGPU) {
- const min = Math.round(cpuMin);
- const max = Math.round(cpuMax);
+ const min = Math.round(cpuMinValue!);
+ const max = Math.round(cpuMaxValue!);
const getTooltipColor = (temp: number) => {
if (temp >= 80) return 'text-red-400';
diff --git a/frontend-modern/src/components/shared/Sparkline.tsx b/frontend-modern/src/components/shared/Sparkline.tsx
index 239bfa6..268051d 100644
--- a/frontend-modern/src/components/shared/Sparkline.tsx
+++ b/frontend-modern/src/components/shared/Sparkline.tsx
@@ -5,7 +5,7 @@
* Optimized for rendering many sparklines simultaneously in tables.
*/
-import { onMount, onCleanup, createEffect, createSignal, Component, Show } from 'solid-js';
+import { onCleanup, createEffect, createSignal, Component, Show } from 'solid-js';
import { Portal } from 'solid-js/web';
import type { MetricSnapshot } from '@/stores/metricsHistory';
import { scheduleSparkline } from '@/utils/canvasRenderQueue';
@@ -202,12 +202,6 @@ export const Sparkline: Component = (props) => {
// Redraw when data or dimensions change
createEffect(() => {
- // Capture current values to avoid stale closures
- const currentData = props.data;
- const currentWidth = width();
- const currentHeight = height();
- const currentMetric = props.metric;
-
// Unregister previous draw callback if it exists
if (unregister) {
unregister();
@@ -230,7 +224,6 @@ export const Sparkline: Component = (props) => {
const rect = canvasRef.getBoundingClientRect();
const mouseX = e.clientX - rect.left;
const w = width();
- const h = height();
const data = props.data;
const values = data.map(d => d[props.metric]);
@@ -244,10 +237,6 @@ export const Sparkline: Component = (props) => {
const timestamp = data[nearestIndex].timestamp;
// Calculate absolute viewport position for portal
- const minValue = 0;
- const maxValue = Math.max(100, ...values);
- const y = h - ((value - minValue) / (maxValue - minValue)) * h;
-
setHoveredPoint({
value,
timestamp,
diff --git a/frontend-modern/src/stores/metricsSampler.ts b/frontend-modern/src/stores/metricsSampler.ts
index 7f6d853..d94933d 100644
--- a/frontend-modern/src/stores/metricsSampler.ts
+++ b/frontend-modern/src/stores/metricsSampler.ts
@@ -14,7 +14,6 @@ import { recordMetric } from './metricsHistory';
import { getMetricsViewMode } from './metricsViewMode';
import { buildMetricKey } from '@/utils/metricsKeys';
import { logger } from '@/utils/logger';
-import type { Node, VM, Container, DockerHost } from '@/types/api';
const SAMPLE_INTERVAL_MS = 30 * 1000; // 30 seconds
@@ -110,9 +109,9 @@ function sampleMetrics(): void {
const contCpu = container.cpuPercent || 0;
const contMem = container.memoryPercent || 0;
- const contDisk = container.disk && container.disk.total > 0
- ? (container.disk.used / container.disk.total) * 100
- : 0;
+ const usableTotal = container.rootFilesystemBytes ?? 0;
+ const writable = container.writableLayerBytes ?? 0;
+ const contDisk = usableTotal > 0 ? (writable / usableTotal) * 100 : 0;
const containerKey = buildMetricKey('dockerContainer', container.id);
recordMetric(containerKey, contCpu, contMem, contDisk);
diff --git a/frontend-modern/src/utils/apiClient.ts b/frontend-modern/src/utils/apiClient.ts
index 177d841..eb0514f 100644
--- a/frontend-modern/src/utils/apiClient.ts
+++ b/frontend-modern/src/utils/apiClient.ts
@@ -4,6 +4,8 @@
import { logger } from '@/utils/logger';
import { STORAGE_KEYS } from '@/utils/localStorage';
+const AUTH_STORAGE_KEY = STORAGE_KEYS.AUTH;
+
const getSessionStorage = (): Storage | undefined => {
if (typeof window === 'undefined') {
return undefined;