mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-09 15:35:39 +02:00
Added update module
This commit is contained in:
@@ -46,7 +46,7 @@ async def get_settings(db: AsyncSession = Depends(get_db)):
|
||||
for setting in db_settings:
|
||||
if setting.key in settings_dict:
|
||||
# Parse the value based on the expected type
|
||||
if setting.key in ["auto_archive", "save_thumbnails", "capture_finish_photo", "spoolman_enabled"]:
|
||||
if setting.key in ["auto_archive", "save_thumbnails", "capture_finish_photo", "spoolman_enabled", "check_updates"]:
|
||||
settings_dict[setting.key] = setting.value.lower() == "true"
|
||||
elif setting.key in ["default_filament_cost", "energy_cost_per_kwh"]:
|
||||
settings_dict[setting.key] = float(setting.value)
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
"""Update checking and management routes."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from backend.app.core.config import APP_VERSION, GITHUB_REPO, settings
|
||||
from backend.app.core.database import get_db
|
||||
from backend.app.api.routes.settings import get_setting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/updates", tags=["updates"])
|
||||
|
||||
# Global state for update progress
|
||||
_update_status = {
|
||||
"status": "idle", # idle, checking, downloading, installing, complete, error
|
||||
"progress": 0,
|
||||
"message": "",
|
||||
"error": None,
|
||||
}
|
||||
|
||||
|
||||
def parse_version(version: str) -> tuple[int, ...]:
|
||||
"""Parse version string into tuple for comparison."""
|
||||
# Remove 'v' prefix if present
|
||||
version = version.lstrip("v")
|
||||
# Split and convert to integers
|
||||
parts = []
|
||||
for part in version.split("."):
|
||||
try:
|
||||
parts.append(int(part))
|
||||
except ValueError:
|
||||
# Handle pre-release versions like "1.0.0-beta"
|
||||
num = "".join(c for c in part if c.isdigit())
|
||||
parts.append(int(num) if num else 0)
|
||||
return tuple(parts)
|
||||
|
||||
|
||||
def is_newer_version(latest: str, current: str) -> bool:
|
||||
"""Check if latest version is newer than current."""
|
||||
try:
|
||||
return parse_version(latest) > parse_version(current)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
@router.get("/version")
|
||||
async def get_version():
|
||||
"""Get current application version."""
|
||||
return {
|
||||
"version": APP_VERSION,
|
||||
"repo": GITHUB_REPO,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/check")
|
||||
async def check_for_updates(db: AsyncSession = Depends(get_db)):
|
||||
"""Check GitHub for available updates."""
|
||||
global _update_status
|
||||
|
||||
_update_status = {
|
||||
"status": "checking",
|
||||
"progress": 0,
|
||||
"message": "Checking for updates...",
|
||||
"error": None,
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(
|
||||
f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest",
|
||||
headers={"Accept": "application/vnd.github.v3+json"},
|
||||
timeout=10.0,
|
||||
)
|
||||
|
||||
if response.status_code == 404:
|
||||
# No releases yet
|
||||
_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",
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
release_data = response.json()
|
||||
|
||||
latest_version = release_data.get("tag_name", "").lstrip("v")
|
||||
release_name = release_data.get("name", latest_version)
|
||||
release_notes = release_data.get("body", "")
|
||||
release_url = release_data.get("html_url", "")
|
||||
published_at = release_data.get("published_at", "")
|
||||
|
||||
update_available = is_newer_version(latest_version, APP_VERSION)
|
||||
|
||||
_update_status = {
|
||||
"status": "idle",
|
||||
"progress": 100,
|
||||
"message": "Update available" if update_available else "Up to date",
|
||||
"error": None,
|
||||
}
|
||||
|
||||
return {
|
||||
"update_available": update_available,
|
||||
"current_version": APP_VERSION,
|
||||
"latest_version": latest_version,
|
||||
"release_name": release_name,
|
||||
"release_notes": release_notes,
|
||||
"release_url": release_url,
|
||||
"published_at": published_at,
|
||||
}
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
logger.error(f"Failed to check for updates: {e}")
|
||||
_update_status = {
|
||||
"status": "error",
|
||||
"progress": 0,
|
||||
"message": "Failed to check for updates",
|
||||
"error": str(e),
|
||||
}
|
||||
return {
|
||||
"update_available": False,
|
||||
"current_version": APP_VERSION,
|
||||
"latest_version": None,
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
async def _perform_update():
|
||||
"""Perform the actual update using git pull."""
|
||||
global _update_status
|
||||
|
||||
try:
|
||||
_update_status = {
|
||||
"status": "downloading",
|
||||
"progress": 20,
|
||||
"message": "Pulling latest changes...",
|
||||
"error": None,
|
||||
}
|
||||
|
||||
# Run git pull in the project directory
|
||||
base_dir = settings.base_dir
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"git", "pull", "--rebase",
|
||||
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 pull failed"
|
||||
logger.error(f"Git pull failed: {error_msg}")
|
||||
_update_status = {
|
||||
"status": "error",
|
||||
"progress": 0,
|
||||
"message": "Failed to pull updates",
|
||||
"error": error_msg,
|
||||
}
|
||||
return
|
||||
|
||||
_update_status = {
|
||||
"status": "installing",
|
||||
"progress": 50,
|
||||
"message": "Installing dependencies...",
|
||||
"error": None,
|
||||
}
|
||||
|
||||
# Install Python dependencies
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
sys.executable, "-m", "pip", "install", "-r", "requirements.txt", "-q",
|
||||
cwd=str(base_dir),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
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
|
||||
frontend_dir = base_dir / "frontend"
|
||||
if frontend_dir.exists():
|
||||
# npm install
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"npm", "install",
|
||||
cwd=str(frontend_dir),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
await process.communicate()
|
||||
|
||||
# npm run build
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"npm", "run", "build",
|
||||
cwd=str(frontend_dir),
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
logger.warning(f"Frontend build warning: {stderr.decode() if stderr else 'unknown'}")
|
||||
|
||||
_update_status = {
|
||||
"status": "complete",
|
||||
"progress": 100,
|
||||
"message": "Update complete! Please restart the application.",
|
||||
"error": None,
|
||||
}
|
||||
|
||||
logger.info("Update completed successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Update failed: {e}")
|
||||
_update_status = {
|
||||
"status": "error",
|
||||
"progress": 0,
|
||||
"message": "Update failed",
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/apply")
|
||||
async def apply_update(background_tasks: BackgroundTasks):
|
||||
"""Apply available update (git pull + rebuild)."""
|
||||
global _update_status
|
||||
|
||||
if _update_status["status"] in ["downloading", "installing"]:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Update already in progress",
|
||||
"status": _update_status,
|
||||
}
|
||||
|
||||
# Start update in background
|
||||
background_tasks.add_task(_perform_update)
|
||||
|
||||
_update_status = {
|
||||
"status": "downloading",
|
||||
"progress": 10,
|
||||
"message": "Starting update...",
|
||||
"error": None,
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Update started",
|
||||
"status": _update_status,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
async def get_update_status():
|
||||
"""Get current update status."""
|
||||
return _update_status
|
||||
@@ -1,6 +1,10 @@
|
||||
from pathlib import Path
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
# Application version - single source of truth
|
||||
APP_VERSION = "0.1.4"
|
||||
GITHUB_REPO = "maziggy/bambusy"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "BambuTrack"
|
||||
|
||||
+4
-3
@@ -9,7 +9,7 @@ from logging.handlers import RotatingFileHandler
|
||||
from fastapi import FastAPI
|
||||
|
||||
# Import settings first for logging configuration
|
||||
from backend.app.core.config import settings as app_settings
|
||||
from backend.app.core.config import settings as app_settings, APP_VERSION
|
||||
|
||||
# Configure logging based on settings
|
||||
# DEBUG=true -> DEBUG level, else use LOG_LEVEL setting
|
||||
@@ -54,7 +54,7 @@ from fastapi.responses import FileResponse
|
||||
from backend.app.core.database import init_db, async_session
|
||||
from sqlalchemy import select, or_
|
||||
from backend.app.core.websocket import ws_manager
|
||||
from backend.app.api.routes import printers, archives, websocket, filaments, cloud, smart_plugs, print_queue, kprofiles, notifications, spoolman
|
||||
from backend.app.api.routes import printers, archives, websocket, filaments, cloud, smart_plugs, print_queue, kprofiles, notifications, spoolman, updates
|
||||
from backend.app.api.routes import settings as settings_routes
|
||||
from backend.app.services.notification_service import notification_service
|
||||
from backend.app.services.printer_manager import (
|
||||
@@ -963,7 +963,7 @@ async def lifespan(app: FastAPI):
|
||||
app = FastAPI(
|
||||
title=app_settings.app_name,
|
||||
description="Archive and manage Bambu Lab 3MF files",
|
||||
version="0.1.2",
|
||||
version=APP_VERSION,
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
@@ -978,6 +978,7 @@ app.include_router(print_queue.router, prefix=app_settings.api_prefix)
|
||||
app.include_router(kprofiles.router, prefix=app_settings.api_prefix)
|
||||
app.include_router(notifications.router, prefix=app_settings.api_prefix)
|
||||
app.include_router(spoolman.router, prefix=app_settings.api_prefix)
|
||||
app.include_router(updates.router, prefix=app_settings.api_prefix)
|
||||
app.include_router(websocket.router, prefix=app_settings.api_prefix)
|
||||
|
||||
|
||||
|
||||
@@ -17,6 +17,9 @@ class AppSettings(BaseModel):
|
||||
spoolman_url: str = Field(default="", description="Spoolman server URL (e.g., http://localhost:7912)")
|
||||
spoolman_sync_mode: str = Field(default="auto", description="Sync mode: 'auto' syncs immediately, 'manual' requires button press")
|
||||
|
||||
# Updates
|
||||
check_updates: bool = Field(default=True, description="Automatically check for updates on startup")
|
||||
|
||||
|
||||
class AppSettingsUpdate(BaseModel):
|
||||
"""Schema for updating settings (all fields optional)."""
|
||||
@@ -31,3 +34,4 @@ class AppSettingsUpdate(BaseModel):
|
||||
spoolman_enabled: bool | None = None
|
||||
spoolman_url: str | None = None
|
||||
spoolman_sync_mode: str | None = None
|
||||
check_updates: bool | None = None
|
||||
|
||||
@@ -153,6 +153,7 @@ export interface AppSettings {
|
||||
currency: string;
|
||||
energy_cost_per_kwh: number;
|
||||
energy_tracking_mode: 'print' | 'total';
|
||||
check_updates: boolean;
|
||||
}
|
||||
|
||||
export type AppSettingsUpdate = Partial<AppSettings>;
|
||||
@@ -492,6 +493,31 @@ export interface SpoolmanSyncResult {
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// Update types
|
||||
export interface VersionInfo {
|
||||
version: string;
|
||||
repo: string;
|
||||
}
|
||||
|
||||
export interface UpdateCheckResult {
|
||||
update_available: boolean;
|
||||
current_version: string;
|
||||
latest_version: string | null;
|
||||
release_name?: string;
|
||||
release_notes?: string;
|
||||
release_url?: string;
|
||||
published_at?: string;
|
||||
error?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface UpdateStatus {
|
||||
status: 'idle' | 'checking' | 'downloading' | 'installing' | 'complete' | 'error';
|
||||
progress: number;
|
||||
message: string;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
// API functions
|
||||
export const api = {
|
||||
// Printers
|
||||
@@ -906,4 +932,13 @@ export const api = {
|
||||
request<{ spools: unknown[] }>('/spoolman/spools'),
|
||||
getSpoolmanFilaments: () =>
|
||||
request<{ filaments: unknown[] }>('/spoolman/filaments'),
|
||||
|
||||
// Updates
|
||||
getVersion: () => request<VersionInfo>('/updates/version'),
|
||||
checkForUpdates: () => request<UpdateCheckResult>('/updates/check'),
|
||||
applyUpdate: () =>
|
||||
request<{ success: boolean; message: string; status: UpdateStatus }>('/updates/apply', {
|
||||
method: 'POST',
|
||||
}),
|
||||
getUpdateStatus: () => request<UpdateStatus>('/updates/status'),
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
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, type LucideIcon } from 'lucide-react';
|
||||
import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, type LucideIcon } from 'lucide-react';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { api } from '../api/client';
|
||||
|
||||
interface NavItem {
|
||||
id: string;
|
||||
@@ -76,6 +78,27 @@ export function Layout() {
|
||||
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
||||
const hasRedirected = useRef(false);
|
||||
|
||||
// Check for updates
|
||||
const { data: versionInfo } = useQuery({
|
||||
queryKey: ['version'],
|
||||
queryFn: api.getVersion,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const { data: settings } = useQuery({
|
||||
queryKey: ['settings'],
|
||||
queryFn: api.getSettings,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes
|
||||
});
|
||||
|
||||
const { data: updateCheck } = useQuery({
|
||||
queryKey: ['updateCheck'],
|
||||
queryFn: api.checkForUpdates,
|
||||
enabled: settings?.check_updates !== false,
|
||||
staleTime: 60 * 60 * 1000, // 1 hour
|
||||
refetchInterval: 60 * 60 * 1000, // Check every hour
|
||||
});
|
||||
|
||||
// Redirect to default view on initial load
|
||||
useEffect(() => {
|
||||
if (!hasRedirected.current && location.pathname === '/') {
|
||||
@@ -239,7 +262,19 @@ export function Layout() {
|
||||
<div className="p-2 border-t border-bambu-dark-tertiary">
|
||||
{sidebarExpanded ? (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<span className="text-sm text-bambu-gray">v0.1.4</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-bambu-gray">v{versionInfo?.version || '...'}</span>
|
||||
{updateCheck?.update_available && (
|
||||
<button
|
||||
onClick={() => navigate('/settings')}
|
||||
className="flex items-center gap-1 text-xs text-bambu-green hover:text-bambu-green/80 transition-colors"
|
||||
title={`Update available: v${updateCheck.latest_version}`}
|
||||
>
|
||||
<ArrowUpCircle className="w-4 h-4" />
|
||||
<span>Update</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<a
|
||||
href="https://github.com/maziggy/bambusy"
|
||||
@@ -268,6 +303,15 @@ export function Layout() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center gap-1">
|
||||
{updateCheck?.update_available && (
|
||||
<button
|
||||
onClick={() => navigate('/settings')}
|
||||
className="p-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors text-bambu-green hover:text-bambu-green/80"
|
||||
title={`Update available: v${updateCheck.latest_version}`}
|
||||
>
|
||||
<ArrowUpCircle className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href="https://github.com/maziggy/bambusy"
|
||||
target="_blank"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Save, Loader2, Check, Plus, Plug, AlertTriangle, RotateCcw, Bell } from 'lucide-react';
|
||||
import { Save, Loader2, Check, Plus, Plug, AlertTriangle, RotateCcw, Bell, Download, RefreshCw, ExternalLink } from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import type { AppSettings, SmartPlug, NotificationProvider } from '../api/client';
|
||||
import type { AppSettings, SmartPlug, NotificationProvider, UpdateStatus } from '../api/client';
|
||||
import { Card, CardContent, CardHeader } from '../components/Card';
|
||||
import { Button } from '../components/Button';
|
||||
import { SmartPlugCard } from '../components/SmartPlugCard';
|
||||
@@ -53,6 +53,37 @@ export function SettingsPage() {
|
||||
queryFn: api.checkFfmpeg,
|
||||
});
|
||||
|
||||
const { data: versionInfo } = useQuery({
|
||||
queryKey: ['version'],
|
||||
queryFn: api.getVersion,
|
||||
});
|
||||
|
||||
const { data: updateCheck, refetch: refetchUpdateCheck, isRefetching: isCheckingUpdate } = useQuery({
|
||||
queryKey: ['updateCheck'],
|
||||
queryFn: api.checkForUpdates,
|
||||
staleTime: 5 * 60 * 1000,
|
||||
});
|
||||
|
||||
const { data: updateStatus, refetch: refetchUpdateStatus } = useQuery({
|
||||
queryKey: ['updateStatus'],
|
||||
queryFn: api.getUpdateStatus,
|
||||
refetchInterval: (query) => {
|
||||
const status = query.state.data as UpdateStatus | undefined;
|
||||
// Poll while update is in progress
|
||||
if (status?.status === 'downloading' || status?.status === 'installing') {
|
||||
return 1000;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
const applyUpdateMutation = useMutation({
|
||||
mutationFn: api.applyUpdate,
|
||||
onSuccess: () => {
|
||||
refetchUpdateStatus();
|
||||
},
|
||||
});
|
||||
|
||||
// Sync local state when settings load
|
||||
useEffect(() => {
|
||||
if (settings && !localSettings) {
|
||||
@@ -70,7 +101,8 @@ export function SettingsPage() {
|
||||
settings.default_filament_cost !== localSettings.default_filament_cost ||
|
||||
settings.currency !== localSettings.currency ||
|
||||
settings.energy_cost_per_kwh !== localSettings.energy_cost_per_kwh ||
|
||||
settings.energy_tracking_mode !== localSettings.energy_tracking_mode;
|
||||
settings.energy_tracking_mode !== localSettings.energy_tracking_mode ||
|
||||
settings.check_updates !== localSettings.check_updates;
|
||||
setHasChanges(changed);
|
||||
}
|
||||
}, [settings, localSettings]);
|
||||
@@ -326,29 +358,137 @@ export function SettingsPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Second Column - Spoolman & Updates */}
|
||||
<div className="space-y-6 flex-1 max-w-md">
|
||||
<SpoolmanSettings />
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<h2 className="text-lg font-semibold text-white">About</h2>
|
||||
<h2 className="text-lg font-semibold text-white">Updates</h2>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2 text-sm">
|
||||
<p className="text-white">Bambusy v0.1.2</p>
|
||||
<p className="text-bambu-gray">
|
||||
Archive and manage your Bambu Lab 3MF files
|
||||
</p>
|
||||
<p className="text-bambu-gray">
|
||||
Connect to printers via LAN mode (developer mode required)
|
||||
</p>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-white">Check for updates</p>
|
||||
<p className="text-sm text-bambu-gray">
|
||||
Automatically check for new versions on startup
|
||||
</p>
|
||||
</div>
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={localSettings.check_updates}
|
||||
onChange={(e) => updateSetting('check_updates', e.target.checked)}
|
||||
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>
|
||||
<p className="text-white">Current version</p>
|
||||
<p className="text-sm text-bambu-gray">v{versionInfo?.version || '...'}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => refetchUpdateCheck()}
|
||||
disabled={isCheckingUpdate}
|
||||
>
|
||||
{isCheckingUpdate ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="w-4 h-4" />
|
||||
)}
|
||||
Check now
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{updateCheck?.update_available ? (
|
||||
<div className="mt-4 p-3 bg-bambu-green/10 border border-bambu-green/30 rounded-lg">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<p className="text-bambu-green font-medium">
|
||||
Update available: v{updateCheck.latest_version}
|
||||
</p>
|
||||
{updateCheck.release_name && updateCheck.release_name !== updateCheck.latest_version && (
|
||||
<p className="text-sm text-bambu-gray mt-1">{updateCheck.release_name}</p>
|
||||
)}
|
||||
{updateCheck.release_notes && (
|
||||
<p className="text-sm text-bambu-gray mt-2 whitespace-pre-line line-clamp-3">
|
||||
{updateCheck.release_notes}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{updateCheck.release_url && (
|
||||
<a
|
||||
href={updateCheck.release_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-bambu-gray hover:text-white transition-colors"
|
||||
title="View release on GitHub"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{updateStatus?.status === 'downloading' || updateStatus?.status === 'installing' ? (
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center gap-2 text-sm text-bambu-gray">
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
<span>{updateStatus.message}</span>
|
||||
</div>
|
||||
<div className="mt-2 w-full bg-bambu-dark-tertiary rounded-full h-2">
|
||||
<div
|
||||
className="bg-bambu-green h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${updateStatus.progress}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : updateStatus?.status === 'complete' ? (
|
||||
<div className="mt-3 p-2 bg-bambu-green/20 rounded text-sm text-bambu-green">
|
||||
{updateStatus.message}
|
||||
</div>
|
||||
) : updateStatus?.status === 'error' ? (
|
||||
<div className="mt-3 p-2 bg-red-500/20 rounded text-sm text-red-400">
|
||||
{updateStatus.error || updateStatus.message}
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
className="mt-3"
|
||||
onClick={() => applyUpdateMutation.mutate()}
|
||||
disabled={applyUpdateMutation.isPending}
|
||||
>
|
||||
{applyUpdateMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Download className="w-4 h-4" />
|
||||
)}
|
||||
Install Update
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : updateCheck?.error ? (
|
||||
<div className="mt-2 p-2 bg-red-500/10 border border-red-500/30 rounded text-sm text-red-400">
|
||||
Failed to check for updates: {updateCheck.error}
|
||||
</div>
|
||||
) : updateCheck && !updateCheck.update_available ? (
|
||||
<p className="mt-2 text-sm text-bambu-gray">
|
||||
You're running the latest version
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Middle Column - Smart Plugs */}
|
||||
<div className="w-96 flex-shrink-0">
|
||||
{/* Third Column - Smart Plugs */}
|
||||
<div className="w-80 flex-shrink-0">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
@@ -400,8 +540,8 @@ export function SettingsPage() {
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right Column - Notifications */}
|
||||
<div className="w-96 flex-shrink-0">
|
||||
{/* Fourth Column - Notifications */}
|
||||
<div className="w-80 flex-shrink-0">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2
-2
@@ -7,8 +7,8 @@
|
||||
<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-zNwYBAHC.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CfyzT_2B.css">
|
||||
<script type="module" crossorigin src="/assets/index-DFEXODPu.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CLNqfrMK.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Reference in New Issue
Block a user