Browse Source

Fixed os.path issue in update module

maziggy 5 months ago
parent
commit
ce18b38264

+ 110 - 20
backend/app/api/routes/updates.py

@@ -2,7 +2,8 @@
 
 import asyncio
 import logging
-import subprocess
+import os
+import shutil
 import sys
 from pathlib import Path
 
@@ -27,6 +28,30 @@ _update_status = {
 }
 
 
+def _find_executable(name: str) -> str | None:
+    """Find an executable in PATH or common locations."""
+    # Try standard PATH first
+    path = shutil.which(name)
+    if path:
+        return path
+
+    # Common locations for executables (useful when running as systemd service)
+    common_paths = [
+        f"/usr/bin/{name}",
+        f"/usr/local/bin/{name}",
+        f"/opt/homebrew/bin/{name}",
+        f"/home/linuxbrew/.linuxbrew/bin/{name}",
+        f"{os.path.expanduser('~')}/.nvm/current/bin/{name}",
+        f"{os.path.expanduser('~')}/.local/bin/{name}",
+    ]
+
+    for p in common_paths:
+        if os.path.isfile(p) and os.access(p, os.X_OK):
+            return p
+
+    return None
+
+
 def parse_version(version: str) -> tuple[int, ...]:
     """Parse version string into tuple for comparison."""
     # Remove 'v' prefix if present
@@ -140,21 +165,55 @@ async def check_for_updates(db: AsyncSession = Depends(get_db)):
 
 
 async def _perform_update():
-    """Perform the actual update using git pull."""
+    """Perform the actual update using git fetch and reset."""
     global _update_status
 
     try:
+        base_dir = settings.base_dir
+
+        # Find git executable (may not be in PATH when running as systemd service)
+        git_path = _find_executable("git")
+        if not git_path:
+            _update_status = {
+                "status": "error",
+                "progress": 0,
+                "message": "Git not found",
+                "error": "Could not find git executable. Please ensure git is installed.",
+            }
+            return
+
+        logger.info(f"Using git at: {git_path}")
+
+        # Git config to avoid safe.directory issues
+        git_config = ["-c", f"safe.directory={base_dir}"]
+
+        _update_status = {
+            "status": "downloading",
+            "progress": 10,
+            "message": "Configuring git...",
+            "error": None,
+        }
+
+        # Ensure remote uses HTTPS (SSH may not be available)
+        https_url = f"https://github.com/{GITHUB_REPO}.git"
+        process = await asyncio.create_subprocess_exec(
+            git_path, *git_config, "remote", "set-url", "origin", https_url,
+            cwd=str(base_dir),
+            stdout=asyncio.subprocess.PIPE,
+            stderr=asyncio.subprocess.PIPE,
+        )
+        await process.communicate()
+
         _update_status = {
             "status": "downloading",
             "progress": 20,
-            "message": "Pulling latest changes...",
+            "message": "Fetching latest changes...",
             "error": None,
         }
 
-        # Run git pull in the project directory
-        base_dir = settings.base_dir
+        # Fetch from origin
         process = await asyncio.create_subprocess_exec(
-            "git", "pull", "--rebase",
+            git_path, *git_config, "fetch", "origin", "main",
             cwd=str(base_dir),
             stdout=asyncio.subprocess.PIPE,
             stderr=asyncio.subprocess.PIPE,
@@ -162,12 +221,39 @@ async def _perform_update():
         stdout, stderr = await process.communicate()
 
         if process.returncode != 0:
-            error_msg = stderr.decode() if stderr else "Git pull failed"
-            logger.error(f"Git pull failed: {error_msg}")
+            error_msg = stderr.decode() if stderr else "Git fetch failed"
+            logger.error(f"Git fetch failed: {error_msg}")
             _update_status = {
                 "status": "error",
                 "progress": 0,
-                "message": "Failed to pull updates",
+                "message": "Failed to fetch updates",
+                "error": error_msg,
+            }
+            return
+
+        _update_status = {
+            "status": "downloading",
+            "progress": 40,
+            "message": "Applying updates...",
+            "error": None,
+        }
+
+        # Hard reset to origin/main (clean update, no merge conflicts)
+        process = await asyncio.create_subprocess_exec(
+            git_path, *git_config, "reset", "--hard", "origin/main",
+            cwd=str(base_dir),
+            stdout=asyncio.subprocess.PIPE,
+            stderr=asyncio.subprocess.PIPE,
+        )
+        stdout, stderr = await process.communicate()
+
+        if process.returncode != 0:
+            error_msg = stderr.decode() if stderr else "Git reset failed"
+            logger.error(f"Git reset failed: {error_msg}")
+            _update_status = {
+                "status": "error",
+                "progress": 0,
+                "message": "Failed to apply updates",
                 "error": error_msg,
             }
             return
@@ -191,19 +277,21 @@ async def _perform_update():
         if process.returncode != 0:
             logger.warning(f"pip install warning: {stderr.decode() if stderr else 'unknown'}")
 
-        _update_status = {
-            "status": "installing",
-            "progress": 70,
-            "message": "Building frontend...",
-            "error": None,
-        }
-
-        # Build frontend
+        # Try to build frontend if npm is available (optional - static files are pre-built)
+        npm_path = _find_executable("npm")
         frontend_dir = base_dir / "frontend"
-        if frontend_dir.exists():
+
+        if npm_path and frontend_dir.exists():
+            _update_status = {
+                "status": "installing",
+                "progress": 70,
+                "message": "Building frontend...",
+                "error": None,
+            }
+
             # npm install
             process = await asyncio.create_subprocess_exec(
-                "npm", "install",
+                npm_path, "install",
                 cwd=str(frontend_dir),
                 stdout=asyncio.subprocess.PIPE,
                 stderr=asyncio.subprocess.PIPE,
@@ -212,7 +300,7 @@ async def _perform_update():
 
             # npm run build
             process = await asyncio.create_subprocess_exec(
-                "npm", "run", "build",
+                npm_path, "run", "build",
                 cwd=str(frontend_dir),
                 stdout=asyncio.subprocess.PIPE,
                 stderr=asyncio.subprocess.PIPE,
@@ -221,6 +309,8 @@ async def _perform_update():
 
             if process.returncode != 0:
                 logger.warning(f"Frontend build warning: {stderr.decode() if stderr else 'unknown'}")
+        else:
+            logger.info("npm not found or frontend dir missing - using pre-built static files")
 
         _update_status = {
             "status": "complete",

+ 43 - 1
frontend/src/components/Layout.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect, useCallback, useRef } from 'react';
 import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
-import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, type LucideIcon } from 'lucide-react';
+import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, X, type LucideIcon } from 'lucide-react';
 import { useTranslation } from 'react-i18next';
 import { useTheme } from '../contexts/ThemeContext';
 import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
@@ -80,6 +80,9 @@ export function Layout() {
   const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
   const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
   const hasRedirected = useRef(false);
+  const [dismissedUpdateVersion, setDismissedUpdateVersion] = useState<string | null>(() =>
+    sessionStorage.getItem('dismissedUpdateVersion')
+  );
 
   // Check for updates
   const { data: versionInfo } = useQuery({
@@ -102,6 +105,18 @@ export function Layout() {
     refetchInterval: 60 * 60 * 1000, // Check every hour
   });
 
+  // Show update banner if update available and not dismissed for this version
+  const showUpdateBanner = updateCheck?.update_available &&
+    updateCheck.latest_version &&
+    updateCheck.latest_version !== dismissedUpdateVersion;
+
+  const dismissUpdateBanner = () => {
+    if (updateCheck?.latest_version) {
+      sessionStorage.setItem('dismissedUpdateVersion', updateCheck.latest_version);
+      setDismissedUpdateVersion(updateCheck.latest_version);
+    }
+  };
+
   // Redirect to default view on initial load
   useEffect(() => {
     if (!hasRedirected.current && location.pathname === '/') {
@@ -345,6 +360,33 @@ export function Layout() {
 
       {/* Main content */}
       <main className={`flex-1 bg-bambu-dark overflow-auto ${sidebarExpanded ? 'ml-64' : 'ml-16'} transition-all duration-300`}>
+        {/* Persistent update banner */}
+        {showUpdateBanner && (
+          <div className="bg-bambu-green/20 border-b border-bambu-green/30 px-4 py-2 flex items-center justify-between">
+            <div className="flex items-center gap-2 text-sm">
+              <ArrowUpCircle className="w-4 h-4 text-bambu-green" />
+              <span>
+                {t('nav.updateAvailableBanner', {
+                  version: updateCheck?.latest_version,
+                  defaultValue: `Version ${updateCheck?.latest_version} is available!`
+                })}
+              </span>
+              <button
+                onClick={() => navigate('/settings')}
+                className="text-bambu-green hover:text-bambu-green/80 font-medium underline"
+              >
+                {t('nav.viewUpdate', { defaultValue: 'View update' })}
+              </button>
+            </div>
+            <button
+              onClick={dismissUpdateBanner}
+              className="p-1 hover:bg-bambu-dark-tertiary rounded transition-colors"
+              title={t('common.dismiss', { defaultValue: 'Dismiss' })}
+            >
+              <X className="w-4 h-4" />
+            </button>
+          </div>
+        )}
         <Outlet />
       </main>
 

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-BdUzePl9.js


+ 1 - 1
static/index.html

@@ -7,7 +7,7 @@
     <link rel="icon" type="image/png" sizes="32x32" href="/img/favicon-32x32.png" />
     <link rel="icon" type="image/png" sizes="16x16" href="/img/favicon-16x16.png" />
     <link rel="apple-touch-icon" sizes="180x180" href="/img/apple-touch-icon.png" />
-    <script type="module" crossorigin src="/assets/index-E695Z6RQ.js"></script>
+    <script type="module" crossorigin src="/assets/index-BdUzePl9.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-Crbfjp9b.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff