Improve auth timeout handling (related to #705)
This commit is contained in:
parent
4f950de29d
commit
3fa7356ff3
11 changed files with 87 additions and 97 deletions
|
|
@ -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',
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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={
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue