mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-24 19:35:44 +08:00
feat(version): warn when the server was updated but not restarted (#898)
When the package is updated on disk but the long-lived server process is not restarted, the new frontend bundle (served from disk) talks to the old running backend. New DB-backed features then fail silently — e.g. deleting/archiving a session appears to do nothing — because the new schema/routes only take effect on restart. Nothing currently detects this skew: useVersionCheck only compares the frontend's build-time version against the latest GitHub release. This exposes the running server's version (captured once at startup) via /health, compares it to the frontend's build-time version in useVersionCheck, and shows a "restart required" banner in the sidebar (and a small indicator in the collapsed sidebar) when they differ. - server: add `version` (RUNNING_VERSION, read once at startup) to /health - useVersionCheck: return `restartRequired` / `runningVersion` - SidebarFooter / SidebarCollapsed: surface a restart-required banner - i18n: add `version.restartRequired` to all 10 sidebar locales Verified with `tsc --noEmit` (client + server) and eslint. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
This commit is contained in:
@@ -43,7 +43,7 @@ function Sidebar({
|
||||
}: SidebarProps) {
|
||||
const { t } = useTranslation(['sidebar', 'common']);
|
||||
const { isPWA } = useDeviceSettings({ trackMobile: false });
|
||||
const { updateAvailable, latestVersion, currentVersion, releaseInfo, installMode } = useVersionCheck(
|
||||
const { updateAvailable, restartRequired, latestVersion, currentVersion, releaseInfo, installMode } = useVersionCheck(
|
||||
'siteboon',
|
||||
'claudecodeui',
|
||||
);
|
||||
@@ -224,6 +224,7 @@ function Sidebar({
|
||||
onExpand={handleExpandSidebar}
|
||||
onShowSettings={onShowSettings}
|
||||
updateAvailable={updateAvailable}
|
||||
restartRequired={restartRequired}
|
||||
onShowVersionModal={() => setShowVersionModal(true)}
|
||||
t={t}
|
||||
/>
|
||||
@@ -296,6 +297,7 @@ function Sidebar({
|
||||
onCreateProject={() => setShowNewProject(true)}
|
||||
onCollapseSidebar={handleCollapseSidebar}
|
||||
updateAvailable={updateAvailable}
|
||||
restartRequired={restartRequired}
|
||||
releaseInfo={releaseInfo}
|
||||
latestVersion={latestVersion}
|
||||
currentVersion={currentVersion}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Settings, Sparkles, PanelLeftOpen, Bug } from 'lucide-react';
|
||||
import { Settings, Sparkles, PanelLeftOpen, Bug, AlertTriangle } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
const DISCORD_INVITE_URL = 'https://discord.gg/buxwujPNRE';
|
||||
@@ -16,6 +16,7 @@ type SidebarCollapsedProps = {
|
||||
onExpand: () => void;
|
||||
onShowSettings: () => void;
|
||||
updateAvailable: boolean;
|
||||
restartRequired: boolean;
|
||||
onShowVersionModal: () => void;
|
||||
t: TFunction;
|
||||
};
|
||||
@@ -24,6 +25,7 @@ export default function SidebarCollapsed({
|
||||
onExpand,
|
||||
onShowSettings,
|
||||
updateAvailable,
|
||||
restartRequired,
|
||||
onShowVersionModal,
|
||||
t,
|
||||
}: SidebarCollapsedProps) {
|
||||
@@ -75,6 +77,18 @@ export default function SidebarCollapsed({
|
||||
<DiscordIcon className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-foreground" />
|
||||
</a>
|
||||
|
||||
{/* Restart-required indicator */}
|
||||
{restartRequired && (
|
||||
<div
|
||||
className="relative flex h-8 w-8 items-center justify-center rounded-lg"
|
||||
aria-label={t('version.restartRequired')}
|
||||
title={t('version.restartRequired')}
|
||||
>
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||
<span className="absolute right-1.5 top-1.5 h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Update indicator */}
|
||||
{updateAvailable && (
|
||||
<button
|
||||
|
||||
@@ -141,6 +141,7 @@ type SidebarContentProps = {
|
||||
onCreateProject: () => void;
|
||||
onCollapseSidebar: () => void;
|
||||
updateAvailable: boolean;
|
||||
restartRequired: boolean;
|
||||
releaseInfo: ReleaseInfo | null;
|
||||
latestVersion: string | null;
|
||||
currentVersion: string;
|
||||
@@ -178,6 +179,7 @@ export default function SidebarContent({
|
||||
onCreateProject,
|
||||
onCollapseSidebar,
|
||||
updateAvailable,
|
||||
restartRequired,
|
||||
releaseInfo,
|
||||
latestVersion,
|
||||
currentVersion,
|
||||
@@ -553,6 +555,7 @@ export default function SidebarContent({
|
||||
|
||||
<SidebarFooter
|
||||
updateAvailable={updateAvailable}
|
||||
restartRequired={restartRequired}
|
||||
releaseInfo={releaseInfo}
|
||||
latestVersion={latestVersion}
|
||||
currentVersion={currentVersion}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Settings, ArrowUpCircle, Bug } from 'lucide-react';
|
||||
import { Settings, ArrowUpCircle, Bug, AlertTriangle } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { IS_PLATFORM } from '../../../../constants/config';
|
||||
import type { ReleaseInfo } from '../../../../types/sharedTypes';
|
||||
@@ -18,6 +18,7 @@ function DiscordIcon({ className }: { className?: string }) {
|
||||
|
||||
type SidebarFooterProps = {
|
||||
updateAvailable: boolean;
|
||||
restartRequired: boolean;
|
||||
releaseInfo: ReleaseInfo | null;
|
||||
latestVersion: string | null;
|
||||
currentVersion: string;
|
||||
@@ -28,6 +29,7 @@ type SidebarFooterProps = {
|
||||
|
||||
export default function SidebarFooter({
|
||||
updateAvailable,
|
||||
restartRequired,
|
||||
releaseInfo,
|
||||
latestVersion,
|
||||
currentVersion,
|
||||
@@ -37,6 +39,22 @@ export default function SidebarFooter({
|
||||
}: SidebarFooterProps) {
|
||||
return (
|
||||
<div className="flex-shrink-0" style={{ paddingBottom: 'env(safe-area-inset-bottom, 0)' }}>
|
||||
{/* Restart-required banner: the running server version differs from the
|
||||
installed/frontend version (updated but not restarted). */}
|
||||
{restartRequired && (
|
||||
<>
|
||||
<div className="nav-divider" />
|
||||
<div className="px-2 py-1.5 md:px-2 md:py-1.5">
|
||||
<div className="flex items-center gap-2.5 rounded-lg border border-amber-300/60 bg-amber-50/80 px-2.5 py-2 dark:border-amber-700/40 dark:bg-amber-900/15">
|
||||
<AlertTriangle className="h-4 w-4 flex-shrink-0 text-amber-500 dark:text-amber-400" />
|
||||
<span className="min-w-0 flex-1 text-xs font-medium text-amber-700 dark:text-amber-300">
|
||||
{t('version.restartRequired')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Update banner */}
|
||||
{updateAvailable && (
|
||||
<>
|
||||
|
||||
@@ -28,20 +28,31 @@ export const useVersionCheck = (owner: string, repo: string) => {
|
||||
const [latestVersion, setLatestVersion] = useState<string | null>(null);
|
||||
const [releaseInfo, setReleaseInfo] = useState<ReleaseInfo | null>(null);
|
||||
const [installMode, setInstallMode] = useState<InstallMode>('git');
|
||||
const [runningVersion, setRunningVersion] = useState<string | null>(null);
|
||||
const [restartRequired, setRestartRequired] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchInstallMode = async () => {
|
||||
const fetchHealth = async () => {
|
||||
try {
|
||||
const response = await fetch('/health');
|
||||
const data = await response.json();
|
||||
if (data.installMode === 'npm' || data.installMode === 'git') {
|
||||
setInstallMode(data.installMode);
|
||||
}
|
||||
// `data.version` is the version the server process is actually running.
|
||||
// This module's `version` is baked into the frontend bundle at build
|
||||
// time, so it reflects the installed (on-disk) package. If they differ,
|
||||
// the package was updated but the server process was not restarted, and
|
||||
// DB-backed actions may silently fail until it is.
|
||||
if (typeof data.version === 'string' && data.version.length > 0) {
|
||||
setRunningVersion(data.version);
|
||||
setRestartRequired(data.version !== version);
|
||||
}
|
||||
} catch {
|
||||
// Default to git on error
|
||||
// Default to git / no restart hint on error
|
||||
}
|
||||
};
|
||||
fetchInstallMode();
|
||||
fetchHealth();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -84,5 +95,5 @@ export const useVersionCheck = (owner: string, repo: string) => {
|
||||
return () => clearInterval(interval);
|
||||
}, [owner, repo]);
|
||||
|
||||
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo, installMode };
|
||||
return { updateAvailable, latestVersion, currentVersion: version, releaseInfo, installMode, runningVersion, restartRequired };
|
||||
};
|
||||
@@ -115,7 +115,8 @@
|
||||
"restoreSessionError": "Fehler beim Wiederherstellen der Sitzung. Bitte erneut versuchen."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "Update verfügbar"
|
||||
"updateAvailable": "Update verfügbar",
|
||||
"restartRequired": "Update installiert – zum Anwenden Server neu starten"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "Projekte",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"restoreSessionError": "Error restoring session. Please try again."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "Update available"
|
||||
"updateAvailable": "Update available",
|
||||
"restartRequired": "Update installed — restart the server to apply"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "Projects",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"restoreSessionError": "Erreur lors de la restauration de la session. Veuillez réessayer."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "Mise à jour disponible"
|
||||
"updateAvailable": "Mise à jour disponible",
|
||||
"restartRequired": "Mise à jour installée — redémarrez le serveur pour l'appliquer"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "Projets",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"restoreSessionError": "Errore durante il ripristino della sessione. Riprova."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "Aggiornamento disponibile"
|
||||
"updateAvailable": "Aggiornamento disponibile",
|
||||
"restartRequired": "Aggiornamento installato — riavvia il server per applicarlo"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "Progetti",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"restoreSessionError": "セッションの復元でエラーが発生しました。もう一度お試しください。"
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "アップデートあり"
|
||||
"updateAvailable": "アップデートあり",
|
||||
"restartRequired": "更新が適用されていません。サーバーを再起動してください"
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"deleteProject": "プロジェクトを除去",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"restoreSessionError": "세션 복원 오류. 다시 시도해주세요."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "업데이트 가능"
|
||||
"updateAvailable": "업데이트 가능",
|
||||
"restartRequired": "업데이트가 설치됨 — 적용하려면 서버를 재시작하세요"
|
||||
},
|
||||
"deleteConfirmation": {
|
||||
"deleteProject": "프로젝트 제거",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"restoreSessionError": "Ошибка при восстановлении сеанса. Попробуйте снова."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "Доступно обновление"
|
||||
"updateAvailable": "Доступно обновление",
|
||||
"restartRequired": "Обновление установлено — перезапустите сервер для применения"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "Проекты",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"restoreSessionError": "Oturum geri yüklenirken hata oluştu. Lütfen tekrar dene."
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "Güncelleme mevcut"
|
||||
"updateAvailable": "Güncelleme mevcut",
|
||||
"restartRequired": "Güncelleme yüklendi — uygulamak için sunucuyu yeniden başlatın"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "Projeler",
|
||||
|
||||
@@ -115,7 +115,8 @@
|
||||
"restoreSessionError": "恢复会话时出错,请重试。"
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "有可用更新"
|
||||
"updateAvailable": "有可用更新",
|
||||
"restartRequired": "已安装更新 — 请重启服务器以生效"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "项目",
|
||||
|
||||
@@ -114,7 +114,8 @@
|
||||
"restoreSessionError": "還原工作階段時出錯,請重試。"
|
||||
},
|
||||
"version": {
|
||||
"updateAvailable": "有可用更新"
|
||||
"updateAvailable": "有可用更新",
|
||||
"restartRequired": "已安裝更新 — 請重新啟動伺服器以套用"
|
||||
},
|
||||
"search": {
|
||||
"modeProjects": "專案",
|
||||
|
||||
Reference in New Issue
Block a user