import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { authenticatedFetch } from "../../../utils/api"; import { ReleaseInfo } from "../../../types/sharedTypes"; import { copyTextToClipboard } from "../../../utils/clipboard"; import type { InstallMode } from "../../../hooks/useVersionCheck"; import { IS_PLATFORM } from "../../../constants/config"; interface VersionUpgradeModalProps { isOpen: boolean; onClose: () => void; releaseInfo: ReleaseInfo | null; currentVersion: string; latestVersion: string | null; installMode: InstallMode; } const RELOAD_COUNTDOWN_START = 30; export function VersionUpgradeModal({ isOpen, onClose, releaseInfo, currentVersion, latestVersion, installMode }: VersionUpgradeModalProps) { const { t } = useTranslation('common'); const upgradeCommand = installMode === 'npm' ? t('versionUpdate.npmUpgradeCommand') : IS_PLATFORM ? 'npm run update:platform' : 'git checkout main && git pull && npm install'; const [isUpdating, setIsUpdating] = useState(false); const [updateOutput, setUpdateOutput] = useState(''); const [updateError, setUpdateError] = useState(''); const [reloadCountdown, setReloadCountdown] = useState(null); useEffect(() => { if (!IS_PLATFORM || reloadCountdown === null || reloadCountdown <= 0) { return; } const timeoutId = window.setTimeout(() => { setReloadCountdown((previousCountdown) => { if (previousCountdown === null) { return null; } return Math.max(previousCountdown - 1, 0); }); }, 1000); return () => window.clearTimeout(timeoutId); }, [reloadCountdown]); const handleUpdateNow = useCallback(async () => { setIsUpdating(true); setUpdateOutput('Starting update...\n'); setReloadCountdown(IS_PLATFORM ? RELOAD_COUNTDOWN_START : null); setUpdateError(''); try { // Call the backend API to run the update command const response = await authenticatedFetch('/api/system/update', { method: 'POST', }); const data = await response.json(); if (response.ok) { setUpdateOutput(prev => prev + data.output + '\n'); setUpdateOutput(prev => prev + '\n✅ Update completed successfully!\n'); setUpdateOutput(prev => prev + 'Please restart the server to apply changes.' + '\n'); } else { setUpdateError(data.error || 'Update failed'); setUpdateOutput(prev => prev + '\n❌ Update failed: ' + (data.error || 'Unknown error') + '\n'); } } catch (error: any) { setUpdateError(error.message); setUpdateOutput(prev => prev + '\n❌ Update failed: ' + error.message + '\n'); } finally { setIsUpdating(false); } }, []); if (!isOpen) return null; return (
{/* Backdrop */}
{/* Version Info */}
{t('versionUpdate.currentVersion')} {currentVersion}
{t('versionUpdate.latestVersion')} {latestVersion}
{/* Changelog */} {releaseInfo?.body && (

{t('versionUpdate.whatsNew')}

{releaseInfo?.htmlUrl && ( {t('versionUpdate.viewFullRelease')} )}
{cleanChangelog(releaseInfo.body)}
)} {/* Update Output */} {(updateOutput || updateError) && (

{t('versionUpdate.updateProgress')}

{updateOutput}
{IS_PLATFORM && reloadCountdown !== null && (
{reloadCountdown === 0 ? 'Refresh the page now. If that doesn\'t work, RESTART the environment.' : `Refresh the page in ${reloadCountdown} ${reloadCountdown === 1 ? 'second' : 'seconds'}. If that doesn\'t work, RESTART the environment.`}
)} {updateError && (
{updateError}
)}
)} {/* Upgrade Instructions */} {!isUpdating && !updateOutput && (

{t('versionUpdate.manualUpgrade')}

{upgradeCommand}

{t('versionUpdate.manualUpgradeHint')}

)} {/* Actions */}
{!updateOutput && ( <> )}
); }; // Clean up changelog by removing GitHub-specific metadata const cleanChangelog = (body: string) => { if (!body) return ''; return body // Remove full commit hashes (40 character hex strings) .replace(/\b[0-9a-f]{40}\b/gi, '') // Remove short commit hashes (7-10 character hex strings at start of line or after dash/space) .replace(/(?:^|\s|-)([0-9a-f]{7,10})\b/gi, '') // Remove "Full Changelog" links .replace(/\*\*Full Changelog\*\*:.*$/gim, '') // Remove compare links (e.g., https://github.com/.../compare/v1.0.0...v1.0.1) .replace(/https?:\/\/github\.com\/[^\/]+\/[^\/]+\/compare\/[^\s)]+/gi, '') // Clean up multiple consecutive empty lines .replace(/\n\s*\n\s*\n/g, '\n\n') // Trim whitespace .trim(); };