mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
Frontend ESLint Warnings (8 fixed)
┌─────────────────────────┬───────────────────────────────────────────────────────────┐ │ File │ Fix │ ├─────────────────────────┼───────────────────────────────────────────────────────────┤ │ CameraPage.tsx:75 │ Copy imgRef.current to a variable before cleanup │ ├─────────────────────────┼───────────────────────────────────────────────────────────┤ │ useWebSocket.ts:121 │ Add processMessageQueue to useCallback dependencies │ ├─────────────────────────┼───────────────────────────────────────────────────────────┤ │ SpoolmanSettings.tsx:89 │ Add eslint-disable comment (intentional debounce pattern) │ ├─────────────────────────┼───────────────────────────────────────────────────────────┤ │ MQTTDebugModal.tsx:95 │ Wrap logs in useMemo to prevent recreating on each render │ ├─────────────────────────┼───────────────────────────────────────────────────────────┤ │ Layout.tsx:166 │ Wrap navItemsMap and extLinksMap in useMemo │ ├─────────────────────────┼───────────────────────────────────────────────────────────┤ │ KProfilesView.tsx:715 │ Wrap getProfileKey in useCallback │ ├─────────────────────────┼───────────────────────────────────────────────────────────┤ │ GcodeViewer.tsx:171 │ Add eslint-disable comment (intentional behavior) │ └─────────────────────────┴───────────────────────────────────────────────────────────┘ Docker Integration Test Fix The /api/v1/settings endpoint was returning 404 because: 1. The route was defined at /settings/ (with trailing slash) 2. Curl without -L doesn't follow redirects 3. The catch-all route was intercepting API paths Fixes: - Added routes for both with and without trailing slash in settings.py and notification_templates.py - Updated catch-all in main.py to raise proper HTTPException for API routes Test Results - Frontend lint: 0 errors, 0 warnings - TypeScript: No errors - Backend unit tests: 264 passed - Backend integration tests: 309 passed
This commit is contained in:
@@ -5,6 +5,11 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **Multi-plate 3MF plate selection** - When reprinting multi-plate 3MF files (exported with "All sliced file"), users can now select which plate to print:
|
||||
- Plate selection grid with thumbnails, names, and print times
|
||||
- Filament requirements filtered to show only selected plate's filaments
|
||||
- Prevents incorrect filament mapping across plates
|
||||
- Closes [#93](https://github.com/maziggy/bambuddy/issues/93)
|
||||
- **Home Assistant smart plug integration** - Control any Home Assistant switch/light entity as a smart plug:
|
||||
- Configure HA connection (URL + Long-Lived Access Token) in Settings → Network
|
||||
- Add HA-controlled plugs via Settings → Plugs → Add Smart Plug → Home Assistant tab
|
||||
@@ -19,6 +24,13 @@ All notable changes to Bambuddy will be documented in this file.
|
||||
- F3D files included in backup/restore
|
||||
- API tests for F3D endpoints
|
||||
|
||||
### Fixed
|
||||
- **Multi-plate 3MF metadata extraction** - Single-plate exports from multi-plate projects now show correct thumbnail and name:
|
||||
- Extracts plate index from slice_info.config metadata
|
||||
- Uses correct plate thumbnail (e.g., plate_5.png instead of plate_1.png)
|
||||
- Appends "Plate N" to print name for plates > 1
|
||||
- Closes [#92](https://github.com/maziggy/bambuddy/issues/92)
|
||||
|
||||
## [0.1.6b8] - 2026-01-17
|
||||
|
||||
### Added
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
- Duplicate detection & full-text search
|
||||
- Photo attachments & failure analysis
|
||||
- Timelapse editor (trim, speed, music)
|
||||
- Re-print to any connected printer with AMS mapping (auto-match or manual slot selection)
|
||||
- Re-print to any connected printer with AMS mapping (auto-match or manual slot selection, multi-plate support)
|
||||
- Archive comparison (side-by-side diff)
|
||||
|
||||
### 📊 Monitoring & Control
|
||||
|
||||
@@ -36,6 +36,7 @@ EVENT_NAMES = {
|
||||
|
||||
|
||||
@router.get("", response_model=list[NotificationTemplateResponse])
|
||||
@router.get("/", response_model=list[NotificationTemplateResponse])
|
||||
async def get_templates(db: AsyncSession = Depends(get_db)):
|
||||
"""Get all notification templates."""
|
||||
result = await db.execute(select(NotificationTemplate).order_by(NotificationTemplate.id))
|
||||
|
||||
@@ -53,6 +53,7 @@ async def set_setting(db: AsyncSession, key: str, value: str) -> None:
|
||||
await db.execute(stmt)
|
||||
|
||||
|
||||
@router.get("", response_model=AppSettings)
|
||||
@router.get("/", response_model=AppSettings)
|
||||
async def get_settings(db: AsyncSession = Depends(get_db)):
|
||||
"""Get all application settings."""
|
||||
|
||||
+4
-2
@@ -2036,9 +2036,11 @@ async def serve_service_worker():
|
||||
@app.get("/{full_path:path}")
|
||||
async def serve_spa(full_path: str):
|
||||
"""Serve React app for client-side routing."""
|
||||
# Don't intercept API routes
|
||||
# Don't intercept API routes - raise proper 404 so FastAPI can handle redirects
|
||||
if full_path.startswith("api/"):
|
||||
return {"error": "Not found"}
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
index_file = app_settings.static_dir / "index.html"
|
||||
if index_file.exists():
|
||||
|
||||
@@ -168,7 +168,8 @@ export function GcodeViewer({
|
||||
}
|
||||
initRef.current = false;
|
||||
};
|
||||
}, [gcodeUrl, colorsKey]); // Use colorsKey instead of filamentColors
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [gcodeUrl, colorsKey]); // Intentionally use colorsKey instead of filamentColors, buildVolume rarely changes
|
||||
|
||||
const handleLayerChange = useCallback((layer: number) => {
|
||||
if (!previewRef.current) return;
|
||||
|
||||
@@ -711,8 +711,8 @@ export function KProfilesView() {
|
||||
const [showBulkDeleteConfirm, setShowBulkDeleteConfirm] = useState(false);
|
||||
const [bulkDeleteInProgress, setBulkDeleteInProgress] = useState(false);
|
||||
|
||||
// Helper to create unique profile key for selection
|
||||
const getProfileKey = (profile: KProfile) => `${profile.slot_id}_${profile.extruder_id}`;
|
||||
// Helper to create unique profile key for selection - wrapped in useCallback to prevent re-renders
|
||||
const getProfileKey = useCallback((profile: KProfile) => `${profile.slot_id}_${profile.extruder_id}`, []);
|
||||
|
||||
// Save nozzle diameter to localStorage when it changes
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, useMemo } 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, FolderKanban, X, Menu, Info, Plug, Bug, type LucideIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -162,9 +162,9 @@ export function Layout() {
|
||||
return () => clearInterval(interval);
|
||||
}, [debugLoggingState?.enabled, debugLoggingState?.enabled_at]);
|
||||
|
||||
// Build the unified sidebar items list
|
||||
const navItemsMap = new Map(defaultNavItems.map(item => [item.id, item]));
|
||||
const extLinksMap = new Map((externalLinks || []).map(link => [`ext-${link.id}`, link]));
|
||||
// Build the unified sidebar items list - memoized to prevent re-renders
|
||||
const navItemsMap = useMemo(() => new Map(defaultNavItems.map(item => [item.id, item])), []);
|
||||
const extLinksMap = useMemo(() => new Map((externalLinks || []).map(link => [`ext-${link.id}`, link])), [externalLinks]);
|
||||
|
||||
// Compute the ordered sidebar: include stored order + any new items
|
||||
const orderedSidebarIds = (() => {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function MQTTDebugModal({ printerId, printerName, onClose }: MQTTDebugMod
|
||||
};
|
||||
|
||||
const loggingEnabled = data?.logging_enabled ?? false;
|
||||
const logs = data?.logs ?? [];
|
||||
const logs = useMemo(() => data?.logs ?? [], [data?.logs]);
|
||||
|
||||
// Filter logs based on search query and direction filter
|
||||
const filteredLogs = useMemo(() => {
|
||||
|
||||
@@ -72,6 +72,7 @@ export function SpoolmanSettings() {
|
||||
}, [settings]);
|
||||
|
||||
// Auto-save when settings change (after initial load)
|
||||
// Intentionally omit saveMutation and settings from deps to avoid infinite loops
|
||||
useEffect(() => {
|
||||
if (!isInitialized || !settings) return;
|
||||
|
||||
@@ -86,6 +87,7 @@ export function SpoolmanSettings() {
|
||||
}, 500);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [localEnabled, localUrl, localSyncMode, isInitialized]);
|
||||
|
||||
// Save mutation
|
||||
|
||||
@@ -118,7 +118,7 @@ export function useWebSocket() {
|
||||
};
|
||||
|
||||
wsRef.current = ws;
|
||||
}, []);
|
||||
}, [processMessageQueue]);
|
||||
|
||||
// Throttled printer status update - coalesces rapid updates per printer
|
||||
const throttledPrinterStatusUpdate = useCallback((printerId: number, data: Record<string, unknown>) => {
|
||||
|
||||
@@ -67,12 +67,15 @@ export function CameraPage() {
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
// Store ref value for cleanup - ref may change by cleanup time
|
||||
const imgElement = imgRef.current;
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
|
||||
// Clear the image source first to stop the stream
|
||||
if (imgRef.current) {
|
||||
imgRef.current.src = '';
|
||||
if (imgElement) {
|
||||
imgElement.src = '';
|
||||
}
|
||||
// Send stop signal only once
|
||||
sendStopOnce();
|
||||
|
||||
Reference in New Issue
Block a user