From f6326c8082dfbe8a65dcdb836d3e71c635594c26 Mon Sep 17 00:00:00 2001 From: Koya Kikuchi Date: Tue, 23 Jun 2026 05:49:57 +0900 Subject: [PATCH] feat(version): warn when the server was updated but not restarted (#898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Co-authored-by: Simos Mikelatos --- server/index.js | 16 ++++++++++++++- src/components/sidebar/view/Sidebar.tsx | 4 +++- .../view/subcomponents/SidebarCollapsed.tsx | 16 ++++++++++++++- .../view/subcomponents/SidebarContent.tsx | 3 +++ .../view/subcomponents/SidebarFooter.tsx | 20 ++++++++++++++++++- src/hooks/useVersionCheck.ts | 19 ++++++++++++++---- src/i18n/locales/de/sidebar.json | 3 ++- src/i18n/locales/en/sidebar.json | 3 ++- src/i18n/locales/fr/sidebar.json | 3 ++- src/i18n/locales/it/sidebar.json | 3 ++- src/i18n/locales/ja/sidebar.json | 3 ++- src/i18n/locales/ko/sidebar.json | 3 ++- src/i18n/locales/ru/sidebar.json | 3 ++- src/i18n/locales/tr/sidebar.json | 3 ++- src/i18n/locales/zh-CN/sidebar.json | 3 ++- src/i18n/locales/zh-TW/sidebar.json | 3 ++- 16 files changed, 90 insertions(+), 18 deletions(-) diff --git a/server/index.js b/server/index.js index f4c9e25e..d957ef58 100755 --- a/server/index.js +++ b/server/index.js @@ -76,6 +76,19 @@ const __dirname = getModuleDir(import.meta.url); // Resolving the app root once keeps every repo-level lookup below aligned across both layouts. const APP_ROOT = findAppRoot(__dirname); const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm'; +// Version of the code that is actually running, captured once at process +// startup. This intentionally does NOT re-read package.json per request: after +// an update replaces the files on disk, package.json reflects the NEW version +// while this long-lived process still runs the OLD code. The frontend bundle is +// rebuilt on update, so a mismatch between this value and the frontend's +// build-time version means the server was updated but not restarted. +const RUNNING_VERSION = (() => { + try { + return JSON.parse(fs.readFileSync(path.join(APP_ROOT, 'package.json'), 'utf8')).version || null; + } catch { + return null; + } +})(); const MAX_FILE_UPLOAD_SIZE_MB = 200; const MAX_FILE_UPLOAD_SIZE_BYTES = MAX_FILE_UPLOAD_SIZE_MB * 1024 * 1024; const MAX_FILE_UPLOAD_COUNT = 20; @@ -156,7 +169,8 @@ app.get('/health', (req, res) => { res.json({ status: 'ok', timestamp: new Date().toISOString(), - installMode + installMode, + version: RUNNING_VERSION }); }); diff --git a/src/components/sidebar/view/Sidebar.tsx b/src/components/sidebar/view/Sidebar.tsx index 15d96990..5e544b08 100644 --- a/src/components/sidebar/view/Sidebar.tsx +++ b/src/components/sidebar/view/Sidebar.tsx @@ -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} diff --git a/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx b/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx index 90a6338f..c4ae4300 100644 --- a/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarCollapsed.tsx @@ -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({ + {/* Restart-required indicator */} + {restartRequired && ( +
+ + +
+ )} + {/* Update indicator */} {updateAvailable && (