Bladeren bron

Merge branch '0.2.1' into feature/cost_tracker

MartinNYHC 3 maanden geleden
bovenliggende
commit
c72366c9d3

+ 9 - 1
CHANGELOG.md

@@ -2,7 +2,15 @@
 
 All notable changes to Bambuddy will be documented in this file.
 
-## [0.2.1b] - Not released
+## [0.2.1] - Unreleased
+
+### Fixed
+- **Queue Stuck on "Busy" for "Any Model" Jobs** ([#435](https://github.com/maziggy/bambuddy/issues/435)) — When a print was queued with "Any [Model]" (e.g., "Any P1S"), it was created with `printer_id=NULL` and `target_model="P1S"`. After the assigned printer finished, the queue widget queried only for items matching `printer_id=X`, missing the next pending model-based item (`printer_id IS NULL`). With no next item found, the "Clear Plate & Start Next" button never appeared, leaving the scheduler stuck reporting "Busy". The queue API now accepts an optional `target_model` parameter; when combined with `printer_id`, it uses OR logic to also return unassigned items whose `target_model` matches the printer's model. The frontend passes the printer's model through to this query.
+
+### New Features
+- **Include Beta Updates Setting** — New toggle in Settings → Updates to opt in to beta/prerelease update notifications. Default: off (stable only). The update checker now fetches `/releases` instead of `/releases/latest` and filters by `parse_version()` prerelease detection (not GitHub's `prerelease` flag, which may not be set correctly). Users on the Docker `latest` tag will no longer see notifications for beta releases they can't install.
+
+## [0.2.1b] - 2026-02-19
 
 ### Fixed
 - **PAUSED State Never Matched** ([#447](https://github.com/maziggy/bambuddy/issues/447)) — Removed dead `PAUSED` checks across frontend and backend. The printer only sends `PAUSE` via MQTT `gcode_state`, so `PAUSED` comparisons were unreachable code.

+ 17 - 1
backend/app/api/routes/print_queue.py

@@ -8,7 +8,7 @@ from pathlib import Path
 
 import defusedxml.ElementTree as ET
 from fastapi import APIRouter, Depends, HTTPException, Query
-from sqlalchemy import func, select
+from sqlalchemy import and_, func, or_, select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
@@ -248,6 +248,9 @@ def _enrich_response(item: PrintQueueItem) -> PrintQueueItemResponse:
 async def list_queue(
     printer_id: int | None = Query(None, description="Filter by printer (-1 for unassigned)"),
     status: str | None = Query(None, description="Filter by status"),
+    target_model: str | None = Query(
+        None, description="Filter by target model (also includes model-based items when combined with printer_id)"
+    ),
     db: AsyncSession = Depends(get_db),
     _: User | None = RequirePermissionIfAuthEnabled(Permission.QUEUE_READ),
 ):
@@ -267,8 +270,21 @@ async def list_queue(
         if printer_id == -1:
             # Special value: filter for unassigned items
             query = query.where(PrintQueueItem.printer_id.is_(None))
+        elif target_model:
+            # Include both printer-specific items AND model-based (unassigned) items
+            query = query.where(
+                or_(
+                    PrintQueueItem.printer_id == printer_id,
+                    and_(
+                        PrintQueueItem.printer_id.is_(None),
+                        func.lower(PrintQueueItem.target_model) == target_model.lower(),
+                    ),
+                )
+            )
         else:
             query = query.where(PrintQueueItem.printer_id == printer_id)
+    elif target_model:
+        query = query.where(func.lower(PrintQueueItem.target_model) == target_model.lower())
     if status:
         query = query.where(PrintQueueItem.status == status)
 

+ 1 - 0
backend/app/api/routes/settings.py

@@ -89,6 +89,7 @@ async def get_settings(
                 "spoolman_report_partial_usage",
                 "check_updates",
                 "check_printer_firmware",
+                "include_beta_updates",
                 "virtual_printer_enabled",
                 "ftp_retry_enabled",
                 "mqtt_enabled",

+ 36 - 2
backend/app/api/routes/updates.py

@@ -189,6 +189,11 @@ async def check_for_updates(
             "message": "Update checks are disabled",
         }
 
+    # Check if beta updates should be included
+    result = await db.execute(select(Settings).where(Settings.key == "include_beta_updates"))
+    beta_setting = result.scalar_one_or_none()
+    include_beta = beta_setting and beta_setting.value.lower() == "true"
+
     _update_status = {
         "status": "checking",
         "progress": 0,
@@ -199,7 +204,7 @@ async def check_for_updates(
     try:
         async with httpx.AsyncClient() as client:
             response = await client.get(
-                f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest",
+                f"https://api.github.com/repos/{GITHUB_REPO}/releases?per_page=20",
                 headers={"Accept": "application/vnd.github.v3+json"},
                 timeout=10.0,
             )
@@ -220,7 +225,36 @@ async def check_for_updates(
                 }
 
             response.raise_for_status()
-            release_data = response.json()
+            releases = response.json()
+
+            # Find the appropriate release based on beta setting
+            release_data = None
+            for release in releases:
+                tag = release.get("tag_name", "")
+                if include_beta:
+                    # Accept any release (first = newest)
+                    release_data = release
+                    break
+                else:
+                    # Skip prereleases (based on version parsing, not GitHub flag)
+                    parsed = parse_version(tag)
+                    if parsed[4] == 0:  # is_prerelease == 0
+                        release_data = release
+                        break
+
+            if not release_data:
+                _update_status = {
+                    "status": "idle",
+                    "progress": 100,
+                    "message": "No releases found",
+                    "error": None,
+                }
+                return {
+                    "update_available": False,
+                    "current_version": APP_VERSION,
+                    "latest_version": None,
+                    "message": "No releases found",
+                }
 
             latest_version = release_data.get("tag_name", "").lstrip("v")
             release_name = release_data.get("name", latest_version)

+ 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.2.1b"
+APP_VERSION = "0.2.1"
 GITHUB_REPO = "maziggy/bambuddy"
 
 # App directory - where the application is installed (for static files)

+ 2 - 0
backend/app/schemas/settings.py

@@ -35,6 +35,7 @@ class AppSettings(BaseModel):
     # Updates
     check_updates: bool = Field(default=True, description="Automatically check for updates on startup")
     check_printer_firmware: bool = Field(default=True, description="Check for printer firmware updates from Bambu Lab")
+    include_beta_updates: bool = Field(default=False, description="Include beta/prerelease versions in update checks")
 
     # Language
     notification_language: str = Field(default="en", description="Language for push notifications (en, de)")
@@ -165,6 +166,7 @@ class AppSettingsUpdate(BaseModel):
     spoolman_report_partial_usage: bool | None = None
     check_updates: bool | None = None
     check_printer_firmware: bool | None = None
+    include_beta_updates: bool | None = None
     notification_language: str | None = None
     bed_cooled_threshold: float | None = None
     ams_humidity_good: int | None = None

+ 3 - 1
frontend/src/api/client.ts

@@ -761,6 +761,7 @@ export interface AppSettings {
   energy_tracking_mode: 'print' | 'total';
   check_updates: boolean;
   check_printer_firmware: boolean;
+  include_beta_updates: boolean;
   notification_language: string;
   // AMS threshold settings
   ams_humidity_good: number;  // <= this is green
@@ -3190,10 +3191,11 @@ export const api = {
     request<HASensorEntity[]>('/smart-plugs/ha/sensors'),
 
   // Print Queue
-  getQueue: (printerId?: number, status?: string) => {
+  getQueue: (printerId?: number, status?: string, targetModel?: string) => {
     const params = new URLSearchParams();
     if (printerId) params.set('printer_id', String(printerId));
     if (status) params.set('status', status);
+    if (targetModel) params.set('target_model', targetModel);
     return request<PrintQueueItem[]>(`/queue/?${params}`);
   },
   getQueueItem: (id: number) => request<PrintQueueItem>(`/queue/${id}`),

+ 4 - 3
frontend/src/components/PrinterQueueWidget.tsx

@@ -9,18 +9,19 @@ import { formatRelativeTime } from '../utils/date';
 
 interface PrinterQueueWidgetProps {
   printerId: number;
+  printerModel?: string | null;
   printerState?: string | null;
   plateCleared?: boolean;
 }
 
-export function PrinterQueueWidget({ printerId, printerState, plateCleared }: PrinterQueueWidgetProps) {
+export function PrinterQueueWidget({ printerId, printerModel, printerState, plateCleared }: PrinterQueueWidgetProps) {
   const { t } = useTranslation();
   const queryClient = useQueryClient();
   const { showToast } = useToast();
   const { hasPermission } = useAuth();
   const { data: queue } = useQuery({
-    queryKey: ['queue', printerId, 'pending'],
-    queryFn: () => api.getQueue(printerId, 'pending'),
+    queryKey: ['queue', printerId, 'pending', printerModel],
+    queryFn: () => api.getQueue(printerId, 'pending', printerModel || undefined),
     refetchInterval: 30000,
   });
 

+ 2 - 0
frontend/src/i18n/locales/de.ts

@@ -1259,6 +1259,8 @@ export default {
     // Updates
     checkForUpdatesLabel: 'Nach Updates suchen',
     checkPrinterFirmware: 'Drucker-Firmware prüfen',
+    includeBetaUpdates: 'Beta-Versionen einschließen',
+    includeBetaUpdatesDesc: 'Über Beta- und Vorabversionen bei der Updateprüfung benachrichtigen',
     // Queue
     enableRetry: 'Wiederholung aktivieren',
     // Home Assistant

+ 2 - 0
frontend/src/i18n/locales/en.ts

@@ -1259,6 +1259,8 @@ export default {
     // Updates
     checkForUpdatesLabel: 'Check for updates',
     checkPrinterFirmware: 'Check printer firmware',
+    includeBetaUpdates: 'Include beta versions',
+    includeBetaUpdatesDesc: 'Notify about beta and prerelease versions when checking for updates',
     // Queue
     enableRetry: 'Enable retry',
     // Home Assistant

+ 2 - 0
frontend/src/i18n/locales/fr.ts

@@ -1247,6 +1247,8 @@ export default {
     // Updates
     checkForUpdatesLabel: 'Vérifier les mises à jour',
     checkPrinterFirmware: 'Vérifier le firmware imprimante',
+    includeBetaUpdates: 'Inclure les versions bêta',
+    includeBetaUpdatesDesc: 'Notifier des versions bêta et préliminaires lors de la vérification des mises à jour',
     // Queue
     enableRetry: 'Activer la rétentative',
     // Home Assistant

+ 2 - 0
frontend/src/i18n/locales/it.ts

@@ -1151,6 +1151,8 @@ export default {
     // Updates
     checkForUpdatesLabel: 'Controlla aggiornamenti',
     checkPrinterFirmware: 'Controlla firmware stampante',
+    includeBetaUpdates: 'Includi versioni beta',
+    includeBetaUpdatesDesc: 'Notifica versioni beta e prerelease durante il controllo aggiornamenti',
     // Queue
     enableRetry: 'Abilita retry',
     // Home Assistant

+ 2 - 0
frontend/src/i18n/locales/ja.ts

@@ -1248,6 +1248,8 @@ export default {
     captureFinishPhoto: '完了写真を撮影',
     noPrintersConfigured: 'プリンターが設定されていません',
     checkPrinterFirmware: 'プリンターファームウェアの確認',
+    includeBetaUpdates: 'ベータ版を含める',
+    includeBetaUpdatesDesc: 'アップデート確認時にベータ版およびプレリリース版を通知する',
     enableRetry: 'リトライを有効化',
     homeAssistantDescription: 'Home Assistantに接続してHA REST APIでスマートプラグを制御します。switch、light、input_booleanエンティティに対応しています。',
     environmentManagedLabel: '(環境変数で管理)',

+ 2 - 0
frontend/src/i18n/locales/pt-BR.ts

@@ -1259,6 +1259,8 @@ export default {
     // Updates
     checkForUpdatesLabel: 'Verificar atualizações',
     checkPrinterFirmware: 'Verificar firmware da impressora',
+    includeBetaUpdates: 'Incluir versões beta',
+    includeBetaUpdatesDesc: 'Notificar sobre versões beta e pré-lançamento ao verificar atualizações',
     // Queue
     enableRetry: 'Habilitar tentativa',
     // Home Assistant

+ 1 - 1
frontend/src/pages/PrintersPage.tsx

@@ -2473,7 +2473,7 @@ function PrinterCard({
                 </div>
 
                 {/* Queue Widget - always visible when there are pending items */}
-                <PrinterQueueWidget printerId={printer.id} printerState={status.state} plateCleared={status.plate_cleared} />
+                <PrinterQueueWidget printerId={printer.id} printerModel={printer.model} printerState={status.state} plateCleared={status.plate_cleared} />
               </>
             )}
 

+ 20 - 0
frontend/src/pages/SettingsPage.tsx

@@ -699,6 +699,7 @@ export function SettingsPage() {
       settings.energy_tracking_mode !== localSettings.energy_tracking_mode ||
       settings.check_updates !== localSettings.check_updates ||
       (settings.check_printer_firmware ?? true) !== (localSettings.check_printer_firmware ?? true) ||
+      (settings.include_beta_updates ?? false) !== (localSettings.include_beta_updates ?? false) ||
       settings.notification_language !== localSettings.notification_language ||
       (settings.bed_cooled_threshold ?? 35) !== (localSettings.bed_cooled_threshold ?? 35) ||
       settings.ams_humidity_good !== localSettings.ams_humidity_good ||
@@ -764,6 +765,7 @@ export function SettingsPage() {
         energy_tracking_mode: localSettings.energy_tracking_mode,
         check_updates: localSettings.check_updates,
         check_printer_firmware: localSettings.check_printer_firmware,
+        include_beta_updates: localSettings.include_beta_updates,
         notification_language: localSettings.notification_language,
         bed_cooled_threshold: localSettings.bed_cooled_threshold,
         ams_humidity_good: localSettings.ams_humidity_good,
@@ -1654,6 +1656,24 @@ export function SettingsPage() {
                   <div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
                 </label>
               </div>
+              <div className={`flex items-center justify-between ${!localSettings.check_updates ? 'opacity-50' : ''}`}>
+                <div>
+                  <p className="text-white">{t('settings.includeBetaUpdates')}</p>
+                  <p className="text-sm text-bambu-gray">
+                    {t('settings.includeBetaUpdatesDesc')}
+                  </p>
+                </div>
+                <label className="relative inline-flex items-center cursor-pointer">
+                  <input
+                    type="checkbox"
+                    checked={localSettings.include_beta_updates ?? false}
+                    onChange={(e) => updateSetting('include_beta_updates', e.target.checked)}
+                    disabled={!localSettings.check_updates}
+                    className="sr-only peer"
+                  />
+                  <div className="w-11 h-6 bg-bambu-dark-tertiary peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-bambu-green"></div>
+                </label>
+              </div>
               <div className="border-t border-bambu-dark-tertiary pt-4">
                 <div className="flex items-center justify-between mb-2">
                   <div>

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


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-jjif866q.js"></script>
+    <script type="module" crossorigin src="/assets/index-Cwc85_YR.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-DPf6CLKV.css">
   </head>
   <body>

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