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:
maziggy
2026-01-19 13:29:37 +01:00
committed by MartinNYHC
parent af0dad11ec
commit b2831d09a2
12 changed files with 36 additions and 14 deletions
+12
View File
@@ -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
+1 -1
View File
@@ -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))
+1
View File
@@ -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
View File
@@ -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():
+2 -1
View File
@@ -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;
+2 -2
View File
@@ -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(() => {
+4 -4
View File
@@ -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 = (() => {
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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>) => {
+5 -2
View File
@@ -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();