import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
Plus,
Wifi,
WifiOff,
Thermometer,
Clock,
MoreVertical,
Trash2,
RefreshCw,
Box,
HardDrive,
AlertTriangle,
Terminal,
Power,
PowerOff,
Zap,
} from 'lucide-react';
import { api } from '../api/client';
import type { Printer, PrinterCreate } from '../api/client';
import { Card, CardContent } from '../components/Card';
import { Button } from '../components/Button';
import { ConfirmModal } from '../components/ConfirmModal';
import { FileManagerModal } from '../components/FileManagerModal';
import { MQTTDebugModal } from '../components/MQTTDebugModal';
import { HMSErrorModal } from '../components/HMSErrorModal';
function formatTime(seconds: number): string {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
}
function CoverImage({ url, printName }: { url: string | null; printName?: string }) {
const [loaded, setLoaded] = useState(false);
const [error, setError] = useState(false);
const [showOverlay, setShowOverlay] = useState(false);
return (
<>
url && loaded && setShowOverlay(true)}
>
{url && !error ? (
<>

setLoaded(true)}
onError={() => setError(true)}
/>
{!loaded &&
}
>
) : (
)}
{/* Cover Image Overlay */}
{showOverlay && url && (
setShowOverlay(false)}
>

{printName && (
{printName}
)}
)}
>
);
}
function PrinterCard({ printer, hideIfDisconnected }: { printer: Printer; hideIfDisconnected?: boolean }) {
const queryClient = useQueryClient();
const [showMenu, setShowMenu] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [showFileManager, setShowFileManager] = useState(false);
const [showMQTTDebug, setShowMQTTDebug] = useState(false);
const [showPowerOnConfirm, setShowPowerOnConfirm] = useState(false);
const [showPowerOffConfirm, setShowPowerOffConfirm] = useState(false);
const [showHMSModal, setShowHMSModal] = useState(false);
const { data: status } = useQuery({
queryKey: ['printerStatus', printer.id],
queryFn: () => api.getPrinterStatus(printer.id),
refetchInterval: 30000, // Fallback polling, WebSocket handles real-time
});
// Fetch smart plug for this printer
const { data: smartPlug } = useQuery({
queryKey: ['smartPlugByPrinter', printer.id],
queryFn: () => api.getSmartPlugByPrinter(printer.id),
});
// Fetch smart plug status if plug exists
const { data: plugStatus } = useQuery({
queryKey: ['smartPlugStatus', smartPlug?.id],
queryFn: () => smartPlug ? api.getSmartPlugStatus(smartPlug.id) : null,
enabled: !!smartPlug,
refetchInterval: 30000,
});
// Determine if this card should be hidden
const shouldHide = hideIfDisconnected && status && !status.connected;
const deleteMutation = useMutation({
mutationFn: () => api.deletePrinter(printer.id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['printers'] });
},
});
const connectMutation = useMutation({
mutationFn: () => api.connectPrinter(printer.id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['printerStatus', printer.id] });
},
});
// Smart plug control mutations
const powerControlMutation = useMutation({
mutationFn: (action: 'on' | 'off') =>
smartPlug ? api.controlSmartPlug(smartPlug.id, action) : Promise.reject('No plug'),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smartPlugStatus', smartPlug?.id] });
},
});
const toggleAutoOffMutation = useMutation({
mutationFn: (enabled: boolean) =>
smartPlug ? api.updateSmartPlug(smartPlug.id, { auto_off: enabled }) : Promise.reject('No plug'),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['smartPlugByPrinter', printer.id] });
},
});
if (shouldHide) {
return null;
}
return (
{/* Header */}
{printer.name}
{printer.model || 'Unknown Model'}
{status?.connected ? (
) : (
)}
{status?.connected ? 'Connected' : 'Offline'}
{/* HMS Status Indicator */}
{status?.connected && (
)}
{showMenu && (
)}
{/* Delete Confirmation */}
{showDeleteConfirm && (
{
deleteMutation.mutate();
setShowDeleteConfirm(false);
}}
onCancel={() => setShowDeleteConfirm(false)}
/>
)}
{/* Status */}
{status?.connected && (
<>
{/* Current Print or Idle Placeholder */}
{/* Cover Image */}
{/* Print Info */}
{status.current_print && status.state === 'RUNNING' ? (
<>
Printing
{status.subtask_name || status.current_print}
{Math.round(status.progress || 0)}%
{status.remaining_time != null && status.remaining_time > 0 && (
{formatTime(status.remaining_time * 60)}
)}
{status.layer_num != null && status.total_layers != null && status.total_layers > 0 && (
Layer {status.layer_num}/{status.total_layers}
)}
>
) : (
<>
Status
{status.state?.toLowerCase() || 'Idle'}
Ready to print
>
)}
{/* Temperatures */}
{status.temperatures && (
Nozzle
{Math.round(status.temperatures.nozzle || 0)}°C
Bed
{Math.round(status.temperatures.bed || 0)}°C
{status.temperatures.chamber !== undefined && (
Chamber
{Math.round(status.temperatures.chamber || 0)}°C
)}
)}
>
)}
{/* Smart Plug Controls */}
{smartPlug && (
{/* Plug name and status */}
{smartPlug.name}
{plugStatus && (
{plugStatus.state || '?'}
)}
{/* Spacer */}
{/* Power buttons */}
{/* Auto-off toggle */}
Auto-off
)}
{/* Connection Info & Actions */}
{printer.ip_address}
{printer.serial_number}
{/* File Manager Modal */}
{showFileManager && (
setShowFileManager(false)}
/>
)}
{/* MQTT Debug Modal */}
{showMQTTDebug && (
setShowMQTTDebug(false)}
/>
)}
{/* Power On Confirmation */}
{showPowerOnConfirm && smartPlug && (
{
powerControlMutation.mutate('on');
setShowPowerOnConfirm(false);
}}
onCancel={() => setShowPowerOnConfirm(false)}
/>
)}
{/* Power Off Confirmation */}
{showPowerOffConfirm && smartPlug && (
{
powerControlMutation.mutate('off');
setShowPowerOffConfirm(false);
}}
onCancel={() => setShowPowerOffConfirm(false)}
/>
)}
{/* HMS Error Modal */}
{showHMSModal && (
setShowHMSModal(false)}
/>
)}
);
}
function AddPrinterModal({
onClose,
onAdd,
}: {
onClose: () => void;
onAdd: (data: PrinterCreate) => void;
}) {
const [form, setForm] = useState({
name: '',
serial_number: '',
ip_address: '',
access_code: '',
model: '',
auto_archive: true,
});
// Close on Escape key
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', handleKeyDown);
return () => window.removeEventListener('keydown', handleKeyDown);
}, [onClose]);
return (
e.stopPropagation()}>
Add Printer
);
}
export function PrintersPage() {
const [showAddModal, setShowAddModal] = useState(false);
const [hideDisconnected, setHideDisconnected] = useState(() => {
return localStorage.getItem('hideDisconnectedPrinters') === 'true';
});
const queryClient = useQueryClient();
const { data: printers, isLoading } = useQuery({
queryKey: ['printers'],
queryFn: api.getPrinters,
});
const addMutation = useMutation({
mutationFn: api.createPrinter,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['printers'] });
setShowAddModal(false);
},
});
const toggleHideDisconnected = () => {
const newValue = !hideDisconnected;
setHideDisconnected(newValue);
localStorage.setItem('hideDisconnectedPrinters', String(newValue));
};
return (
Printers
Manage your Bambu Lab printers
{isLoading ? (
Loading printers...
) : printers?.length === 0 ? (
No printers configured yet
) : (
{printers?.map((printer) => (
))}
)}
{showAddModal && (
setShowAddModal(false)}
onAdd={(data) => addMutation.mutate(data)}
/>
)}
);
}