mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-19 07:33:18 +02:00
- Improved backup/restore module
This commit is contained in:
@@ -1160,6 +1160,7 @@ export const api = {
|
||||
if (categories.filaments !== undefined) params.set('include_filaments', String(categories.filaments));
|
||||
if (categories.maintenance !== undefined) params.set('include_maintenance', String(categories.maintenance));
|
||||
if (categories.archives !== undefined) params.set('include_archives', String(categories.archives));
|
||||
if (categories.access_codes !== undefined) params.set('include_access_codes', String(categories.access_codes));
|
||||
}
|
||||
const url = `${API_BASE}/settings/backup${params.toString() ? '?' + params.toString() : ''}`;
|
||||
const response = await fetch(url);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Download, X, Settings, Bell, FileText, Plug, Printer, Palette, Wrench, Archive, Loader2 } from 'lucide-react';
|
||||
import { Download, X, Settings, Bell, FileText, Plug, Printer, Palette, Wrench, Archive, Loader2, Key, AlertTriangle } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Card, CardContent } from './Card';
|
||||
import { Button } from './Button';
|
||||
import { Toggle } from './Toggle';
|
||||
|
||||
interface BackupCategory {
|
||||
id: string;
|
||||
@@ -94,6 +95,7 @@ export function BackupModal({ onClose, onExport }: BackupModalProps) {
|
||||
});
|
||||
return initial;
|
||||
});
|
||||
const [includeAccessCodes, setIncludeAccessCodes] = useState(false);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
|
||||
// Close on Escape key
|
||||
@@ -130,7 +132,7 @@ export function BackupModal({ onClose, onExport }: BackupModalProps) {
|
||||
const handleExport = async () => {
|
||||
setIsExporting(true);
|
||||
try {
|
||||
await onExport(selected);
|
||||
await onExport({ ...selected, access_codes: includeAccessCodes && selected.printers });
|
||||
} finally {
|
||||
setIsExporting(false);
|
||||
}
|
||||
@@ -221,14 +223,40 @@ export function BackupModal({ onClose, onExport }: BackupModalProps) {
|
||||
<div className="mx-4 mb-2 p-3 rounded-lg bg-yellow-500/10 border border-yellow-500/30">
|
||||
<div className="flex items-start gap-2 text-sm">
|
||||
<Archive className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
|
||||
<div className="text-yellow-200">
|
||||
<div className="text-yellow-200 dark:text-yellow-200 text-yellow-700">
|
||||
<span className="font-medium">ZIP file will be created.</span>
|
||||
<span className="text-yellow-200/70"> Includes all 3MF files, thumbnails, timelapses, and photos. This may take a while and result in a large file.</span>
|
||||
<span className="text-yellow-600 dark:text-yellow-200/70"> Includes all 3MF files, thumbnails, timelapses, and photos. This may take a while and result in a large file.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Access codes option - only shown when printers are selected */}
|
||||
{selected.printers && (
|
||||
<div className="mx-4 mb-2 p-3 rounded-lg bg-bambu-dark border border-bambu-dark-tertiary">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-start gap-2">
|
||||
<Key className="w-4 h-4 text-orange-500 dark:text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">Include Access Codes</p>
|
||||
<p className="text-xs text-bambu-gray">For transferring to another machine</p>
|
||||
</div>
|
||||
</div>
|
||||
<Toggle checked={includeAccessCodes} onChange={setIncludeAccessCodes} />
|
||||
</div>
|
||||
{includeAccessCodes && (
|
||||
<div className="mt-2 p-2 rounded bg-orange-500/10 border border-orange-500/30">
|
||||
<div className="flex items-start gap-2 text-xs">
|
||||
<AlertTriangle className="w-3 h-3 text-orange-500 dark:text-orange-400 mt-0.5 flex-shrink-0" />
|
||||
<span className="text-orange-700 dark:text-orange-200">
|
||||
Access codes will be included in plain text. Keep this backup file secure!
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-bambu-dark-tertiary">
|
||||
<span className="text-sm text-bambu-gray">
|
||||
|
||||
@@ -96,9 +96,14 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4"
|
||||
onClick={state !== 'restoring' ? onClose : undefined}
|
||||
onMouseDown={(e) => {
|
||||
// Only close if clicking directly on the backdrop, not on children
|
||||
if (e.target === e.currentTarget && state !== 'restoring') {
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Card className="w-full max-w-lg" onClick={(e: React.MouseEvent) => e.stopPropagation()}>
|
||||
<Card className="w-full max-w-lg">
|
||||
<CardContent className="p-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary">
|
||||
@@ -155,6 +160,7 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
|
||||
onChange={handleFileSelect}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className={`w-full p-4 border-2 border-dashed rounded-lg transition-colors ${
|
||||
selectedFile
|
||||
@@ -230,10 +236,11 @@ export function RestoreModal({ onClose, onRestore, onSuccess }: RestoreModalProp
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-3 p-4 border-t border-bambu-dark-tertiary">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
<Button type="button" variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleRestore}
|
||||
disabled={!selectedFile}
|
||||
className="bg-bambu-green hover:bg-bambu-green-dark disabled:opacity-50"
|
||||
|
||||
@@ -37,9 +37,9 @@ export function SpoolmanSettings() {
|
||||
const [localEnabled, setLocalEnabled] = useState(false);
|
||||
const [localUrl, setLocalUrl] = useState('');
|
||||
const [localSyncMode, setLocalSyncMode] = useState('auto');
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [showSaved, setShowSaved] = useState(false);
|
||||
const [selectedPrinterId, setSelectedPrinterId] = useState<number | 'all'>('all');
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
// Fetch Spoolman settings
|
||||
const { data: settings, isLoading: settingsLoading } = useQuery({
|
||||
@@ -66,19 +66,26 @@ export function SpoolmanSettings() {
|
||||
setLocalEnabled(settings.spoolman_enabled === 'true');
|
||||
setLocalUrl(settings.spoolman_url || '');
|
||||
setLocalSyncMode(settings.spoolman_sync_mode || 'auto');
|
||||
setIsInitialized(true);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
// Track changes
|
||||
// Auto-save when settings change (after initial load)
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
const changed =
|
||||
(settings.spoolman_enabled === 'true') !== localEnabled ||
|
||||
(settings.spoolman_url || '') !== localUrl ||
|
||||
(settings.spoolman_sync_mode || 'auto') !== localSyncMode;
|
||||
setHasChanges(changed);
|
||||
if (!isInitialized || !settings) return;
|
||||
|
||||
const hasChanges =
|
||||
(settings.spoolman_enabled === 'true') !== localEnabled ||
|
||||
(settings.spoolman_url || '') !== localUrl ||
|
||||
(settings.spoolman_sync_mode || 'auto') !== localSyncMode;
|
||||
|
||||
if (hasChanges) {
|
||||
const timeoutId = setTimeout(() => {
|
||||
saveMutation.mutate();
|
||||
}, 500);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}
|
||||
}, [settings, localEnabled, localUrl, localSyncMode]);
|
||||
}, [localEnabled, localUrl, localSyncMode, isInitialized]);
|
||||
|
||||
// Save mutation
|
||||
const saveMutation = useMutation({
|
||||
@@ -91,7 +98,6 @@ export function SpoolmanSettings() {
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['spoolman-settings'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['spoolman-status'] });
|
||||
setHasChanges(false);
|
||||
setShowSaved(true);
|
||||
setTimeout(() => setShowSaved(false), 2000);
|
||||
},
|
||||
@@ -173,20 +179,11 @@ export function SpoolmanSettings() {
|
||||
<Database className="w-5 h-5 text-bambu-green" />
|
||||
<h2 className="text-lg font-semibold text-white">Spoolman Integration</h2>
|
||||
</div>
|
||||
{hasChanges && (
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => saveMutation.mutate()}
|
||||
disabled={saveMutation.isPending}
|
||||
>
|
||||
{saveMutation.isPending ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : showSaved ? (
|
||||
<Check className="w-4 h-4" />
|
||||
) : (
|
||||
'Save'
|
||||
)}
|
||||
</Button>
|
||||
{saveMutation.isPending && (
|
||||
<Loader2 className="w-4 h-4 text-bambu-green animate-spin" />
|
||||
)}
|
||||
{showSaved && (
|
||||
<Check className="w-4 h-4 text-bambu-green" />
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user