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 (
{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 (
);
}
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;