Browse Source

Merge pull request #43 from maziggy/0.1.6b4

**v0.1.6b4**

### New Features

    ## Printer Cards
    - Refactored AMS section for better visual grouping and spacing

### Bugfixes

    ## Printer Hour Counter
    - Fixed runtime_seconds not incrementing during prints
    - first timestamp was set but never committed

    ## Slicer Protocol
    - Add OS detection for slicer protocol handler
    - Windows: bambustudio://, macOS/Linux: bambustudioopen://
    - Updated all usages in Archives and Model Viewer

    ## Camera Popup Window
    - Auto-resize to fit video resolution on first open
    - Persist window size and position to localStorage
    - Restore saved window state for subsequent opens

    ## Maintenance Page
    - Improved duration display with better precision (weeks instead of imprecise months)
    - Large print hours now show readable units (e.g., 478h → 3w, 100h → 4d)

    ## Docker
    - Docker update detection for in-app updates
      - Added _is_docker_environment() function
      - Check endpoint returns is_docker and update_method fields
      - Apply endpoint rejects Docker with helpful instructions
      - Added updates API tests
MartinNYHC 4 months ago
parent
commit
bae287baa7

+ 1 - 0
CHANGELOG.md

@@ -5,6 +5,7 @@ All notable changes to Bambuddy will be documented in this file.
 ## [0.1.6b4] - 2026-01-01
 
 ### Added
+- **Docker update detection** - Automatically detects Docker installations and shows appropriate update instructions instead of failing with git errors
 - **Camera popup window improvements**
   - Auto-resize to fit video resolution on first open
   - Persist window size and position to localStorage

+ 29 - 0
backend/app/api/routes/updates.py

@@ -27,6 +27,20 @@ _update_status = {
 }
 
 
+def _is_docker_environment() -> bool:
+    """Detect if running inside a Docker container."""
+    if os.path.exists("/.dockerenv"):
+        return True
+    try:
+        with open("/proc/1/cgroup") as f:
+            if "docker" in f.read():
+                return True
+    except (FileNotFoundError, PermissionError):
+        pass
+    git_dir = settings.base_dir / ".git"
+    return not git_dir.exists()
+
+
 def _find_executable(name: str) -> str | None:
     """Find an executable in PATH or common locations."""
     # Try standard PATH first
@@ -199,6 +213,7 @@ async def check_for_updates(db: AsyncSession = Depends(get_db)):
                 "error": None,
             }
 
+            is_docker = _is_docker_environment()
             return {
                 "update_available": update_available,
                 "current_version": APP_VERSION,
@@ -207,6 +222,8 @@ async def check_for_updates(db: AsyncSession = Depends(get_db)):
                 "release_notes": release_notes,
                 "release_url": release_url,
                 "published_at": published_at,
+                "is_docker": is_docker,
+                "update_method": "docker" if is_docker else "git",
             }
 
     except httpx.HTTPError as e:
@@ -426,6 +443,18 @@ async def apply_update(background_tasks: BackgroundTasks):
             "status": _update_status,
         }
 
+    # Check if running in Docker
+    if _is_docker_environment():
+        return {
+            "success": False,
+            "is_docker": True,
+            "message": (
+                "Docker installations cannot be updated in-app. "
+                "Please update via Docker Compose: "
+                "git pull && docker compose build --pull && docker compose up -d"
+            ),
+        }
+
     # Start update in background
     background_tasks.add_task(_perform_update)
 

+ 1 - 1
backend/app/core/config.py

@@ -5,7 +5,7 @@ from pathlib import Path
 from pydantic_settings import BaseSettings
 
 # Application version - single source of truth
-APP_VERSION = "0.1.6b3"
+APP_VERSION = "0.1.6b4"
 GITHUB_REPO = "maziggy/bambuddy"
 
 # App directory - where the application is installed (for static files)

+ 47 - 0
backend/tests/integration/test_updates_api.py

@@ -0,0 +1,47 @@
+"""Integration tests for Updates API endpoints."""
+
+from unittest.mock import AsyncMock, MagicMock, patch
+
+import pytest
+from httpx import AsyncClient
+
+
+class TestUpdatesAPI:
+    @pytest.mark.asyncio
+    async def test_get_version(self, async_client: AsyncClient):
+        response = await async_client.get("/api/v1/updates/version")
+        assert response.status_code == 200
+
+    @pytest.mark.asyncio
+    async def test_apply_update_docker_rejection(self, async_client: AsyncClient):
+        with patch("backend.app.api.routes.updates._is_docker_environment", return_value=True):
+            response = await async_client.post("/api/v1/updates/apply")
+        result = response.json()
+        assert result["success"] is False
+        assert result["is_docker"] is True
+
+    @pytest.mark.asyncio
+    async def test_apply_update_non_docker(self, async_client: AsyncClient):
+        """Test non-Docker path - mock _perform_update to prevent side effects."""
+        with (
+            patch("backend.app.api.routes.updates._is_docker_environment", return_value=False),
+            patch("backend.app.api.routes.updates._perform_update", new_callable=AsyncMock),
+        ):
+            response = await async_client.post("/api/v1/updates/apply")
+        assert response.json()["success"] is True
+
+    def test_is_docker_with_dockerenv(self):
+        from backend.app.api.routes.updates import _is_docker_environment
+
+        with patch("os.path.exists", return_value=True):
+            assert _is_docker_environment() is True
+
+    def test_parse_version(self):
+        from backend.app.api.routes.updates import parse_version
+
+        assert parse_version("0.1.5")[:3] == (0, 1, 5)
+
+    def test_is_newer_version(self):
+        from backend.app.api.routes.updates import is_newer_version
+
+        assert is_newer_version("0.1.5", "0.1.5b7") is True