mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 21:47:39 +02:00
[Feature] Spoolbuddy Fixes and Improvements (#787)
[Feature] Spoolbuddy Fixes and Improvements (#787)
This commit is contained in:
@@ -24,9 +24,17 @@ interface SpoolFormModalProps {
|
||||
spool?: InventorySpool | null;
|
||||
printersWithCalibrations?: PrinterWithCalibrations[];
|
||||
currencySymbol: string;
|
||||
onSpoolsCreated?: (spools: InventorySpool[]) => void;
|
||||
}
|
||||
|
||||
export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibrations = [], currencySymbol }: SpoolFormModalProps) {
|
||||
export function SpoolFormModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
spool,
|
||||
printersWithCalibrations = [],
|
||||
currencySymbol,
|
||||
onSpoolsCreated,
|
||||
}: SpoolFormModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const { showToast } = useToast();
|
||||
@@ -317,6 +325,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
await saveKProfiles(newSpool.id);
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
|
||||
if (onSpoolsCreated) onSpoolsCreated([newSpool]);
|
||||
showToast(t('inventory.spoolCreated'), 'success');
|
||||
onClose();
|
||||
},
|
||||
@@ -335,6 +344,7 @@ export function SpoolFormModal({ isOpen, onClose, spool, printersWithCalibration
|
||||
}
|
||||
}
|
||||
await queryClient.invalidateQueries({ queryKey: ['inventory-spools'] });
|
||||
if (onSpoolsCreated) onSpoolsCreated(newSpools);
|
||||
showToast(t('inventory.spoolsCreated', { count: newSpools.length }), 'success');
|
||||
onClose();
|
||||
},
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { X, Play, RotateCw } from 'lucide-react';
|
||||
import { spoolbuddyApi } from '../../api/client';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface DiagnosticModalProps {
|
||||
type: 'scale' | 'nfc' | 'read_tag';
|
||||
deviceId: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function DiagnosticModal({ type, deviceId, onClose }: DiagnosticModalProps) {
|
||||
const { t } = useTranslation();
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [output, setOutput] = useState<string>('');
|
||||
const [error, setError] = useState<string>('');
|
||||
const [hasRun, setHasRun] = useState(false);
|
||||
|
||||
// Close on Escape
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && !isRunning) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [isRunning, onClose]);
|
||||
|
||||
const runDiagnostic = useCallback(async () => {
|
||||
setIsRunning(true);
|
||||
setOutput('');
|
||||
setError('');
|
||||
setHasRun(true);
|
||||
|
||||
try {
|
||||
// Step 1: Queue the diagnostic on the device
|
||||
setOutput(t('spoolbuddy.diagnostic.queuing', 'Queuing diagnostic on device...\n'));
|
||||
await spoolbuddyApi.queueDiagnostics(deviceId, type);
|
||||
|
||||
// Step 2: Poll for results with timeout
|
||||
let result = null;
|
||||
const maxRetries = 60; // 30s timeout with 500ms polling
|
||||
let retryCount = 0;
|
||||
|
||||
while (retryCount < maxRetries && !result) {
|
||||
// Wait a bit before polling
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
try {
|
||||
result = await spoolbuddyApi.getDiagnosticResult(deviceId, type);
|
||||
break;
|
||||
} catch (e) {
|
||||
// Not ready yet, continue polling
|
||||
retryCount++;
|
||||
if (retryCount % 4 === 0) {
|
||||
// Update every 2 seconds (after 4 retries of 500ms)
|
||||
setOutput(prev => prev + '.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new Error('Diagnostic timed out - device did not report results');
|
||||
}
|
||||
|
||||
setOutput(result.output);
|
||||
if (!result.success) {
|
||||
setError(`Exit code: ${result.exit_code}`);
|
||||
}
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Unknown error');
|
||||
setOutput('');
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
}, [type, deviceId]);
|
||||
|
||||
const title = type === 'scale'
|
||||
? t('spoolbuddy.diagnostic.scaleTitle', 'Scale Diagnostic')
|
||||
: type === 'read_tag'
|
||||
? t('spoolbuddy.diagnostic.readTagTitle', 'Read Tag Diagnostic')
|
||||
: t('spoolbuddy.diagnostic.nfcTitle', 'NFC Reader Diagnostic');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 animate-fade-in"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-zinc-800 rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[80vh] flex flex-col animate-slide-up"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center p-4 border-b border-zinc-700">
|
||||
<h2 className="text-lg font-semibold text-white">{title}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-zinc-400 hover:text-white transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-4 bg-black/50 font-mono text-sm">
|
||||
{isRunning ? (
|
||||
<div className="flex items-center gap-2 text-green-400">
|
||||
<div className="animate-spin w-4 h-4 border-2 border-green-400 border-t-transparent rounded-full" />
|
||||
<span>{t('spoolbuddy.diagnostic.running', 'Running diagnostic on device...')}</span>
|
||||
</div>
|
||||
) : output ? (
|
||||
<>
|
||||
<div className="text-green-400 whitespace-pre-wrap break-words">
|
||||
{output}
|
||||
</div>
|
||||
{error && (
|
||||
<div className="text-red-400 mt-2">
|
||||
❌ {error}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : hasRun ? (
|
||||
<div>
|
||||
{error ? (
|
||||
<div className="text-red-400">ERROR: {error}</div>
|
||||
) : (
|
||||
<span className="text-green-400">{t('spoolbuddy.diagnostic.completed', 'Diagnostic completed successfully.')}</span>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-zinc-500">
|
||||
{t('spoolbuddy.diagnostic.clickStart', 'Click "Run Diagnostic" to start the hardware diagnostic on')} {deviceId}.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex gap-2 p-4 border-t border-zinc-700 bg-zinc-800">
|
||||
<button
|
||||
onClick={runDiagnostic}
|
||||
disabled={isRunning}
|
||||
className="flex-1 flex items-center justify-center gap-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-600 disabled:cursor-not-allowed px-4 py-2 rounded font-semibold text-white transition-colors"
|
||||
>
|
||||
{isRunning ? (
|
||||
<>
|
||||
<div className="animate-spin w-4 h-4 border-2 border-white border-t-transparent rounded-full" />
|
||||
{t('spoolbuddy.diagnostic.runningBtn', 'Running...')}
|
||||
</>
|
||||
) : hasRun ? (
|
||||
<>
|
||||
<RotateCw size={16} />
|
||||
{t('spoolbuddy.diagnostic.runAgain', 'Run Again')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Play size={16} />
|
||||
{t('spoolbuddy.diagnostic.runBtn', 'Run Diagnostic')}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded bg-zinc-700 hover:bg-zinc-600 text-white font-semibold transition-colors"
|
||||
>
|
||||
{t('spoolbuddy.diagnostic.close', 'Close')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -37,6 +37,11 @@ export function SpoolBuddyLayout() {
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
const device = devices[0];
|
||||
const effectiveDeviceOnline = sbState.deviceOnline || Boolean(device?.online);
|
||||
const sbStateForUi = useMemo(
|
||||
() => ({ ...sbState, deviceOnline: effectiveDeviceOnline }),
|
||||
[sbState, effectiveDeviceOnline]
|
||||
);
|
||||
|
||||
// Sync display settings from device on initial load
|
||||
const initializedRef = useRef(false);
|
||||
@@ -69,14 +74,14 @@ export function SpoolBuddyLayout() {
|
||||
|
||||
// Update alert based on device state and available updates
|
||||
useEffect(() => {
|
||||
if (!sbState.deviceOnline) {
|
||||
if (!effectiveDeviceOnline) {
|
||||
setAlert({ type: 'warning', message: 'SpoolBuddy device disconnected' });
|
||||
} else if (updateCheck?.update_available && updateCheck.latest_version) {
|
||||
setAlert({ type: 'info', message: `Update available: v${updateCheck.latest_version}` });
|
||||
} else {
|
||||
setAlert(null);
|
||||
}
|
||||
}, [sbState.deviceOnline, updateCheck]);
|
||||
}, [effectiveDeviceOnline, updateCheck?.update_available, updateCheck?.latest_version]);
|
||||
|
||||
// Track user activity for screen blank
|
||||
const resetActivity = useCallback(() => {
|
||||
@@ -118,12 +123,12 @@ export function SpoolBuddyLayout() {
|
||||
<SpoolBuddyTopBar
|
||||
selectedPrinterId={selectedPrinterId}
|
||||
onPrinterChange={setSelectedPrinterId}
|
||||
deviceOnline={sbState.deviceOnline}
|
||||
deviceOnline={effectiveDeviceOnline}
|
||||
/>
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
<Outlet context={{
|
||||
selectedPrinterId, setSelectedPrinterId, sbState, setAlert,
|
||||
selectedPrinterId, setSelectedPrinterId, sbState: sbStateForUi, setAlert,
|
||||
displayBrightness, setDisplayBrightness,
|
||||
displayBlankTimeout, setDisplayBlankTimeout,
|
||||
}} />
|
||||
|
||||
Reference in New Issue
Block a user