Ver código fonte

fix version manager

tuanchris 5 meses atrás
pai
commit
1c5567802f
5 arquivos alterados com 299 adições e 23 exclusões
  1. 1 0
      VERSION
  2. 44 0
      main.py
  3. 116 0
      modules/core/version_manager.py
  4. 132 18
      static/js/settings.js
  5. 6 5
      templates/settings.html

+ 1 - 0
VERSION

@@ -0,0 +1 @@
+3.0.0

+ 44 - 0
main.py

@@ -22,6 +22,7 @@ from contextlib import asynccontextmanager
 from modules.led.led_controller import LEDController, effect_idle
 import math
 from modules.core.cache_manager import generate_all_image_previews, get_cache_path, generate_image_preview, get_pattern_metadata
+from modules.core.version_manager import version_manager
 import json
 import base64
 import time
@@ -840,6 +841,49 @@ def signal_handler(signum, frame):
         logger.info("Exiting application...")
         os._exit(0)  # Force exit regardless of other threads
 
+@app.get("/api/version")
+async def get_version_info():
+    """Get current and latest version information"""
+    try:
+        version_info = await version_manager.get_version_info()
+        return JSONResponse(content=version_info)
+    except Exception as e:
+        logger.error(f"Error getting version info: {e}")
+        return JSONResponse(
+            content={
+                "current": version_manager.get_current_version(),
+                "latest": version_manager.get_current_version(),
+                "update_available": False,
+                "error": "Unable to check for updates"
+            },
+            status_code=200
+        )
+
+@app.post("/api/update")
+async def trigger_update():
+    """Trigger software update (placeholder for future implementation)"""
+    try:
+        # For now, just return the GitHub release URL
+        version_info = await version_manager.get_version_info()
+        if version_info.get("latest_release"):
+            return JSONResponse(content={
+                "success": False,
+                "message": "Automatic updates not implemented yet",
+                "manual_update_url": version_info["latest_release"].get("html_url"),
+                "instructions": "Please visit the GitHub release page to download and install the update manually"
+            })
+        else:
+            return JSONResponse(content={
+                "success": False,
+                "message": "No updates available"
+            })
+    except Exception as e:
+        logger.error(f"Error triggering update: {e}")
+        return JSONResponse(
+            content={"success": False, "message": "Failed to check for updates"},
+            status_code=500
+        )
+
 def entrypoint():
     import uvicorn
     logger.info("Starting FastAPI server on port 8080...")

+ 116 - 0
modules/core/version_manager.py

@@ -0,0 +1,116 @@
+"""
+Version management for Dune Weaver
+Handles current version reading and GitHub API integration for latest version checking
+"""
+
+import asyncio
+import aiohttp
+import json
+import os
+from pathlib import Path
+from typing import Dict, Optional
+import logging
+
+logger = logging.getLogger(__name__)
+
+class VersionManager:
+    def __init__(self):
+        self.repo_owner = "tuanchris"
+        self.repo_name = "dune-weaver"
+        self.github_api_url = f"https://api.github.com/repos/{self.repo_owner}/{self.repo_name}"
+        self._current_version = None
+        
+    def get_current_version(self) -> str:
+        """Read current version from VERSION file"""
+        if self._current_version is None:
+            try:
+                version_file = Path(__file__).parent.parent.parent / "VERSION"
+                if version_file.exists():
+                    self._current_version = version_file.read_text().strip()
+                else:
+                    logger.warning("VERSION file not found, using default version")
+                    self._current_version = "1.0.0"
+            except Exception as e:
+                logger.error(f"Error reading VERSION file: {e}")
+                self._current_version = "1.0.0"
+        
+        return self._current_version
+    
+    async def get_latest_release(self) -> Dict[str, any]:
+        """Get latest release info from GitHub API"""
+        try:
+            async with aiohttp.ClientSession() as session:
+                async with session.get(
+                    f"{self.github_api_url}/releases/latest",
+                    timeout=aiohttp.ClientTimeout(total=10)
+                ) as response:
+                    if response.status == 200:
+                        data = await response.json()
+                        return {
+                            "version": data.get("tag_name", "").lstrip("v"),
+                            "name": data.get("name", ""),
+                            "published_at": data.get("published_at", ""),
+                            "html_url": data.get("html_url", ""),
+                            "body": data.get("body", ""),
+                            "prerelease": data.get("prerelease", False)
+                        }
+                    elif response.status == 404:
+                        # No releases found
+                        logger.info("No releases found on GitHub")
+                        return None
+                    else:
+                        logger.warning(f"GitHub API returned status {response.status}")
+                        return None
+                        
+        except asyncio.TimeoutError:
+            logger.warning("Timeout while fetching latest release from GitHub")
+            return None
+        except Exception as e:
+            logger.error(f"Error fetching latest release: {e}")
+            return None
+    
+    def compare_versions(self, version1: str, version2: str) -> int:
+        """Compare two semantic versions. Returns -1, 0, or 1"""
+        try:
+            # Parse semantic versions (e.g., "1.2.3")
+            v1_parts = [int(x) for x in version1.split('.')]
+            v2_parts = [int(x) for x in version2.split('.')]
+            
+            # Pad shorter version with zeros
+            max_len = max(len(v1_parts), len(v2_parts))
+            v1_parts.extend([0] * (max_len - len(v1_parts)))
+            v2_parts.extend([0] * (max_len - len(v2_parts)))
+            
+            if v1_parts < v2_parts:
+                return -1
+            elif v1_parts > v2_parts:
+                return 1
+            else:
+                return 0
+                
+        except (ValueError, AttributeError):
+            logger.warning(f"Invalid version format: {version1} vs {version2}")
+            return 0
+    
+    async def get_version_info(self) -> Dict[str, any]:
+        """Get complete version information"""
+        current = self.get_current_version()
+        latest_release = await self.get_latest_release()
+        
+        if latest_release:
+            latest = latest_release["version"]
+            comparison = self.compare_versions(current, latest)
+            update_available = comparison < 0
+        else:
+            latest = current  # Fallback if no releases found
+            update_available = False
+            
+        return {
+            "current": current,
+            "latest": latest,
+            "update_available": update_available,
+            "latest_release": latest_release
+        }
+
+# Global instance
+version_manager = VersionManager()

+ 132 - 18
static/js/settings.js

@@ -165,7 +165,7 @@ document.addEventListener('DOMContentLoaded', async () => {
         fetch('/get_wled_ip').then(response => response.json()).catch(() => ({ wled_ip: null })),
         
         // Load current version and check for updates
-        fetch('/check_software_update').then(response => response.json()).catch(() => ({ current_version: '1.0.0', update_available: false })),
+        fetch('/api/version').then(response => response.json()).catch(() => ({ current: '1.0.0', latest: '1.0.0', update_available: false })),
         
         // Load available serial ports
         fetch('/list_serial_ports').then(response => response.json()).catch(() => [])
@@ -183,12 +183,39 @@ document.addEventListener('DOMContentLoaded', async () => {
         }
         
         // Update version display
-        document.querySelector('.text-slate-500.text-sm.font-normal.leading-normal').textContent = updateData.current_version;
-        
-        // Show/hide update button
+        const currentVersionText = document.getElementById('currentVersionText');
+        const latestVersionText = document.getElementById('latestVersionText');
         const updateButton = document.getElementById('updateSoftware');
-        if (updateButton) {
-            updateButton.style.display = updateData.update_available ? 'flex' : 'none';
+        const updateIcon = document.getElementById('updateIcon');
+        const updateText = document.getElementById('updateText');
+        
+        if (currentVersionText) {
+            currentVersionText.textContent = updateData.current;
+        }
+        
+        if (latestVersionText) {
+            if (updateData.error) {
+                latestVersionText.textContent = 'Error checking updates';
+                latestVersionText.className = 'text-red-500 text-sm font-normal leading-normal';
+            } else {
+                latestVersionText.textContent = updateData.latest;
+                latestVersionText.className = 'text-slate-500 text-sm font-normal leading-normal';
+            }
+        }
+        
+        // Update button state
+        if (updateButton && updateIcon && updateText) {
+            if (updateData.update_available) {
+                updateButton.disabled = false;
+                updateButton.className = 'flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors';
+                updateIcon.textContent = 'download';
+                updateText.textContent = 'Update';
+            } else {
+                updateButton.disabled = true;
+                updateButton.className = 'flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-gray-400 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors disabled:opacity-50 disabled:cursor-not-allowed';
+                updateIcon.textContent = 'check';
+                updateText.textContent = 'Up to date';
+            }
         }
         
         // Update port selection
@@ -274,17 +301,31 @@ function setupEventListeners() {
     const updateSoftware = document.getElementById('updateSoftware');
     if (updateSoftware) {
         updateSoftware.addEventListener('click', async () => {
+            if (updateSoftware.disabled) {
+                return;
+            }
+            
             try {
-                const response = await fetch('/update_software', {
+                const response = await fetch('/api/update', {
                     method: 'POST'
                 });
-                if (response.ok) {
-                    logMessage('Software update started successfully', LOG_TYPE.SUCCESS);
+                const data = await response.json();
+                
+                if (data.success) {
+                    showStatusMessage('Software update started successfully', 'success');
+                } else if (data.manual_update_url) {
+                    // Show modal with manual update instructions, but use wiki link
+                    const wikiData = {
+                        ...data,
+                        manual_update_url: 'https://github.com/tuanchris/dune-weaver/wiki/Updating-software'
+                    };
+                    showUpdateInstructionsModal(wikiData);
                 } else {
-                    throw new Error('Failed to start software update');
+                    showStatusMessage(data.message || 'No updates available', 'info');
                 }
             } catch (error) {
                 logMessage(`Error updating software: ${error.message}`, LOG_TYPE.ERROR);
+                showStatusMessage('Failed to check for updates', 'error');
             }
         });
     }
@@ -343,7 +384,8 @@ function setupEventListeners() {
 document.addEventListener('DOMContentLoaded', function() {
     // Home button
     const homeButton = document.getElementById('homeButton');
-    homeButton.addEventListener('click', async () => {
+    if (homeButton) {
+        homeButton.addEventListener('click', async () => {
         try {
             const response = await fetch('/send_home', {
                 method: 'POST',
@@ -359,11 +401,13 @@ document.addEventListener('DOMContentLoaded', function() {
             console.error('Error sending home command:', error);
             updateStatus('Error: Failed to move to home position');
         }
-    });
+        });
+    }
 
     // Stop button
     const stopButton = document.getElementById('stopButton');
-    stopButton.addEventListener('click', async () => {
+    if (stopButton) {
+        stopButton.addEventListener('click', async () => {
         try {
             const response = await fetch('/stop_execution', {
                 method: 'POST',
@@ -379,11 +423,13 @@ document.addEventListener('DOMContentLoaded', function() {
             console.error('Error stopping execution:', error);
             updateStatus('Error: Failed to stop execution');
         }
-    });
+        });
+    }
 
     // Move to Center button
     const centerButton = document.getElementById('centerButton');
-    centerButton.addEventListener('click', async () => {
+    if (centerButton) {
+        centerButton.addEventListener('click', async () => {
         try {
             const response = await fetch('/move_to_center', {
                 method: 'POST',
@@ -399,11 +445,13 @@ document.addEventListener('DOMContentLoaded', function() {
             console.error('Error moving to center:', error);
             updateStatus('Error: Failed to move to center');
         }
-    });
+        });
+    }
 
     // Move to Perimeter button
     const perimeterButton = document.getElementById('perimeterButton');
-    perimeterButton.addEventListener('click', async () => {
+    if (perimeterButton) {
+        perimeterButton.addEventListener('click', async () => {
         try {
             const response = await fetch('/move_to_perimeter', {
                 method: 'POST',
@@ -419,7 +467,8 @@ document.addEventListener('DOMContentLoaded', function() {
             console.error('Error moving to perimeter:', error);
             updateStatus('Error: Failed to move to perimeter');
         }
-    });
+        });
+    }
 });
 
 // Function to update status
@@ -434,4 +483,69 @@ function updateStatus(message) {
             }, 3000);
         }
     }
+}
+
+// Function to show status messages (using existing base.js showStatusMessage if available)
+function showStatusMessage(message, type) {
+    if (typeof window.showStatusMessage === 'function') {
+        window.showStatusMessage(message, type);
+    } else {
+        // Fallback to console logging
+        console.log(`[${type}] ${message}`);
+    }
+}
+
+// Function to show update instructions modal
+function showUpdateInstructionsModal(data) {
+    // Create modal HTML
+    const modal = document.createElement('div');
+    modal.id = 'updateInstructionsModal';
+    modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
+    modal.innerHTML = `
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md">
+            <div class="p-6">
+                <div class="text-center mb-4">
+                    <h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200 mb-2">Manual Update Required</h2>
+                    <p class="text-gray-600 dark:text-gray-400 text-sm">
+                        ${data.message}
+                    </p>
+                </div>
+                
+                <div class="text-gray-700 dark:text-gray-300 text-sm mb-6">
+                    <p class="mb-3">${data.instructions}</p>
+                </div>
+                
+                <div class="flex gap-3 justify-center">
+                    <button id="openGitHubRelease" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors">
+                        View Update Instructions
+                    </button>
+                    <button id="closeUpdateModal" class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg transition-colors">
+                        Close
+                    </button>
+                </div>
+            </div>
+        </div>
+    `;
+    
+    document.body.appendChild(modal);
+    
+    // Add event listeners
+    const openGitHubButton = modal.querySelector('#openGitHubRelease');
+    const closeButton = modal.querySelector('#closeUpdateModal');
+    
+    openGitHubButton.addEventListener('click', () => {
+        window.open(data.manual_update_url, '_blank');
+        document.body.removeChild(modal);
+    });
+    
+    closeButton.addEventListener('click', () => {
+        document.body.removeChild(modal);
+    });
+    
+    // Close on outside click
+    modal.addEventListener('click', (e) => {
+        if (e.target === modal) {
+            document.body.removeChild(modal);
+        }
+    });
 } 

+ 6 - 5
templates/settings.html

@@ -205,7 +205,7 @@ endblock %}
           <p class="text-slate-800 text-base font-medium leading-normal">
             Current Version
           </p>
-          <p class="text-slate-500 text-sm font-normal leading-normal">1.0.0</p>
+          <p id="currentVersionText" class="text-slate-500 text-sm font-normal leading-normal">Loading...</p>
         </div>
       </div>
       <div class="flex items-center gap-4 px-6 py-5">
@@ -218,14 +218,15 @@ endblock %}
           <p class="text-slate-800 text-base font-medium leading-normal">
             Latest Version
           </p>
-          <p class="text-slate-500 text-sm font-normal leading-normal">1.0.1</p>
+          <p id="latestVersionText" class="text-slate-500 text-sm font-normal leading-normal">Checking...</p>
         </div>
         <button
           id="updateSoftware"
-          class="flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-emerald-500 hover:bg-emerald-600 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors"
+          class="flex items-center justify-center gap-1.5 min-w-[84px] cursor-pointer rounded-lg h-9 px-3 bg-gray-400 text-white text-xs font-medium leading-normal tracking-[0.015em] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+          disabled
         >
-          <span class="material-icons text-base">download</span>
-          <span class="truncate">Update</span>
+          <span id="updateIcon" class="material-icons text-base">download</span>
+          <span id="updateText" class="truncate">Update</span>
         </button>
       </div>
     </div>