mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-18 03:54:17 +02:00
Added system info page
This commit is contained in:
@@ -11,6 +11,7 @@ import { MaintenancePage } from './pages/MaintenancePage';
|
||||
import { ProjectsPage } from './pages/ProjectsPage';
|
||||
import { CameraPage } from './pages/CameraPage';
|
||||
import { ExternalLinkPage } from './pages/ExternalLinkPage';
|
||||
import { SystemInfoPage } from './pages/SystemInfoPage';
|
||||
import { useWebSocket } from './hooks/useWebSocket';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import { ToastProvider } from './contexts/ToastContext';
|
||||
@@ -49,6 +50,7 @@ function App() {
|
||||
<Route path="maintenance" element={<MaintenancePage />} />
|
||||
<Route path="projects" element={<ProjectsPage />} />
|
||||
<Route path="settings" element={<SettingsPage />} />
|
||||
<Route path="system" element={<SystemInfoPage />} />
|
||||
<Route path="external/:id" element={<ExternalLinkPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
@@ -1907,6 +1907,9 @@ export const api = {
|
||||
// AMS History
|
||||
getAMSHistory: (printerId: number, amsId: number, hours = 24) =>
|
||||
request<AMSHistoryResponse>(`/ams-history/${printerId}/${amsId}?hours=${hours}`),
|
||||
|
||||
// System Info
|
||||
getSystemInfo: () => request<SystemInfo>('/system/info'),
|
||||
};
|
||||
|
||||
// AMS History types
|
||||
@@ -1928,3 +1931,74 @@ export interface AMSHistoryResponse {
|
||||
max_temperature: number | null;
|
||||
avg_temperature: number | null;
|
||||
}
|
||||
|
||||
// System Info types
|
||||
export interface SystemInfo {
|
||||
app: {
|
||||
version: string;
|
||||
base_dir: string;
|
||||
archive_dir: string;
|
||||
};
|
||||
database: {
|
||||
archives: number;
|
||||
archives_completed: number;
|
||||
archives_failed: number;
|
||||
archives_printing: number;
|
||||
printers: number;
|
||||
filaments: number;
|
||||
projects: number;
|
||||
smart_plugs: number;
|
||||
total_print_time_seconds: number;
|
||||
total_print_time_formatted: string;
|
||||
total_filament_grams: number;
|
||||
total_filament_kg: number;
|
||||
};
|
||||
printers: {
|
||||
total: number;
|
||||
connected: number;
|
||||
connected_list: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
state: string;
|
||||
model: string;
|
||||
}>;
|
||||
};
|
||||
storage: {
|
||||
archive_size_bytes: number;
|
||||
archive_size_formatted: string;
|
||||
database_size_bytes: number;
|
||||
database_size_formatted: string;
|
||||
disk_total_bytes: number;
|
||||
disk_total_formatted: string;
|
||||
disk_used_bytes: number;
|
||||
disk_used_formatted: string;
|
||||
disk_free_bytes: number;
|
||||
disk_free_formatted: string;
|
||||
disk_percent_used: number;
|
||||
};
|
||||
system: {
|
||||
platform: string;
|
||||
platform_release: string;
|
||||
platform_version: string;
|
||||
architecture: string;
|
||||
hostname: string;
|
||||
python_version: string;
|
||||
uptime_seconds: number;
|
||||
uptime_formatted: string;
|
||||
boot_time: string;
|
||||
};
|
||||
memory: {
|
||||
total_bytes: number;
|
||||
total_formatted: string;
|
||||
available_bytes: number;
|
||||
available_formatted: string;
|
||||
used_bytes: number;
|
||||
used_formatted: string;
|
||||
percent_used: number;
|
||||
};
|
||||
cpu: {
|
||||
count: number;
|
||||
count_logical: number;
|
||||
percent: number;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, X, Menu, type LucideIcon } from 'lucide-react';
|
||||
import { Printer, Archive, Calendar, BarChart3, Cloud, Settings, Sun, Moon, ChevronLeft, ChevronRight, Keyboard, Github, GripVertical, ArrowUpCircle, Wrench, FolderKanban, X, Menu, Info, type LucideIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
import { KeyboardShortcutsModal } from './KeyboardShortcutsModal';
|
||||
@@ -457,6 +457,17 @@ export function Layout() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<NavLink
|
||||
to="/system"
|
||||
className={({ isActive }) =>
|
||||
`p-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors ${
|
||||
isActive ? 'text-bambu-green' : 'text-bambu-gray-light hover:text-white'
|
||||
}`
|
||||
}
|
||||
title={t('nav.system')}
|
||||
>
|
||||
<Info className="w-5 h-5" />
|
||||
</NavLink>
|
||||
<a
|
||||
href="https://github.com/maziggy/bambuddy"
|
||||
target="_blank"
|
||||
@@ -493,6 +504,17 @@ export function Layout() {
|
||||
<ArrowUpCircle className="w-5 h-5" />
|
||||
</button>
|
||||
)}
|
||||
<NavLink
|
||||
to="/system"
|
||||
className={({ isActive }) =>
|
||||
`p-2 rounded-lg hover:bg-bambu-dark-tertiary transition-colors ${
|
||||
isActive ? 'text-bambu-green' : 'text-bambu-gray-light hover:text-white'
|
||||
}`
|
||||
}
|
||||
title={t('nav.system')}
|
||||
>
|
||||
<Info className="w-5 h-5" />
|
||||
</NavLink>
|
||||
<a
|
||||
href="https://github.com/maziggy/bambuddy"
|
||||
target="_blank"
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
maintenance: 'Wartung',
|
||||
projects: 'Projekte',
|
||||
settings: 'Einstellungen',
|
||||
system: 'System',
|
||||
collapseSidebar: 'Seitenleiste einklappen',
|
||||
expandSidebar: 'Seitenleiste ausklappen',
|
||||
update: 'Update',
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
maintenance: 'Maintenance',
|
||||
projects: 'Projects',
|
||||
settings: 'Settings',
|
||||
system: 'System',
|
||||
collapseSidebar: 'Collapse sidebar',
|
||||
expandSidebar: 'Expand sidebar',
|
||||
update: 'Update',
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import { useQuery } 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,
|
||||
} from 'lucide-react';
|
||||
import { api } from '../api/client';
|
||||
import { Card } from '../components/Card';
|
||||
|
||||
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 { data: systemInfo, isLoading, refetch, isFetching } = useQuery({
|
||||
queryKey: ['systemInfo'],
|
||||
queryFn: api.getSystemInfo,
|
||||
refetchInterval: 30000, // Auto-refresh every 30 seconds
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
{/* Database Stats */}
|
||||
<Section title={t('system.database', 'Database')} icon={Database}>
|
||||
<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-2 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}
|
||||
/>
|
||||
</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={new Date(systemInfo.system.boot_time).toLocaleString()}
|
||||
/>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user