import React, { useState, useEffect, useCallback } from 'react'; const API_BASE = ""; function App() { const [overview, setOverview] = useState(null); const [selectedDrive, setSelectedDrive] = useState(null); const [driveDetail, setDriveDetail] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const fetchOverview = useCallback(async () => { try { const res = await fetch(`${API_BASE}/api/overview`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); setOverview(data); setError(null); setLastUpdated(new Date()); } catch (err) { setError(err.message); } finally { setLoading(false); } }, []); useEffect(() => { fetchOverview(); const interval = setInterval(fetchOverview, 30000); return () => clearInterval(interval); }, [fetchOverview]); const fetchDriveDetail = async (device) => { setSelectedDrive(device); setDriveDetail(null); try { const res = await fetch(`${API_BASE}/api/drives/${device}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); setDriveDetail(await res.json()); } catch (err) { setDriveDetail({ error: err.message }); } }; if (loading) return
Loading...
; if (error && !overview) return
Error: {error}
; return (

JBOD Monitor

{lastUpdated && ( Updated {lastUpdated.toLocaleTimeString()} )}
{overview && (
0 ? '#f59e0b' : undefined} /> 0 ? '#ef4444' : undefined} />
)} {overview?.enclosures.map((enc) => ( ))} {selectedDrive && ( { setSelectedDrive(null); setDriveDetail(null); }} /> )}
); } function StatusBadge({ healthy }) { const color = healthy ? '#22c55e' : '#ef4444'; const text = healthy ? 'HEALTHY' : 'DEGRADED'; return ( {text} ); } function SummaryCard({ label, value, color }) { return (
{value}
{label}
); } function EnclosureCard({ enclosure, onDriveClick, selectedDrive }) { const [expanded, setExpanded] = useState(true); const enc = enclosure; return (
setExpanded(!expanded)}>
{enc.vendor.trim()} {enc.model.trim()} {enc.id} {enc.sg_device && {enc.sg_device}}
{enc.populated_slots}/{enc.total_slots} slots {expanded ? '\u25BC' : '\u25B6'}
{expanded && (
{enc.slots.map((slot) => ( slot.device && onDriveClick(slot.device)} /> ))}
)}
); } function SlotCell({ slot, selected, onClick }) { let bg = '#1e293b'; // empty let border = '#334155'; if (slot.populated && slot.drive) { if (slot.drive.smart_healthy === false) { bg = '#7f1d1d'; border = '#ef4444'; } else if (slot.drive.smart_healthy === null) { bg = '#78350f'; border = '#f59e0b'; } else { bg = '#14532d'; border = '#22c55e'; } } else if (slot.populated) { bg = '#1e3a5f'; border = '#3b82f6'; } if (selected) border = '#fff'; return (
{slot.slot}
{slot.device &&
{slot.device}
} {slot.drive && (
{slot.drive.temperature_c != null ? `${slot.drive.temperature_c}C` : ''}
)}
); } function DriveDetailPanel({ device, detail, onClose }) { return (
e.stopPropagation()}>

/dev/{device}

{!detail ? (
Loading SMART data...
) : detail.error ? (
Error: {detail.error}
) : (
{detail.wear_leveling_percent != null && ( )} {detail.zfs_pool && }
)}
); } function DetailRow({ label, value }) { if (value == null) return null; return (
{label} {String(value)}
); } function formatBytes(bytes) { const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; let i = 0; let val = bytes; while (val >= 1000 && i < units.length - 1) { val /= 1000; i++; } return `${val.toFixed(1)} ${units[i]}`; } const styles = { container: { fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace', backgroundColor: '#0f172a', color: '#e2e8f0', minHeight: '100vh', padding: '20px', }, header: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', paddingBottom: '16px', borderBottom: '1px solid #334155', }, title: { fontSize: '24px', fontWeight: 'bold', color: '#f8fafc' }, headerRight: { display: 'flex', alignItems: 'center', gap: '12px' }, badge: { padding: '4px 12px', borderRadius: '4px', fontSize: '12px', fontWeight: 'bold', color: '#fff', }, timestamp: { fontSize: '12px', color: '#94a3b8' }, refreshBtn: { padding: '6px 14px', backgroundColor: '#334155', color: '#e2e8f0', border: '1px solid #475569', borderRadius: '4px', cursor: 'pointer', fontSize: '13px', }, summaryBar: { display: 'flex', gap: '16px', marginBottom: '24px', }, summaryCard: { backgroundColor: '#1e293b', padding: '16px 24px', borderRadius: '8px', border: '1px solid #334155', textAlign: 'center', flex: 1, }, summaryValue: { fontSize: '28px', fontWeight: 'bold' }, summaryLabel: { fontSize: '12px', color: '#94a3b8', marginTop: '4px', textTransform: 'uppercase' }, enclosure: { backgroundColor: '#1e293b', borderRadius: '8px', border: '1px solid #334155', marginBottom: '16px', overflow: 'hidden', }, enclosureHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 18px', cursor: 'pointer', borderBottom: '1px solid #334155', }, enclosureTitle: { fontSize: '16px', fontWeight: 'bold', color: '#f8fafc' }, enclosureId: { fontSize: '12px', color: '#64748b', marginLeft: '12px' }, enclosureSg: { fontSize: '12px', color: '#64748b', marginLeft: '8px' }, slotCount: { fontSize: '14px', color: '#94a3b8' }, expandIcon: { marginLeft: '8px', fontSize: '10px' }, slotGrid: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(70px, 1fr))', gap: '6px', padding: '12px', }, slot: { border: '2px solid', borderRadius: '6px', padding: '6px', textAlign: 'center', minHeight: '60px', display: 'flex', flexDirection: 'column', justifyContent: 'center', transition: 'border-color 0.15s', }, slotNum: { fontSize: '11px', color: '#94a3b8' }, slotDev: { fontSize: '12px', fontWeight: 'bold', color: '#f8fafc' }, slotTemp: { fontSize: '10px', color: '#94a3b8' }, overlay: { position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.6)', display: 'flex', justifyContent: 'center', alignItems: 'center', zIndex: 1000, }, detailPanel: { backgroundColor: '#1e293b', border: '1px solid #475569', borderRadius: '12px', padding: '24px', width: '450px', maxHeight: '80vh', overflowY: 'auto', }, detailHeader: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '20px', borderBottom: '1px solid #334155', paddingBottom: '12px', }, detailTitle: { fontSize: '18px', color: '#f8fafc' }, closeBtn: { background: 'none', border: 'none', color: '#94a3b8', fontSize: '18px', cursor: 'pointer', }, detailBody: {}, detailRow: { display: 'flex', justifyContent: 'space-between', padding: '8px 0', borderBottom: '1px solid #1e293b', }, detailLabel: { color: '#94a3b8', fontSize: '13px' }, detailValue: { color: '#f8fafc', fontSize: '13px', fontWeight: '500' }, loading: { display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', fontSize: '18px', color: '#94a3b8', }, error: { display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100vh', fontSize: '18px', color: '#ef4444', }, }; export default App;