The update progress modal was stuck showing 'initializing' even after the backend restarted and websocket reconnected. Users could see the connection status badge reconnecting behind the modal, but the modal never cleared. Now the modal: - Watches websocket connection status during update - Detects when backend disconnects and reconnects - Verifies health after reconnection - Automatically reloads the page when update is complete - Shows clearer messaging about restart progress
310 lines
13 KiB
TypeScript
310 lines
13 KiB
TypeScript
import { Show, createSignal, createEffect, For, useContext } from 'solid-js';
|
|
import { useNavigate } from '@solidjs/router';
|
|
import { updateStore } from '@/stores/updates';
|
|
import { UpdatesAPI, type UpdatePlan } from '@/api/updates';
|
|
import { UpdateConfirmationModal } from './UpdateConfirmationModal';
|
|
import { UpdateProgressModal } from './UpdateProgressModal';
|
|
import { WebSocketContext } from '@/App';
|
|
|
|
export function UpdateBanner() {
|
|
const navigate = useNavigate();
|
|
const wsContext = useContext(WebSocketContext);
|
|
const [isExpanded, setIsExpanded] = createSignal(false);
|
|
const [updatePlan, setUpdatePlan] = createSignal<UpdatePlan | null>(null);
|
|
const [showConfirmModal, setShowConfirmModal] = createSignal(false);
|
|
const [showProgressModal, setShowProgressModal] = createSignal(false);
|
|
const [isApplying, setIsApplying] = createSignal(false);
|
|
const [copiedIndex, setCopiedIndex] = createSignal<number | null>(null);
|
|
|
|
// Fetch update plan when update info is available
|
|
createEffect(async () => {
|
|
const info = updateStore.updateInfo();
|
|
if (info?.available && info.latestVersion) {
|
|
try {
|
|
const plan = await UpdatesAPI.getUpdatePlan(info.latestVersion);
|
|
setUpdatePlan(plan);
|
|
} catch (error) {
|
|
console.error('Failed to fetch update plan:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
const handleApplyUpdate = () => {
|
|
setShowConfirmModal(true);
|
|
};
|
|
|
|
const handleConfirmUpdate = async () => {
|
|
const info = updateStore.updateInfo();
|
|
if (!info?.downloadUrl) return;
|
|
|
|
setIsApplying(true);
|
|
try {
|
|
await UpdatesAPI.applyUpdate(info.downloadUrl);
|
|
// Close confirmation and show progress
|
|
setShowConfirmModal(false);
|
|
setShowProgressModal(true);
|
|
} catch (error) {
|
|
console.error('Failed to start update:', error);
|
|
alert('Failed to start update. Please try again.');
|
|
} finally {
|
|
setIsApplying(false);
|
|
}
|
|
};
|
|
|
|
const copyToClipboard = async (text: string, index: number) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
setCopiedIndex(index);
|
|
setTimeout(() => setCopiedIndex(null), 2000);
|
|
} catch (error) {
|
|
console.error('Failed to copy:', error);
|
|
}
|
|
};
|
|
|
|
// Get deployment type message
|
|
const getUpdateInstructions = () => {
|
|
const versionInfo = updateStore.versionInfo();
|
|
const deploymentType = versionInfo?.deploymentType || 'systemd';
|
|
|
|
switch (deploymentType) {
|
|
case 'proxmoxve':
|
|
return "ProxmoxVE users: type 'update' in console";
|
|
case 'docker':
|
|
return 'Docker: pull latest image';
|
|
case 'source':
|
|
return 'Source: pull and rebuild';
|
|
default:
|
|
return ''; // No message, just the version info
|
|
}
|
|
};
|
|
|
|
const getShortMessage = () => {
|
|
const info = updateStore.updateInfo();
|
|
if (!info) return '';
|
|
return `New version available: ${info.latestVersion}`;
|
|
};
|
|
|
|
return (
|
|
<Show when={updateStore.isUpdateVisible()}>
|
|
<div class="bg-blue-50 dark:bg-blue-900/20 border-b border-blue-200 dark:border-blue-800 text-blue-800 dark:text-blue-200 relative animate-slideDown">
|
|
<div class="px-4 py-1.5">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
{/* Update icon */}
|
|
<svg
|
|
class="w-4 h-4 flex-shrink-0"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<path
|
|
d="M12 2v6m0 0l3-3m-3 3l-3-3"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
<path
|
|
d="M2 17l.621 2.485A2 2 0 0 0 4.561 21h14.878a2 2 0 0 0 1.94-1.515L22 17"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
/>
|
|
</svg>
|
|
|
|
<div class="flex items-center gap-3 flex-wrap">
|
|
<span class="text-sm font-medium">{getShortMessage()}</span>
|
|
|
|
{/* Apply Update Button (automated deployments) */}
|
|
<Show when={updatePlan()?.canAutoUpdate && !isExpanded()}>
|
|
<button
|
|
onClick={handleApplyUpdate}
|
|
class="px-3 py-1 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors"
|
|
>
|
|
Apply Update
|
|
</button>
|
|
</Show>
|
|
|
|
{/* Manual Steps Badge (non-automated deployments) */}
|
|
<Show when={updatePlan() && !updatePlan()?.canAutoUpdate && !isExpanded()}>
|
|
<span class="px-2 py-0.5 text-xs font-medium bg-orange-100 dark:bg-orange-900/30 text-orange-800 dark:text-orange-200 rounded">
|
|
Manual steps required
|
|
</span>
|
|
</Show>
|
|
|
|
{!isExpanded() && getUpdateInstructions() && (
|
|
<>
|
|
<span class="text-blue-600 dark:text-blue-400 text-sm hidden sm:inline">•</span>
|
|
<span class="text-blue-600 dark:text-blue-400 text-sm hidden sm:inline">
|
|
{getUpdateInstructions()}
|
|
</span>
|
|
</>
|
|
)}
|
|
{!isExpanded() && (
|
|
<a
|
|
href={`https://github.com/rcourtman/Pulse/releases/tag/${updateStore.updateInfo()?.latestVersion}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-blue-600 dark:text-blue-400 underline text-sm hidden sm:inline hover:text-blue-700 dark:hover:text-blue-300"
|
|
>
|
|
View details →
|
|
</a>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
{/* Expand/Collapse button */}
|
|
<button
|
|
onClick={() => setIsExpanded(!isExpanded())}
|
|
class="p-1 hover:bg-blue-100 dark:hover:bg-blue-800/30 rounded transition-colors"
|
|
title={isExpanded() ? 'Show less' : 'Show more'}
|
|
>
|
|
<svg
|
|
class={`w-4 h-4 transform transition-transform ${isExpanded() ? 'rotate-180' : ''}`}
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<polyline points="6 9 12 15 18 9"></polyline>
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Dismiss button */}
|
|
<button
|
|
onClick={() => updateStore.dismissUpdate()}
|
|
class="p-1 hover:bg-blue-100 dark:hover:bg-blue-800/30 rounded transition-colors"
|
|
title="Dismiss this update"
|
|
>
|
|
<svg
|
|
class="w-4 h-4"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
>
|
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Expanded content */}
|
|
<Show when={isExpanded()}>
|
|
<div class="mt-2 pb-1">
|
|
<div class="text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
|
<p>
|
|
<span class="font-medium">Current:</span>{' '}
|
|
{updateStore.versionInfo()?.version || 'Unknown'} →
|
|
<span class="font-medium ml-1">Latest:</span>{' '}
|
|
{updateStore.updateInfo()?.latestVersion}
|
|
</p>
|
|
{getUpdateInstructions() && (
|
|
<p>
|
|
<span class="font-medium">Quick upgrade:</span> {getUpdateInstructions()}
|
|
</p>
|
|
)}
|
|
<Show when={updateStore.updateInfo()?.isPrerelease}>
|
|
<p class="text-orange-600 dark:text-orange-400 text-xs">
|
|
This is a pre-release version
|
|
</p>
|
|
</Show>
|
|
|
|
{/* Manual Update Instructions */}
|
|
<Show when={updatePlan()?.instructions && (updatePlan()?.instructions?.length ?? 0) > 0}>
|
|
<div class="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
|
|
<div class="font-medium mb-2">Update Instructions:</div>
|
|
<div class="space-y-2">
|
|
<For each={updatePlan()?.instructions || []}>
|
|
{(instruction, index) => (
|
|
<div class="bg-gray-50 dark:bg-gray-900/50 rounded border border-blue-200 dark:border-blue-700 p-2">
|
|
<div class="flex items-start justify-between gap-2">
|
|
<code class="text-xs text-gray-800 dark:text-gray-200 font-mono flex-1 break-all">
|
|
{instruction}
|
|
</code>
|
|
<button
|
|
onClick={() => copyToClipboard(instruction, index())}
|
|
class="flex-shrink-0 p-1 hover:bg-blue-100 dark:hover:bg-blue-800/30 rounded transition-colors"
|
|
title="Copy to clipboard"
|
|
>
|
|
<Show when={copiedIndex() === index()} fallback={
|
|
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
</svg>
|
|
}>
|
|
<svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
</Show>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</For>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
|
|
{/* Apply Update Button (expanded view for automated deployments) */}
|
|
<Show when={updatePlan()?.canAutoUpdate}>
|
|
<div class="mt-3 pt-3 border-t border-blue-200 dark:border-blue-800">
|
|
<button
|
|
onClick={handleApplyUpdate}
|
|
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors"
|
|
>
|
|
Apply Update Automatically
|
|
</button>
|
|
</div>
|
|
</Show>
|
|
|
|
<div class="flex gap-3 mt-2">
|
|
<a
|
|
href={`https://github.com/rcourtman/Pulse/releases/tag/${updateStore.updateInfo()?.latestVersion}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
class="text-blue-600 dark:text-blue-400 underline hover:text-blue-700 dark:hover:text-blue-300 text-xs"
|
|
>
|
|
View release notes
|
|
</a>
|
|
<button
|
|
onClick={() => updateStore.dismissUpdate()}
|
|
class="text-blue-600/70 dark:text-blue-400/70 hover:text-blue-700 dark:hover:text-blue-300 text-xs underline"
|
|
>
|
|
Don't show again for this version
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Show>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Update Confirmation Modal */}
|
|
<UpdateConfirmationModal
|
|
isOpen={showConfirmModal()}
|
|
onClose={() => setShowConfirmModal(false)}
|
|
onConfirm={handleConfirmUpdate}
|
|
currentVersion={updateStore.versionInfo()?.version || 'Unknown'}
|
|
latestVersion={updateStore.updateInfo()?.latestVersion || ''}
|
|
plan={updatePlan() || {
|
|
canAutoUpdate: false,
|
|
requiresRoot: false,
|
|
rollbackSupport: false,
|
|
}}
|
|
isApplying={isApplying()}
|
|
/>
|
|
|
|
{/* Update Progress Modal */}
|
|
<UpdateProgressModal
|
|
isOpen={showProgressModal()}
|
|
onClose={() => setShowProgressModal(false)}
|
|
onViewHistory={() => {
|
|
setShowProgressModal(false);
|
|
navigate('/settings/updates');
|
|
}}
|
|
connected={wsContext?.connected}
|
|
reconnecting={wsContext?.reconnecting}
|
|
/>
|
|
</Show>
|
|
);
|
|
}
|