import { useState } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import {
Server,
Database,
HardDrive,
Cpu,
MemoryStick,
Printer,
Archive,
Clock,
CheckCircle2,
XCircle,
Loader2,
RefreshCw,
Plug,
FolderKanban,
Palette,
Bug,
Download,
Headphones,
FolderOpen,
Stethoscope,
HeartPulse,
} from 'lucide-react';
import { api, supportApi, type Printer as PrinterModel } from '../api/client';
import { Card } from '../components/Card';
import { LogViewer } from '../components/LogViewer';
import { ConnectionDiagnosticModal } from '../components/ConnectionDiagnostic';
import { SystemHealthPanel } from '../components/SystemHealthPanel';
import { formatDateTime, type TimeFormat } from '../utils/date';
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
function StatCard({
icon: Icon,
label,
value,
subValue,
color = 'text-bambu-green',
}: {
icon: React.ElementType;
label: string;
value: string | number;
subValue?: string;
color?: string;
}) {
return (
{label}
{value}
{subValue &&
{subValue}
}
);
}
function ProgressBar({ percent, color = 'bg-bambu-green' }: { percent: number; color?: string }) {
return (
);
}
function Section({
title,
icon: Icon,
children,
}: {
title: string;
icon: React.ElementType;
children: React.ReactNode;
}) {
return (
{title}
{children}
);
}
export function SystemInfoPage() {
const { t } = useTranslation();
const queryClient = useQueryClient();
const [bundleError, setBundleError] = useState(null);
const [bundleDownloading, setBundleDownloading] = useState(false);
const [debugToggling, setDebugToggling] = useState(false);
const [diagnosticPrinter, setDiagnosticPrinter] = useState(null);
const { data: systemInfo, isLoading, refetch, isFetching } = useQuery({
queryKey: ['systemInfo'],
queryFn: api.getSystemInfo,
refetchInterval: 30000, // Auto-refresh every 30 seconds
});
const { data: debugLoggingState } = useQuery({
queryKey: ['debugLogging'],
queryFn: supportApi.getDebugLoggingState,
staleTime: 10 * 1000, // 10 seconds
refetchInterval: 10 * 1000,
});
const { data: settings } = useQuery({
queryKey: ['settings'],
queryFn: api.getSettings,
});
const { data: libraryStats } = useQuery({
queryKey: ['library-stats'],
queryFn: api.getLibraryStats,
});
const { data: allPrinters } = useQuery({
queryKey: ['printers'],
queryFn: api.getPrinters,
});
const {
data: systemHealth,
refetch: refetchHealth,
isFetching: healthFetching,
} = useQuery({
queryKey: ['systemHealth'],
queryFn: api.getSystemHealth,
staleTime: 60 * 1000,
});
const timeFormat: TimeFormat = settings?.time_format || 'system';
const handleToggleDebugLogging = async () => {
setDebugToggling(true);
try {
const newState = await supportApi.setDebugLogging(!debugLoggingState?.enabled);
// Immediately update the cache with the new state (includes fresh enabled_at timestamp)
queryClient.setQueryData(['debugLogging'], newState);
} catch (err) {
console.error('Failed to toggle debug logging:', err);
} finally {
setDebugToggling(false);
}
};
const handleDownloadBundle = async () => {
setBundleError(null);
setBundleDownloading(true);
try {
await supportApi.downloadSupportBundle();
} catch (err) {
setBundleError(err instanceof Error ? err.message : 'Failed to download support bundle');
} finally {
setBundleDownloading(false);
}
};
if (isLoading) {
return (
);
}
if (!systemInfo) {
return (
{t('system.failedToLoad', 'Failed to load system information')}
);
}
const diskColor =
systemInfo.storage.disk_percent_used > 90
? 'bg-red-500'
: systemInfo.storage.disk_percent_used > 75
? 'bg-yellow-500'
: 'bg-bambu-green';
const memoryColor =
systemInfo.memory.percent_used > 90
? 'bg-red-500'
: systemInfo.memory.percent_used > 75
? 'bg-yellow-500'
: 'bg-bambu-green';
return (
{/* Header */}
{t('system.title', 'System Information')}
{t('system.subtitle', 'Monitor system resources and database statistics')}
refetch()}
disabled={isFetching}
className="flex items-center gap-2 px-4 py-2 bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary rounded-lg transition-colors disabled:opacity-50"
>
{t('common.refresh', 'Refresh')}
{/* Application Info */}
{/* Support & Troubleshooting */}
{t('support.description', 'Enable debug logging to capture detailed information, then download a support bundle to share when reporting issues.')}
{/* Debug Logging Toggle */}
{t('support.debugLogging', 'Debug Logging')}
{debugLoggingState?.enabled
? t('support.debugLoggingEnabled', 'Capturing detailed logs')
: t('support.debugLoggingDisabled', 'Normal logging level')}
{debugLoggingState?.enabled && debugLoggingState.duration_seconds !== null && (
({Math.floor(debugLoggingState.duration_seconds / 60)}m {debugLoggingState.duration_seconds % 60}s)
)}
{debugToggling && }
{debugLoggingState?.enabled
? t('support.disableDebug', 'Disable')
: t('support.enableDebug', 'Enable')}
{/* Support Bundle Download */}
{t('support.supportBundle', 'Support Bundle')}
{t('support.supportBundleDescription', 'Download system info and logs as a ZIP file')}
{bundleDownloading && }
{bundleDownloading
? t('support.bundleGenerating', 'Generating...')
: t('common.download', 'Download')}
{/* Progress indicator — bundle generation now runs connection +
virtual-printer diagnostics and the log-health scan before
writing the ZIP (#1506 follow-up), so the wait is longer than
a pure file-export. List what's running so it's not opaque. */}
{bundleDownloading && (
{t('support.bundleGenerating', 'Generating...')}
{t('support.bundleStepConnection', 'Running printer connectivity checks')}
{t('support.bundleStepVirtualPrinters', 'Running virtual-printer setup checks')}
{t('support.bundleStepLogScan', 'Scanning recent logs for known issues')}
{t('support.bundleStepBuild', 'Building the support bundle ZIP')}
)}
{/* Error message */}
{bundleError && (
{bundleError}
)}
{/* Instructions */}
{!debugLoggingState?.enabled && (
{t('support.instructions', 'To report an issue:')}
1. {t('support.step1', 'Enable debug logging')}
2. {t('support.step2', 'Reproduce the issue')}
3. {t('support.step3', 'Download the support bundle')}
4. {t('support.step4', 'Attach the ZIP file to your issue report')}
)}
{/* Privacy Info */}
{t('support.privacyTitle', 'What\'s in the support bundle?')}
{t('support.collected', 'Collected:')}
• {t('support.collectItem1', 'App version and debug mode')}
• {t('support.collectItem2', 'OS, architecture, Python version')}
• {t('support.collectItem3', 'Database statistics (counts only)')}
• {t('support.collectItem4', 'Printer models and nozzle counts')}
• {t('support.collectItem5', 'Non-sensitive settings (themes, formats)')}
• {t('support.collectItem6', 'Debug logs (sanitized)')}
• {t('support.collectItem7', 'Printer connectivity and firmware versions')}
• {t('support.collectItem8', 'Integration status (Spoolman, MQTT, HA)')}
• {t('support.collectItem9', 'Network interfaces (subnets only)')}
• {t('support.collectItem10', 'Python package versions')}
• {t('support.collectItem11', 'Database health checks')}
• {t('support.collectItem12', 'Docker environment details')}
{t('support.notCollected', 'NOT collected:')}
• {t('support.notItem1', 'Printer names and serial numbers')}
• {t('support.notItem2', 'Access codes and passwords')}
• {t('support.notItem3', 'Email addresses')}
• {t('support.notItem4', 'API keys and tokens')}
• {t('support.notItem5', 'Webhook URLs')}
• {t('support.notItem6', 'Your hostname or username')}
• {t('support.notItem7', 'IP addresses')}
{t('support.privacyNote', 'Email addresses in logs are replaced with [EMAIL], printer names with [PRINTER], serial numbers with [SERIAL], and IP addresses with [IP].')}
{/* Log Viewer */}
{/* Connection Diagnostic */}
{t(
'diagnostic.sectionDescription',
"Check why a printer won't connect or won't print — port reachability, LAN developer mode, Docker network mode, and credentials.",
)}
{allPrinters && allPrinters.length > 0 ? (
{allPrinters.map((printer) => (
{printer.name}
{printer.ip_address}
setDiagnosticPrinter(printer)}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary text-white rounded-lg transition-colors flex-shrink-0"
>
{t('diagnostic.runButton', 'Run diagnostic')}
))}
) : (
{t('diagnostic.noPrinters', 'No printers configured.')}
)}
{/* System Health */}
{t('systemHealth.sectionDescription')}
refetchHealth()}
disabled={healthFetching}
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-bambu-dark-secondary hover:bg-bambu-dark-tertiary text-white rounded-lg transition-colors flex-shrink-0 disabled:opacity-50"
>
{t('systemHealth.rescan')}
{systemHealth ? (
) : (
{t('common.loading')}
)}
{/* Database Stats */}
{/* Connected Printers */}
{systemInfo.printers.connected}
{t('system.ofTotal', 'of {{total}} printers connected', {
total: systemInfo.printers.total,
})}
{systemInfo.printers.connected_list.length > 0 ? (
{systemInfo.printers.connected_list.map((printer) => (
{printer.model}
{printer.state}
))}
) : (
{t('system.noPrintersConnected', 'No printers connected')}
)}
{/* Storage */}
{t('system.diskUsage', 'Disk Usage')}
{systemInfo.storage.disk_used_formatted} / {systemInfo.storage.disk_total_formatted}
{systemInfo.storage.disk_free_formatted} {t('system.free', 'free')} (
{(100 - systemInfo.storage.disk_percent_used).toFixed(1)}%)
{libraryStats && (
)}
{/* System Resources */}
{/* Memory */}
{t('system.memoryUsage', 'Memory Usage')}
{systemInfo.memory.used_formatted} / {systemInfo.memory.total_formatted}
{systemInfo.memory.available_formatted} {t('system.available', 'available')}
{/* CPU */}
{/* System Details */}
{diagnosticPrinter && (
setDiagnosticPrinter(null)}
/>
)}
);
}