- );
-}
-
-function StatusBadge({ healthy }) {
- const color = healthy ? '#22c55e' : '#ef4444';
- const text = healthy ? 'HEALTHY' : 'DEGRADED';
- return (
-
- );
-}
-
-function EnclosureCard({ enclosure, onDriveClick, selectedDrive }) {
- const [expanded, setExpanded] = useState(true);
- const enc = enclosure;
-
- return (
-
- );
-}
-
-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 (
-
- );
-}
-
-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 &&
}
+ {/* Loading / Error */}
+ {loading && (
+
+ Loading enclosure data...
)}
+ {error && (
+
+ Failed to fetch: {error}
+
+ )}
+
+ {data && (
+ <>
+ {/* Stats */}
+
+
+
+ 0 ? t.health.warning.text : t.health.healthy.dot}
+ t={t}
+ />
+ 0 ? t.health.error.text : t.health.healthy.dot}
+ t={t}
+ />
+
+
+
+ {/* Enclosures */}
+
+ {data.enclosures.map((enc) => (
+
+ ))}
+
+ >
+ )}
+
+ {selected &&
setSelected(null)} t={t} />}
);
}
-
-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;
diff --git a/models/schemas.py b/models/schemas.py
index b3c18b3..6a79ad8 100644
--- a/models/schemas.py
+++ b/models/schemas.py
@@ -40,11 +40,18 @@ class DriveHealthSummary(BaseModel):
device: str
model: str | None = None
serial: str | None = None
+ wwn: str | None = None
+ firmware: str | None = None
+ capacity_bytes: int | None = None
smart_healthy: bool | None = None
smart_supported: bool = True
temperature_c: int | None = None
power_on_hours: int | None = None
+ reallocated_sectors: int | None = None
+ pending_sectors: int | None = None
+ uncorrectable_errors: int | None = None
zfs_pool: str | None = None
+ health_status: str = "healthy"
class SlotWithDrive(BaseModel):
diff --git a/routers/overview.py b/routers/overview.py
index 5e22de9..246305b 100644
--- a/routers/overview.py
+++ b/routers/overview.py
@@ -68,15 +68,33 @@ async def get_overview():
if sd.get("uncorrectable_errors") and sd["uncorrectable_errors"] > 0:
warnings += 1
+ # Compute health_status for frontend
+ realloc = sd.get("reallocated_sectors") or 0
+ pending = sd.get("pending_sectors") or 0
+ unc = sd.get("uncorrectable_errors") or 0
+ if healthy is False:
+ health_status = "error"
+ elif realloc > 0 or pending > 0 or unc > 0 or (healthy is None and sd.get("smart_supported", True)):
+ health_status = "warning"
+ else:
+ health_status = "healthy"
+
drive_summary = DriveHealthSummary(
device=sd["device"],
model=sd.get("model"),
serial=sd.get("serial"),
+ wwn=sd.get("wwn"),
+ firmware=sd.get("firmware"),
+ capacity_bytes=sd.get("capacity_bytes"),
smart_healthy=healthy,
smart_supported=sd.get("smart_supported", True),
temperature_c=sd.get("temperature_c"),
power_on_hours=sd.get("power_on_hours"),
+ reallocated_sectors=sd.get("reallocated_sectors"),
+ pending_sectors=sd.get("pending_sectors"),
+ uncorrectable_errors=sd.get("uncorrectable_errors"),
zfs_pool=pool_map.get(sd["device"]),
+ health_status=health_status,
)
elif s["populated"]:
total_drives += 1