| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689 |
- 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 (
- <div className="flex items-start gap-3 p-4 bg-bambu-dark rounded-lg">
- <div className={`p-2 rounded-lg bg-bambu-dark-tertiary ${color}`}>
- <Icon className="w-5 h-5" />
- </div>
- <div className="flex-1 min-w-0">
- <p className="text-sm text-bambu-gray">{label}</p>
- <p className="text-lg font-semibold text-white truncate">{value}</p>
- {subValue && <p className="text-xs text-bambu-gray mt-0.5">{subValue}</p>}
- </div>
- </div>
- );
- }
- function ProgressBar({ percent, color = 'bg-bambu-green' }: { percent: number; color?: string }) {
- return (
- <div className="w-full h-2 bg-bambu-dark rounded-full overflow-hidden">
- <div
- className={`h-full ${color} transition-all duration-300`}
- style={{ width: `${Math.min(100, percent)}%` }}
- />
- </div>
- );
- }
- function Section({
- title,
- icon: Icon,
- children,
- }: {
- title: string;
- icon: React.ElementType;
- children: React.ReactNode;
- }) {
- return (
- <Card className="p-6">
- <div className="flex items-center gap-2 mb-4">
- <Icon className="w-5 h-5 text-bambu-green" />
- <h2 className="text-lg font-semibold text-white">{title}</h2>
- </div>
- {children}
- </Card>
- );
- }
- export function SystemInfoPage() {
- const { t } = useTranslation();
- const queryClient = useQueryClient();
- const [bundleError, setBundleError] = useState<string | null>(null);
- const [bundleDownloading, setBundleDownloading] = useState(false);
- const [debugToggling, setDebugToggling] = useState(false);
- const [diagnosticPrinter, setDiagnosticPrinter] = useState<PrinterModel | null>(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 (
- <div className="flex items-center justify-center h-64">
- <Loader2 className="w-8 h-8 text-bambu-green animate-spin" />
- </div>
- );
- }
- if (!systemInfo) {
- return (
- <div className="p-6 text-center text-bambu-gray">
- {t('system.failedToLoad', 'Failed to load system information')}
- </div>
- );
- }
- 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 (
- <div className="p-6 space-y-6">
- {/* Header */}
- <div className="flex items-center justify-between">
- <div>
- <h1 className="text-2xl font-bold text-white">{t('system.title', 'System Information')}</h1>
- <p className="text-bambu-gray mt-1">
- {t('system.subtitle', 'Monitor system resources and database statistics')}
- </p>
- </div>
- <button
- onClick={() => 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"
- >
- <RefreshCw className={`w-4 h-4 ${isFetching ? 'animate-spin' : ''}`} />
- {t('common.refresh', 'Refresh')}
- </button>
- </div>
- {/* Application Info */}
- <Section title={t('system.application', 'Application')} icon={Server}>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <StatCard
- icon={Server}
- label={t('system.version', 'Version')}
- value={`v${systemInfo.app.version}`}
- />
- <StatCard
- icon={Clock}
- label={t('system.uptime', 'System Uptime')}
- value={systemInfo.system.uptime_formatted}
- />
- <StatCard
- icon={Server}
- label={t('system.hostname', 'Hostname')}
- value={systemInfo.system.hostname}
- />
- </div>
- </Section>
- {/* Support & Troubleshooting */}
- <Section title={t('support.title', 'Support & Troubleshooting')} icon={Headphones}>
- <div className="space-y-4">
- <p className="text-sm text-bambu-gray">
- {t('support.description', 'Enable debug logging to capture detailed information, then download a support bundle to share when reporting issues.')}
- </p>
- {/* Debug Logging Toggle */}
- <div className="flex items-center justify-between p-4 bg-bambu-dark rounded-lg">
- <div className="flex items-center gap-3">
- <div className={`p-2 rounded-lg ${debugLoggingState?.enabled ? 'bg-amber-500/20 text-amber-500' : 'bg-bambu-dark-tertiary text-bambu-gray'}`}>
- <Bug className="w-5 h-5" />
- </div>
- <div>
- <p className="font-medium text-white">{t('support.debugLogging', 'Debug Logging')}</p>
- <p className="text-sm text-bambu-gray">
- {debugLoggingState?.enabled
- ? t('support.debugLoggingEnabled', 'Capturing detailed logs')
- : t('support.debugLoggingDisabled', 'Normal logging level')}
- {debugLoggingState?.enabled && debugLoggingState.duration_seconds !== null && (
- <span className="text-amber-400 ml-2">
- ({Math.floor(debugLoggingState.duration_seconds / 60)}m {debugLoggingState.duration_seconds % 60}s)
- </span>
- )}
- </p>
- </div>
- </div>
- <button
- onClick={handleToggleDebugLogging}
- disabled={debugToggling}
- className={`px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 ${
- debugLoggingState?.enabled
- ? 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/30'
- : 'bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30'
- } disabled:opacity-50`}
- >
- {debugToggling && <Loader2 className="w-4 h-4 animate-spin" />}
- {debugLoggingState?.enabled
- ? t('support.disableDebug', 'Disable')
- : t('support.enableDebug', 'Enable')}
- </button>
- </div>
- {/* Support Bundle Download */}
- <div className="flex items-center justify-between p-4 bg-bambu-dark rounded-lg">
- <div className="flex items-center gap-3">
- <div className="p-2 rounded-lg bg-bambu-dark-tertiary text-bambu-green">
- <Download className="w-5 h-5" />
- </div>
- <div>
- <p className="font-medium text-white">{t('support.supportBundle', 'Support Bundle')}</p>
- <p className="text-sm text-bambu-gray">
- {t('support.supportBundleDescription', 'Download system info and logs as a ZIP file')}
- </p>
- </div>
- </div>
- <button
- onClick={handleDownloadBundle}
- disabled={bundleDownloading || !debugLoggingState?.enabled}
- className="px-4 py-2 rounded-lg font-medium bg-bambu-green/20 text-bambu-green hover:bg-bambu-green/30 transition-colors flex items-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
- title={!debugLoggingState?.enabled ? t('support.enableDebugFirst', 'Enable debug logging first') : undefined}
- >
- {bundleDownloading && <Loader2 className="w-4 h-4 animate-spin" />}
- {bundleDownloading
- ? t('support.bundleGenerating', 'Generating...')
- : t('common.download', 'Download')}
- </button>
- </div>
- {/* 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 && (
- <div className="p-3 bg-bambu-dark-tertiary/40 rounded-lg space-y-1">
- <p className="text-sm font-medium text-white flex items-center gap-2">
- <Loader2 className="w-3.5 h-3.5 animate-spin text-bambu-green" />
- {t('support.bundleGenerating', 'Generating...')}
- </p>
- <ul className="text-xs text-bambu-gray list-disc list-inside space-y-0.5 pl-1">
- <li>{t('support.bundleStepConnection', 'Running printer connectivity checks')}</li>
- <li>{t('support.bundleStepVirtualPrinters', 'Running virtual-printer setup checks')}</li>
- <li>{t('support.bundleStepLogScan', 'Scanning recent logs for known issues')}</li>
- <li>{t('support.bundleStepBuild', 'Building the support bundle ZIP')}</li>
- </ul>
- </div>
- )}
- {/* Error message */}
- {bundleError && (
- <div className="p-3 bg-red-500/20 border border-red-500/30 rounded-lg text-red-400 text-sm">
- {bundleError}
- </div>
- )}
- {/* Instructions */}
- {!debugLoggingState?.enabled && (
- <div className="p-4 bg-bambu-dark-tertiary/50 rounded-lg">
- <p className="text-sm text-bambu-gray">
- <span className="text-amber-400 font-medium">{t('support.instructions', 'To report an issue:')}</span>
- <br />
- 1. {t('support.step1', 'Enable debug logging')}
- <br />
- 2. {t('support.step2', 'Reproduce the issue')}
- <br />
- 3. {t('support.step3', 'Download the support bundle')}
- <br />
- 4. {t('support.step4', 'Attach the ZIP file to your issue report')}
- </p>
- </div>
- )}
- {/* Privacy Info */}
- <div className="p-4 bg-bambu-dark rounded-lg space-y-3">
- <p className="text-sm font-medium text-white">{t('support.privacyTitle', 'What\'s in the support bundle?')}</p>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
- <div>
- <p className="text-bambu-green font-medium mb-1">{t('support.collected', 'Collected:')}</p>
- <ul className="text-bambu-gray space-y-0.5">
- <li>• {t('support.collectItem1', 'App version and debug mode')}</li>
- <li>• {t('support.collectItem2', 'OS, architecture, Python version')}</li>
- <li>• {t('support.collectItem3', 'Database statistics (counts only)')}</li>
- <li>• {t('support.collectItem4', 'Printer models and nozzle counts')}</li>
- <li>• {t('support.collectItem5', 'Non-sensitive settings (themes, formats)')}</li>
- <li>• {t('support.collectItem6', 'Debug logs (sanitized)')}</li>
- <li>• {t('support.collectItem7', 'Printer connectivity and firmware versions')}</li>
- <li>• {t('support.collectItem8', 'Integration status (Spoolman, MQTT, HA)')}</li>
- <li>• {t('support.collectItem9', 'Network interfaces (subnets only)')}</li>
- <li>• {t('support.collectItem10', 'Python package versions')}</li>
- <li>• {t('support.collectItem11', 'Database health checks')}</li>
- <li>• {t('support.collectItem12', 'Docker environment details')}</li>
- </ul>
- </div>
- <div>
- <p className="text-red-400 font-medium mb-1">{t('support.notCollected', 'NOT collected:')}</p>
- <ul className="text-bambu-gray space-y-0.5">
- <li>• {t('support.notItem1', 'Printer names and serial numbers')}</li>
- <li>• {t('support.notItem2', 'Access codes and passwords')}</li>
- <li>• {t('support.notItem3', 'Email addresses')}</li>
- <li>• {t('support.notItem4', 'API keys and tokens')}</li>
- <li>• {t('support.notItem5', 'Webhook URLs')}</li>
- <li>• {t('support.notItem6', 'Your hostname or username')}</li>
- <li>• {t('support.notItem7', 'IP addresses')}</li>
- </ul>
- </div>
- </div>
- <p className="text-xs text-bambu-gray/70">
- {t('support.privacyNote', 'Email addresses in logs are replaced with [EMAIL], printer names with [PRINTER], serial numbers with [SERIAL], and IP addresses with [IP].')}
- </p>
- </div>
- {/* Log Viewer */}
- <LogViewer />
- </div>
- </Section>
- {/* Connection Diagnostic */}
- <Section title={t('diagnostic.sectionTitle', 'Connection Diagnostic')} icon={Stethoscope}>
- <p className="text-sm text-bambu-gray mb-4">
- {t(
- 'diagnostic.sectionDescription',
- "Check why a printer won't connect or won't print — port reachability, LAN developer mode, Docker network mode, and credentials.",
- )}
- </p>
- {allPrinters && allPrinters.length > 0 ? (
- <div className="space-y-2">
- {allPrinters.map((printer) => (
- <div
- key={printer.id}
- className="flex items-center justify-between p-3 bg-bambu-dark rounded-lg"
- >
- <div className="min-w-0">
- <span className="font-medium text-white">{printer.name}</span>
- <span className="text-sm text-bambu-gray ml-2">{printer.ip_address}</span>
- </div>
- <button
- onClick={() => 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"
- >
- <Stethoscope className="w-4 h-4" />
- {t('diagnostic.runButton', 'Run diagnostic')}
- </button>
- </div>
- ))}
- </div>
- ) : (
- <p className="text-bambu-gray">{t('diagnostic.noPrinters', 'No printers configured.')}</p>
- )}
- </Section>
- {/* System Health */}
- <Section title={t('systemHealth.sectionTitle')} icon={HeartPulse}>
- <div className="flex items-start justify-between gap-3 mb-3">
- <p className="text-sm text-bambu-gray">{t('systemHealth.sectionDescription')}</p>
- <button
- onClick={() => 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"
- >
- <RefreshCw className={`w-4 h-4 ${healthFetching ? 'animate-spin' : ''}`} />
- {t('systemHealth.rescan')}
- </button>
- </div>
- {systemHealth ? (
- <SystemHealthPanel result={systemHealth} />
- ) : (
- <p className="text-sm text-bambu-gray">{t('common.loading')}</p>
- )}
- </Section>
- {/* Database Stats */}
- <Section title={t('system.database', 'Database')} icon={Database}>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
- <StatCard
- icon={Database}
- label={t('system.dbEngine', 'Database Engine')}
- value={systemInfo.database.engine || 'SQLite'}
- />
- <StatCard
- icon={Database}
- label={t('system.dbVersion', 'Version')}
- value={systemInfo.database.version || 'unknown'}
- />
- </div>
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
- <StatCard
- icon={Archive}
- label={t('system.totalArchives', 'Total Archives')}
- value={systemInfo.database.archives}
- />
- <StatCard
- icon={CheckCircle2}
- label={t('system.completed', 'Completed')}
- value={systemInfo.database.archives_completed}
- color="text-green-500"
- />
- <StatCard
- icon={XCircle}
- label={t('system.failed', 'Failed')}
- value={systemInfo.database.archives_failed}
- color="text-red-500"
- />
- <StatCard
- icon={Loader2}
- label={t('system.printing', 'Printing')}
- value={systemInfo.database.archives_printing}
- color="text-yellow-500"
- />
- </div>
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
- <StatCard
- icon={Printer}
- label={t('system.printers', 'Printers')}
- value={systemInfo.database.printers}
- />
- <StatCard
- icon={Palette}
- label={t('system.filaments', 'Filaments')}
- value={systemInfo.database.filaments}
- />
- <StatCard
- icon={FolderKanban}
- label={t('system.projects', 'Projects')}
- value={systemInfo.database.projects}
- />
- <StatCard
- icon={Plug}
- label={t('system.smartPlugs', 'Smart Plugs')}
- value={systemInfo.database.smart_plugs}
- />
- </div>
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
- <StatCard
- icon={Clock}
- label={t('system.totalPrintTime', 'Total Print Time')}
- value={systemInfo.database.total_print_time_formatted}
- />
- <StatCard
- icon={Archive}
- label={t('system.totalFilament', 'Total Filament Used')}
- value={`${systemInfo.database.total_filament_kg} kg`}
- subValue={`${systemInfo.database.total_filament_grams.toLocaleString()} g`}
- />
- </div>
- </Section>
- {/* Connected Printers */}
- <Section title={t('system.connectedPrinters', 'Connected Printers')} icon={Printer}>
- <div className="flex items-center gap-4 mb-4">
- <div className="text-3xl font-bold text-bambu-green">
- {systemInfo.printers.connected}
- </div>
- <div className="text-bambu-gray">
- {t('system.ofTotal', 'of {{total}} printers connected', {
- total: systemInfo.printers.total,
- })}
- </div>
- </div>
- {systemInfo.printers.connected_list.length > 0 ? (
- <div className="space-y-2">
- {systemInfo.printers.connected_list.map((printer) => (
- <div
- key={printer.id}
- className="flex items-center justify-between p-3 bg-bambu-dark rounded-lg"
- >
- <div className="flex items-center gap-3">
- <div className="w-2 h-2 rounded-full bg-bambu-green" />
- <span className="font-medium text-white">{printer.name}</span>
- </div>
- <div className="flex items-center gap-4 text-sm text-bambu-gray">
- <span>{printer.model}</span>
- <span
- className={`px-2 py-0.5 rounded ${
- printer.state === 'RUNNING'
- ? 'bg-bambu-green/20 text-bambu-green'
- : printer.state === 'IDLE'
- ? 'bg-blue-500/20 text-blue-400'
- : 'bg-bambu-dark-tertiary'
- }`}
- >
- {printer.state}
- </span>
- </div>
- </div>
- ))}
- </div>
- ) : (
- <p className="text-bambu-gray">{t('system.noPrintersConnected', 'No printers connected')}</p>
- )}
- </Section>
- {/* Storage */}
- <Section title={t('system.storage', 'Storage')} icon={HardDrive}>
- <div className="space-y-4">
- <div>
- <div className="flex justify-between text-sm mb-1">
- <span className="text-bambu-gray">{t('system.diskUsage', 'Disk Usage')}</span>
- <span className="text-white">
- {systemInfo.storage.disk_used_formatted} / {systemInfo.storage.disk_total_formatted}
- </span>
- </div>
- <ProgressBar percent={systemInfo.storage.disk_percent_used} color={diskColor} />
- <p className="text-xs text-bambu-gray mt-1">
- {systemInfo.storage.disk_free_formatted} {t('system.free', 'free')} (
- {(100 - systemInfo.storage.disk_percent_used).toFixed(1)}%)
- </p>
- </div>
- <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
- <StatCard
- icon={Archive}
- label={t('system.archiveStorage', 'Archive Storage')}
- value={systemInfo.storage.archive_size_formatted}
- />
- <StatCard
- icon={Database}
- label={t('system.databaseSize', 'Database Size')}
- value={systemInfo.storage.database_size_formatted}
- />
- {libraryStats && (
- <StatCard
- icon={FolderOpen}
- label={t('system.fileManagerStorage', 'File Manager')}
- value={formatBytes(libraryStats.total_size_bytes)}
- subValue={`${libraryStats.total_files} files, ${libraryStats.total_folders} folders`}
- />
- )}
- </div>
- </div>
- </Section>
- {/* System Resources */}
- <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
- {/* Memory */}
- <Section title={t('system.memory', 'Memory')} icon={MemoryStick}>
- <div className="space-y-4">
- <div>
- <div className="flex justify-between text-sm mb-1">
- <span className="text-bambu-gray">{t('system.memoryUsage', 'Memory Usage')}</span>
- <span className="text-white">
- {systemInfo.memory.used_formatted} / {systemInfo.memory.total_formatted}
- </span>
- </div>
- <ProgressBar percent={systemInfo.memory.percent_used} color={memoryColor} />
- <p className="text-xs text-bambu-gray mt-1">
- {systemInfo.memory.available_formatted} {t('system.available', 'available')}
- </p>
- </div>
- </div>
- </Section>
- {/* CPU */}
- <Section title={t('system.cpu', 'CPU')} icon={Cpu}>
- <div className="space-y-4">
- <div className="grid grid-cols-2 gap-4">
- <StatCard
- icon={Cpu}
- label={t('system.cores', 'Cores')}
- value={systemInfo.cpu.count}
- subValue={`${systemInfo.cpu.count_logical} logical`}
- />
- <StatCard
- icon={Cpu}
- label={t('system.usage', 'Usage')}
- value={`${systemInfo.cpu.percent}%`}
- />
- </div>
- </div>
- </Section>
- </div>
- {/* System Details */}
- <Section title={t('system.systemDetails', 'System Details')} icon={Server}>
- <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
- <StatCard
- icon={Server}
- label={t('system.os', 'Operating System')}
- value={systemInfo.system.platform}
- subValue={systemInfo.system.platform_release}
- />
- <StatCard
- icon={Cpu}
- label={t('system.architecture', 'Architecture')}
- value={systemInfo.system.architecture}
- />
- <StatCard
- icon={Server}
- label={t('system.python', 'Python')}
- value={systemInfo.system.python_version}
- />
- <StatCard
- icon={Clock}
- label={t('system.bootTime', 'Boot Time')}
- value={formatDateTime(systemInfo.system.boot_time, timeFormat)}
- />
- </div>
- </Section>
- {diagnosticPrinter && (
- <ConnectionDiagnosticModal
- printerId={diagnosticPrinter.id}
- printerName={diagnosticPrinter.name}
- onClose={() => setDiagnosticPrinter(null)}
- />
- )}
- </div>
- );
- }
|