diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 9c414d0..f42b0c3 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -163,7 +163,6 @@ export const adminApi = { addons: () => apiClient.get('/admin/addons').then(r => r.data), updateAddon: (id: number | string, data: Record) => apiClient.put(`/admin/addons/${id}`, data).then(r => r.data), checkVersion: () => apiClient.get('/admin/version-check').then(r => r.data), - installUpdate: () => apiClient.post('/admin/update', {}, { timeout: 300000 }).then(r => r.data), getBagTracking: () => apiClient.get('/admin/bag-tracking').then(r => r.data), updateBagTracking: (enabled: boolean) => apiClient.put('/admin/bag-tracking', { enabled }).then(r => r.data), packingTemplates: () => apiClient.get('/admin/packing-templates').then(r => r.data), diff --git a/client/src/pages/AdminPage.tsx b/client/src/pages/AdminPage.tsx index b1c44db..3c5e928 100644 --- a/client/src/pages/AdminPage.tsx +++ b/client/src/pages/AdminPage.tsx @@ -16,7 +16,7 @@ import PackingTemplateManager from '../components/Admin/PackingTemplateManager' import AuditLogPanel from '../components/Admin/AuditLogPanel' import AdminMcpTokensPanel from '../components/Admin/AdminMcpTokensPanel' import PermissionsPanel from '../components/Admin/PermissionsPanel' -import { Users, Map, Briefcase, Shield, Trash2, Edit2, Camera, FileText, Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, UserPlus, ArrowUpCircle, ExternalLink, Download, AlertTriangle, RefreshCw, GitBranch, Sun, Link2, Copy, Plus } from 'lucide-react' +import { Users, Map, Briefcase, Shield, Trash2, Edit2, Camera, FileText, Eye, EyeOff, Save, CheckCircle, XCircle, Loader2, UserPlus, ArrowUpCircle, ExternalLink, Download, GitBranch, Sun, Link2, Copy, Plus } from 'lucide-react' import CustomSelect from '../components/shared/CustomSelect' interface AdminUser { @@ -121,8 +121,6 @@ export default function AdminPage(): React.ReactElement { // Version check & update const [updateInfo, setUpdateInfo] = useState(null) const [showUpdateModal, setShowUpdateModal] = useState(false) - const [updating, setUpdating] = useState(false) - const [updateResult, setUpdateResult] = useState<'success' | 'error' | null>(null) const { user: currentUser, updateApiKeys, setAppRequireMfa, setTripRemindersEnabled } = useAuthStore() const navigate = useNavigate() @@ -177,26 +175,6 @@ export default function AdminPage(): React.ReactElement { } } - const handleInstallUpdate = async () => { - setUpdating(true) - setUpdateResult(null) - try { - await adminApi.installUpdate() - setUpdateResult('success') - // Server is restarting — poll until it comes back, then reload - const poll = setInterval(async () => { - try { - await authApi.getAppConfig() - clearInterval(poll) - window.location.reload() - } catch { /* still restarting */ } - }, 2000) - } catch { - setUpdateResult('error') - setUpdating(false) - } - } - const handleToggleRegistration = async (value) => { setAllowRegistration(value) try { @@ -394,23 +372,13 @@ export default function AdminPage(): React.ReactElement { {t('admin.update.button')} )} - {updateInfo.is_docker ? ( - - ) : ( - - )} + )} @@ -1303,78 +1271,37 @@ export default function AdminPage(): React.ReactElement { )} - {/* Update confirmation popup — matches backup restore style */} + {/* Update instructions popup */} {showUpdateModal && (
{ if (!updating) setShowUpdateModal(false) }} + onClick={() => setShowUpdateModal(false)} >
e.stopPropagation()} style={{ width: '100%', maxWidth: 440, borderRadius: 16, overflow: 'hidden' }} className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700" > - {updateResult === 'success' ? ( - <> -
-
- -
-
-

{t('admin.update.success')}

-
-
-
- -

{t('admin.update.reloadHint')}

-
- - ) : updateResult === 'error' ? ( - <> -
-
- -
-
-

{t('admin.update.failed')}

-
-
-
- -
- - ) : ( - <> - {/* Red header */} -
-
- -
-
-

{t('admin.update.confirmTitle')}

-

- v{updateInfo?.current} → v{updateInfo?.latest} -

-
-
+
+
+ +
+
+

{t('admin.update.howTo')}

+

+ v{updateInfo?.current} → v{updateInfo?.latest} +

+
+
- {/* Body */} -
- {updateInfo?.is_docker ? ( - <> -

- {t('admin.update.dockerText').replace('{version}', `v${updateInfo.latest}`)} -

+
+

+ {t('admin.update.dockerText').replace('{version}', `v${updateInfo?.latest ?? ''}`)} +

-
+
{`docker pull mauriceboe/nomad:latest docker stop nomad && docker rm nomad docker run -d --name nomad \\ @@ -1383,87 +1310,42 @@ docker run -d --name nomad \\ -v /opt/nomad/uploads:/app/uploads \\ --restart unless-stopped \\ mauriceboe/nomad:latest`} -
+
-
-
- - {t('admin.update.dataInfo')} -
-
- - ) : ( - <> -

- {updateInfo && t('admin.update.confirmText').replace('{current}', `v${updateInfo.current}`).replace('{version}', `v${updateInfo.latest}`)} -

- -
-
- - {t('admin.update.dataInfo')} -
-
- -
-
- - - {t('admin.update.backupHint')}{' '} - - -
-
- -
-
- - {t('admin.update.warning')} -
-
- - )} +
+
+ + {t('admin.update.dataInfo')}
+
- {/* Footer */} -
- - {!updateInfo?.is_docker && ( - - )} + {updateInfo?.release_url && ( + - - )} + )} +
+ +
+ +
)} diff --git a/server/src/routes/admin.ts b/server/src/routes/admin.ts index 674aa31..eac08dd 100644 --- a/server/src/routes/admin.ts +++ b/server/src/routes/admin.ts @@ -1,7 +1,6 @@ import express, { Request, Response } from 'express'; import bcrypt from 'bcryptjs'; import crypto from 'crypto'; -import { execSync } from 'child_process'; import path from 'path'; import fs from 'fs'; import { db } from '../db/database'; @@ -326,49 +325,6 @@ router.get('/version-check', async (_req: Request, res: Response) => { } }); -router.post('/update', async (req: Request, res: Response) => { - const rootDir = path.resolve(__dirname, '../../..'); - const serverDir = path.resolve(__dirname, '../..'); - const clientDir = path.join(rootDir, 'client'); - const steps: { step: string; success?: boolean; output?: string; version?: string }[] = []; - - try { - const pullOutput = execSync('git pull origin main', { cwd: rootDir, timeout: 60000, encoding: 'utf8' }); - steps.push({ step: 'git pull', success: true, output: pullOutput.trim() }); - - execSync('npm install --production --ignore-scripts', { cwd: serverDir, timeout: 120000, encoding: 'utf8' }); - steps.push({ step: 'npm install (server)', success: true }); - - if (process.env.NODE_ENV === 'production') { - execSync('npm install --ignore-scripts', { cwd: clientDir, timeout: 120000, encoding: 'utf8' }); - execSync('npm run build', { cwd: clientDir, timeout: 120000, encoding: 'utf8' }); - steps.push({ step: 'npm install + build (client)', success: true }); - } - - delete require.cache[require.resolve('../../package.json')]; - const { version: newVersion } = require('../../package.json'); - steps.push({ step: 'version', version: newVersion }); - - const authReq = req as AuthRequest; - writeAudit({ - userId: authReq.user.id, - action: 'admin.system_update', - resource: newVersion, - ip: getClientIp(req), - }); - res.json({ success: true, steps, restarting: true }); - - setTimeout(() => { - console.log('[Update] Restarting after update...'); - process.exit(0); - }, 1000); - } catch (err: unknown) { - console.error(err); - steps.push({ step: 'error', success: false, output: 'Internal error' }); - res.status(500).json({ success: false, steps }); - } -}); - // ── Invite Tokens ─────────────────────────────────────────────────────────── router.get('/invites', (_req: Request, res: Response) => {