Improve auth timeout handling (related to #705)

This commit is contained in:
rcourtman 2025-11-12 21:50:53 +00:00
parent 4f950de29d
commit 3fa7356ff3
11 changed files with 87 additions and 97 deletions

View file

@ -40,6 +40,7 @@ export class NodesAPI {
nodeCount?: number; nodeCount?: number;
clusterNodeCount?: number; clusterNodeCount?: number;
datastoreCount?: number; datastoreCount?: number;
warnings?: string[];
}> { }> {
return apiFetchJSON(`${this.baseUrl}/test-connection`, { return apiFetchJSON(`${this.baseUrl}/test-connection`, {
method: 'POST', method: 'POST',
@ -51,6 +52,7 @@ export class NodesAPI {
status: string; status: string;
message?: string; message?: string;
latency?: number; latency?: number;
warnings?: string[];
}> { }> {
return apiFetchJSON(`${this.baseUrl}/${nodeId}/test`, { return apiFetchJSON(`${this.baseUrl}/${nodeId}/test`, {
method: 'POST', method: 'POST',

View file

@ -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 type { VM, Container } from '@/types/api';
import { formatBytes, formatPercent, formatUptime } from '@/utils/format'; import { formatBytes, formatPercent, formatUptime } from '@/utils/format';
import { MetricBar } from './MetricBar'; import { MetricBar } from './MetricBar';
@ -60,7 +60,6 @@ interface GuestRowProps {
} }
export function GuestRow(props: GuestRowProps) { export function GuestRow(props: GuestRowProps) {
const initialGuestId = buildGuestId(props.guest);
const guestId = createMemo(() => buildGuestId(props.guest)); const guestId = createMemo(() => buildGuestId(props.guest));
const isEditingUrl = createMemo(() => currentlyEditingGuestId() === guestId()); const isEditingUrl = createMemo(() => currentlyEditingGuestId() === guestId());

View file

@ -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 { logError } from '@/utils/logger';
import { SectionHeader } from '@/components/shared/SectionHeader'; import { SectionHeader } from '@/components/shared/SectionHeader';
@ -9,8 +9,6 @@ interface ErrorBoundaryProps {
} }
const DefaultErrorFallback: Component<{ error: Error; reset: () => void }> = (props) => { const DefaultErrorFallback: Component<{ error: Error; reset: () => void }> = (props) => {
const [details, setDetails] = createSignal(false);
return ( return (
<div class="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 p-4"> <div class="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 p-4">
<div class="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6"> <div class="max-w-md w-full bg-white dark:bg-gray-800 rounded-lg shadow-lg p-6">

View file

@ -23,7 +23,6 @@ export const FirstRunSetup: Component<{ force?: boolean; showLegacyBanner?: bool
const [copied, setCopied] = createSignal<'password' | 'token' | null>(null); const [copied, setCopied] = createSignal<'password' | 'token' | null>(null);
const [themeMode, setThemeMode] = createSignal<'system' | 'light' | 'dark'>('system'); const [themeMode, setThemeMode] = createSignal<'system' | 'light' | 'dark'>('system');
const [bootstrapToken, setBootstrapToken] = createSignal(''); const [bootstrapToken, setBootstrapToken] = createSignal('');
const [isUnlocking, setIsUnlocking] = createSignal(false);
const [isUnlocked, setIsUnlocked] = createSignal(false); const [isUnlocked, setIsUnlocked] = createSignal(false);
const [bootstrapTokenPath, setBootstrapTokenPath] = createSignal<string>(''); const [bootstrapTokenPath, setBootstrapTokenPath] = createSignal<string>('');
const [isDocker, setIsDocker] = createSignal<boolean>(false); const [isDocker, setIsDocker] = createSignal<boolean>(false);
@ -371,10 +370,10 @@ IMPORTANT: Keep these credentials secure!
<button <button
type="button" type="button"
onClick={handleUnlock} onClick={handleUnlock}
disabled={isUnlocking() || !bootstrapToken().trim()} disabled={!bootstrapToken().trim()}
class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors disabled:cursor-not-allowed" class="w-full py-3 px-4 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white rounded-lg font-medium transition-colors disabled:cursor-not-allowed"
> >
{isUnlocking() ? 'Unlocking...' : 'Unlock Wizard'} Unlock Wizard
</button> </button>
</div> </div>
</div> </div>

View file

@ -85,6 +85,9 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
const [quickSetupCommand, setQuickSetupCommand] = createSignal(''); const [quickSetupCommand, setQuickSetupCommand] = createSignal('');
const [quickSetupToken, setQuickSetupToken] = createSignal(''); const [quickSetupToken, setQuickSetupToken] = createSignal('');
const [quickSetupExpiry, setQuickSetupExpiry] = createSignal<number | null>(null); const [quickSetupExpiry, setQuickSetupExpiry] = createSignal<number | null>(null);
const showTemperatureMonitoringSection = () =>
typeof props.temperatureMonitoringEnabled === 'boolean';
const temperatureMonitoringEnabledValue = () => props.temperatureMonitoringEnabled ?? true;
const quickSetupExpiryLabel = () => { const quickSetupExpiryLabel = () => {
const expiry = quickSetupExpiry(); const expiry = quickSetupExpiry();
if (!expiry) { if (!expiry) {
@ -1764,40 +1767,39 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
</div> </div>
</div> </div>
<Show when={typeof props.temperatureMonitoringEnabled === 'boolean'}> <Show when={showTemperatureMonitoringSection()}>
{() => { <div class="rounded-lg border border-gray-200 bg-white p-3 text-sm text-gray-700 shadow-sm dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200">
const enabled = props.temperatureMonitoringEnabled ?? true; <div class="flex items-start justify-between gap-3">
return ( <div>
<div class="rounded-lg border border-gray-200 bg-white p-3 text-sm text-gray-700 shadow-sm dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200"> <p class="font-medium text-gray-900 dark:text-gray-100">Temperature monitoring</p>
<div class="flex items-start justify-between gap-3"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
<div> 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.
<p class="font-medium text-gray-900 dark:text-gray-100">Temperature monitoring</p> </p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
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.
</p>
</div>
<TogglePrimitive
checked={enabled}
onChange={(event) => {
props.onToggleTemperatureMonitoring?.(event.currentTarget.checked);
}}
disabled={props.temperatureMonitoringLocked || props.savingTemperatureSetting}
ariaLabel={enabled ? 'Disable temperature monitoring' : 'Enable temperature monitoring'}
/>
</div>
<Show when={!enabled}>
<p class="mt-3 rounded border border-blue-200 bg-blue-50 p-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-200">
Pulse will skip SSH temperature polling for this node. Existing dashboard readings will stop refreshing.
</p>
</Show>
<Show when={props.temperatureMonitoringLocked}>
<p class="mt-3 rounded border border-amber-200 bg-amber-50 p-2 text-xs text-amber-800 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-200">
Locked by environment variables. Remove the override (ENABLE_TEMPERATURE_MONITORING) and restart Pulse to manage it in the UI.
</p>
</Show>
</div> </div>
); <TogglePrimitive
}} checked={temperatureMonitoringEnabledValue()}
onChange={(event) => {
props.onToggleTemperatureMonitoring?.(event.currentTarget.checked);
}}
disabled={props.temperatureMonitoringLocked || props.savingTemperatureSetting}
ariaLabel={
temperatureMonitoringEnabledValue()
? 'Disable temperature monitoring'
: 'Enable temperature monitoring'
}
/>
</div>
<Show when={!temperatureMonitoringEnabledValue()}>
<p class="mt-3 rounded border border-blue-200 bg-blue-50 p-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-200">
Pulse will skip SSH temperature polling for this node. Existing dashboard readings will stop refreshing.
</p>
</Show>
<Show when={props.temperatureMonitoringLocked}>
<p class="mt-3 rounded border border-amber-200 bg-amber-50 p-2 text-xs text-amber-800 dark:border-amber-700 dark:bg-amber-900/20 dark:text-amber-200">
Locked by environment variables. Remove the override (ENABLE_TEMPERATURE_MONITORING) and restart Pulse to manage it in the UI.
</p>
</Show>
</div>
</Show> </Show>
</div> </div>
</Show> </Show>

View file

@ -9,7 +9,6 @@ import {
onCleanup, onCleanup,
on, on,
} from 'solid-js'; } from 'solid-js';
import type { JSX } from 'solid-js';
import { useNavigate, useLocation } from '@solidjs/router'; import { useNavigate, useLocation } from '@solidjs/router';
import { useWebSocket } from '@/App'; import { useWebSocket } from '@/App';
import { showSuccess, showError, showWarning } from '@/utils/toast'; import { showSuccess, showError, showWarning } from '@/utils/toast';
@ -535,6 +534,14 @@ const Settings: Component<SettingsProps> = (props) => {
const [showDeleteNodeModal, setShowDeleteNodeModal] = createSignal(false); const [showDeleteNodeModal, setShowDeleteNodeModal] = createSignal(false);
const [nodePendingDelete, setNodePendingDelete] = createSignal<NodeConfigWithStatus | null>(null); const [nodePendingDelete, setNodePendingDelete] = createSignal<NodeConfigWithStatus | null>(null);
const [deleteNodeLoading, setDeleteNodeLoading] = createSignal(false); 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 [initialLoadComplete, setInitialLoadComplete] = createSignal(false);
const [discoveryScanStatus, setDiscoveryScanStatus] = createSignal<DiscoveryScanStatus>({ const [discoveryScanStatus, setDiscoveryScanStatus] = createSignal<DiscoveryScanStatus>({
scanning: false, scanning: false,
@ -6714,7 +6721,7 @@ const Settings: Component<SettingsProps> = (props) => {
</Show> </Show>
{/* Node Modal - Use separate modals for PVE and PBS to ensure clean state */} {/* Node Modal - Use separate modals for PVE and PBS to ensure clean state */}
<Show when={showNodeModal() && currentNodeType() === 'pve'}> <Show when={isNodeModalVisible('pve')}>
<NodeModal <NodeModal
isOpen={true} isOpen={true}
resetKey={modalResetKey()} resetKey={modalResetKey()}
@ -6727,11 +6734,9 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pve" nodeType="pve"
editingNode={editingNode()?.type === 'pve' ? (editingNode() ?? undefined) : undefined} editingNode={editingNode()?.type === 'pve' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined} securityStatus={securityStatus() ?? undefined}
temperatureMonitoringEnabled={ temperatureMonitoringEnabled={resolveTemperatureMonitoringEnabled(
editingNode()?.temperatureMonitoringEnabled !== undefined editingNode()?.type === 'pve' ? editingNode() : null,
? editingNode()!.temperatureMonitoringEnabled )}
: temperatureMonitoringEnabled()
}
temperatureMonitoringLocked={temperatureMonitoringLocked()} temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()} savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={ onToggleTemperatureMonitoring={
@ -6788,7 +6793,7 @@ const Settings: Component<SettingsProps> = (props) => {
</Show> </Show>
{/* PBS Node Modal - Separate instance to prevent contamination */} {/* PBS Node Modal - Separate instance to prevent contamination */}
<Show when={showNodeModal() && currentNodeType() === 'pbs'}> <Show when={isNodeModalVisible('pbs')}>
<NodeModal <NodeModal
isOpen={true} isOpen={true}
resetKey={modalResetKey()} resetKey={modalResetKey()}
@ -6801,11 +6806,9 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pbs" nodeType="pbs"
editingNode={editingNode()?.type === 'pbs' ? (editingNode() ?? undefined) : undefined} editingNode={editingNode()?.type === 'pbs' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined} securityStatus={securityStatus() ?? undefined}
temperatureMonitoringEnabled={ temperatureMonitoringEnabled={resolveTemperatureMonitoringEnabled(
editingNode()?.temperatureMonitoringEnabled !== undefined editingNode()?.type === 'pbs' ? editingNode() : null,
? editingNode()!.temperatureMonitoringEnabled )}
: temperatureMonitoringEnabled()
}
temperatureMonitoringLocked={temperatureMonitoringLocked()} temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()} savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={ onToggleTemperatureMonitoring={
@ -6861,7 +6864,7 @@ const Settings: Component<SettingsProps> = (props) => {
</Show> </Show>
{/* PMG Node Modal */} {/* PMG Node Modal */}
<Show when={showNodeModal() && currentNodeType() === 'pmg'}> <Show when={isNodeModalVisible('pmg')}>
<NodeModal <NodeModal
isOpen={true} isOpen={true}
resetKey={modalResetKey()} resetKey={modalResetKey()}
@ -6873,11 +6876,9 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pmg" nodeType="pmg"
editingNode={editingNode()?.type === 'pmg' ? (editingNode() ?? undefined) : undefined} editingNode={editingNode()?.type === 'pmg' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined} securityStatus={securityStatus() ?? undefined}
temperatureMonitoringEnabled={ temperatureMonitoringEnabled={resolveTemperatureMonitoringEnabled(
editingNode()?.temperatureMonitoringEnabled !== undefined editingNode()?.type === 'pmg' ? editingNode() : null,
? editingNode()!.temperatureMonitoringEnabled )}
: temperatureMonitoringEnabled()
}
temperatureMonitoringLocked={temperatureMonitoringLocked()} temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()} savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={ onToggleTemperatureMonitoring={

View file

@ -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 { UpdatesAPI, type UpdateStatus } from '@/api/updates';
import { apiFetch } from '@/utils/apiClient';
import { logger } from '@/utils/logger'; import { logger } from '@/utils/logger';
interface UpdateProgressModalProps { interface UpdateProgressModalProps {
@ -16,7 +17,6 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
const [hasError, setHasError] = createSignal(false); const [hasError, setHasError] = createSignal(false);
const [isRestarting, setIsRestarting] = createSignal(false); const [isRestarting, setIsRestarting] = createSignal(false);
const [wsDisconnected, setWsDisconnected] = createSignal(false); const [wsDisconnected, setWsDisconnected] = createSignal(false);
const [usingSSE, setUsingSSE] = createSignal(false);
let pollInterval: number | undefined; let pollInterval: number | undefined;
let healthCheckTimer: number | undefined; let healthCheckTimer: number | undefined;
let healthCheckAttempts = 0; let healthCheckAttempts = 0;
@ -30,12 +30,12 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
}; };
const closeSSE = () => { const closeSSE = () => {
if (eventSource) { if (!eventSource) {
eventSource.close(); return;
eventSource = undefined;
setUsingSSE(false);
logger.info('SSE connection closed');
} }
eventSource.close();
eventSource = undefined;
logger.info('SSE connection closed');
}; };
const setupSSE = () => { const setupSSE = () => {
@ -45,7 +45,6 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
try { try {
// Create EventSource connection to SSE endpoint // Create EventSource connection to SSE endpoint
eventSource = new EventSource('/api/updates/stream'); eventSource = new EventSource('/api/updates/stream');
setUsingSSE(true);
eventSource.onopen = () => { eventSource.onopen = () => {
logger.info('SSE connection established'); logger.info('SSE connection established');
@ -73,7 +72,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
if (updateStatus.status === 'completed' && !updateStatus.error) { if (updateStatus.status === 'completed' && !updateStatus.error) {
closeSSE(); closeSSE();
// Verify backend health and reload // Verify backend health and reload
fetch('/api/health', { cache: 'no-store' }) apiFetch('/api/health', { cache: 'no-store' })
.then((healthCheck) => { .then((healthCheck) => {
if (healthCheck.ok) { if (healthCheck.ok) {
logger.info('Update completed, backend healthy, reloading...'); logger.info('Update completed, backend healthy, reloading...');
@ -157,7 +156,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
} }
// Verify backend is healthy and reload // Verify backend is healthy and reload
try { try {
const healthCheck = await fetch('/api/health', { cache: 'no-store' }); const healthCheck = await apiFetch('/api/health', { cache: 'no-store' });
if (healthCheck.ok) { if (healthCheck.ok) {
logger.info('Update completed, backend healthy, reloading...'); logger.info('Update completed, backend healthy, reloading...');
window.location.reload(); window.location.reload();
@ -214,7 +213,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
let isHealthy = false; let isHealthy = false;
try { try {
const response = await fetch('/api/health', { cache: 'no-store' }); const response = await apiFetch('/api/health', { cache: 'no-store' });
if (response.ok) { if (response.ok) {
isHealthy = true; isHealthy = true;
} }
@ -258,7 +257,7 @@ export function UpdateProgressModal(props: UpdateProgressModalProps) {
// Give it a moment for the backend to fully initialize // Give it a moment for the backend to fully initialize
setTimeout(async () => { setTimeout(async () => {
try { try {
const response = await fetch('/api/health', { cache: 'no-store' }); const response = await apiFetch('/api/health', { cache: 'no-store' });
if (response.ok) { if (response.ok) {
logger.info('Backend healthy after websocket reconnect, reloading...'); logger.info('Backend healthy after websocket reconnect, reloading...');
window.location.reload(); window.location.reload();

View file

@ -651,20 +651,20 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
: 'text-green-600 dark:text-green-400'; : 'text-green-600 dark:text-green-400';
const temp = node!.temperature; const temp = node!.temperature;
const cpuMin = temp?.cpuMin; const cpuMinValue =
const cpuMax = temp?.cpuMaxRecord; typeof temp?.cpuMin === 'number' && temp.cpuMin > 0 ? temp.cpuMin : null;
const hasMinMax = const cpuMaxValue =
typeof cpuMin === 'number' && typeof temp?.cpuMaxRecord === 'number' && temp.cpuMaxRecord > 0
cpuMin > 0 && ? temp.cpuMaxRecord
typeof cpuMax === 'number' && : null;
cpuMax > 0; const hasMinMax = cpuMinValue !== null && cpuMaxValue !== null;
const gpus = temp?.gpu ?? []; const gpus = temp?.gpu ?? [];
const hasGPU = gpus.length > 0; const hasGPU = gpus.length > 0;
if (hasMinMax || hasGPU) { if (hasMinMax || hasGPU) {
const min = Math.round(cpuMin); const min = Math.round(cpuMinValue!);
const max = Math.round(cpuMax); const max = Math.round(cpuMaxValue!);
const getTooltipColor = (temp: number) => { const getTooltipColor = (temp: number) => {
if (temp >= 80) return 'text-red-400'; if (temp >= 80) return 'text-red-400';

View file

@ -5,7 +5,7 @@
* Optimized for rendering many sparklines simultaneously in tables. * 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 { Portal } from 'solid-js/web';
import type { MetricSnapshot } from '@/stores/metricsHistory'; import type { MetricSnapshot } from '@/stores/metricsHistory';
import { scheduleSparkline } from '@/utils/canvasRenderQueue'; import { scheduleSparkline } from '@/utils/canvasRenderQueue';
@ -202,12 +202,6 @@ export const Sparkline: Component<SparklineProps> = (props) => {
// Redraw when data or dimensions change // Redraw when data or dimensions change
createEffect(() => { 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 // Unregister previous draw callback if it exists
if (unregister) { if (unregister) {
unregister(); unregister();
@ -230,7 +224,6 @@ export const Sparkline: Component<SparklineProps> = (props) => {
const rect = canvasRef.getBoundingClientRect(); const rect = canvasRef.getBoundingClientRect();
const mouseX = e.clientX - rect.left; const mouseX = e.clientX - rect.left;
const w = width(); const w = width();
const h = height();
const data = props.data; const data = props.data;
const values = data.map(d => d[props.metric]); const values = data.map(d => d[props.metric]);
@ -244,10 +237,6 @@ export const Sparkline: Component<SparklineProps> = (props) => {
const timestamp = data[nearestIndex].timestamp; const timestamp = data[nearestIndex].timestamp;
// Calculate absolute viewport position for portal // Calculate absolute viewport position for portal
const minValue = 0;
const maxValue = Math.max(100, ...values);
const y = h - ((value - minValue) / (maxValue - minValue)) * h;
setHoveredPoint({ setHoveredPoint({
value, value,
timestamp, timestamp,

View file

@ -14,7 +14,6 @@ import { recordMetric } from './metricsHistory';
import { getMetricsViewMode } from './metricsViewMode'; import { getMetricsViewMode } from './metricsViewMode';
import { buildMetricKey } from '@/utils/metricsKeys'; import { buildMetricKey } from '@/utils/metricsKeys';
import { logger } from '@/utils/logger'; import { logger } from '@/utils/logger';
import type { Node, VM, Container, DockerHost } from '@/types/api';
const SAMPLE_INTERVAL_MS = 30 * 1000; // 30 seconds const SAMPLE_INTERVAL_MS = 30 * 1000; // 30 seconds
@ -110,9 +109,9 @@ function sampleMetrics(): void {
const contCpu = container.cpuPercent || 0; const contCpu = container.cpuPercent || 0;
const contMem = container.memoryPercent || 0; const contMem = container.memoryPercent || 0;
const contDisk = container.disk && container.disk.total > 0 const usableTotal = container.rootFilesystemBytes ?? 0;
? (container.disk.used / container.disk.total) * 100 const writable = container.writableLayerBytes ?? 0;
: 0; const contDisk = usableTotal > 0 ? (writable / usableTotal) * 100 : 0;
const containerKey = buildMetricKey('dockerContainer', container.id); const containerKey = buildMetricKey('dockerContainer', container.id);
recordMetric(containerKey, contCpu, contMem, contDisk); recordMetric(containerKey, contCpu, contMem, contDisk);

View file

@ -4,6 +4,8 @@
import { logger } from '@/utils/logger'; import { logger } from '@/utils/logger';
import { STORAGE_KEYS } from '@/utils/localStorage'; import { STORAGE_KEYS } from '@/utils/localStorage';
const AUTH_STORAGE_KEY = STORAGE_KEYS.AUTH;
const getSessionStorage = (): Storage | undefined => { const getSessionStorage = (): Storage | undefined => {
if (typeof window === 'undefined') { if (typeof window === 'undefined') {
return undefined; return undefined;