Compare commits

...
2 Commits
Author SHA1 Message Date
nessi 75f6de91b6 refactor: move I/O pressure panel into RAID section and capacity panel into categories section with DOM restructuring script
Remove max-width:calc(50% - 6px) from #ioPressurePanel and set max-width:none. Add .datastore-grid>.datastore-panel>.datastore-capacity and .datastore-grid>.raid-panel>#ioPressurePanel margin/padding rules. Add DOMContentLoaded script to select datastore root/grid/capacity/pressure elements, identify categories and raid sections from grid.children[0]/[1], and prepend capacity to
2026-08-17 19:28:14 +02:00
nessi d4c24f02d4 feat: add smart_serial() function to extract manufacturer serial numbers from SMART data with fallback chain and parallel execution
Add smart_serial() function with smartctl -i command execution, fallback to -d sat mode, and Serial Number: field regex extraction with WWN/0x prefix filtering to prefer physical serial over logical identifiers. Add serial_futures dict with service_check_executor.submit() calls for parallel serial reads alongside smart_futures/temperature_futures. Update raid_status() disk
2026-08-17 19:27:21 +02:00
2 changed files with 19 additions and 2 deletions

No files matched your search

+15 -1
View File
@@ -365,6 +365,18 @@ def smart_status(device):
health = re.search(r"(?:SMART overall-health self-assessment test result|SMART Health Status):\s*(.+)", smart_output, re.IGNORECASE)
return health.group(1).strip() if health else ("Fehler: " + smart_error.strip()[:80] if smart_error.strip() else "Nicht unterstützt")
def smart_serial(device):
"""Read the manufacturer serial, not a WWN or SCSI by-id alias."""
if not shutil.which("smartctl"):
return ""
smart_device = device.replace("/dev/", f"{HOST_DEVICE_PATH.rstrip('/')}/", 1)
for command in (["smartctl", "-i", "-d", "sat", smart_device], ["smartctl", "-i", smart_device]):
output, _ = _read_command(command, timeout=6, warn_on_nonzero=False)
match = re.search(r"^\s*Serial Number:\s*(\S+)\s*$", output, re.IGNORECASE | re.MULTILINE)
if match and not match.group(1).lower().startswith(("wwn-", "0x")):
return match.group(1).strip()
return ""
def smart_temperature(device):
"""Read the drive temperature in a controller-friendly way."""
if not SMART_ENABLED or not shutil.which("smartctl"):
@@ -457,9 +469,11 @@ def raid_status():
except (ValueError, AttributeError):
block_devices = {}
smart_futures = {}
serial_futures = {}
temperature_futures = {}
if shutil.which("smartctl"):
smart_futures = {disk["device"]: service_check_executor.submit(smart_status, disk["device"]) for disk in devices}
serial_futures = {disk["device"]: service_check_executor.submit(smart_serial, disk["device"]) for disk in devices}
temperature_futures = {disk["device"]: service_check_executor.submit(smart_temperature, disk["device"]) for disk in devices}
for disk in devices:
info = block_devices.get(disk["device"], {})
@@ -473,7 +487,7 @@ def raid_status():
info = host_items[0] if host_items else {}
except (ValueError, AttributeError):
info = {}
serial = info.get("serial") or _device_serial(disk["device"])
serial = (serial_futures[disk["device"]].result() if disk["device"] in serial_futures else "") or info.get("serial") or _device_serial(disk["device"])
disk.update({"size": format_bytes(info.get("size") or 0) if info.get("size") else "—", "model": info.get("model") or "—", "serial": serial or "—"})
serial_log_key = "serial:" + disk["device"]
if not serial and serial_log_key not in hardware_log_once:
+4 -1
View File
@@ -1,11 +1,14 @@
<!doctype html>
<style>
#ioPressurePanel{max-width:calc(50% - 6px);margin:0 0 12px;padding:14px}.io-pressure-bars{display:grid;gap:10px}.io-pressure-bars>div{display:grid;grid-template-columns:42px 48px 1fr;align-items:center;gap:8px;color:#9da5b2;font-size:11px}.io-pressure-bars b{color:#e8eef5;text-align:right}.io-pressure-bars .datastore-progress{height:8px}@media(max-width:900px){ #ioPressurePanel{max-width:none}}
#ioPressurePanel{max-width:none;margin:0 0 12px;padding:14px}.io-pressure-bars{display:grid;gap:10px}.io-pressure-bars>div{display:grid;grid-template-columns:42px 48px 1fr;align-items:center;gap:8px;color:#9da5b2;font-size:11px}.io-pressure-bars b{color:#e8eef5;text-align:right}.io-pressure-bars .datastore-progress{height:8px}.datastore-grid>.datastore-panel>.datastore-capacity{margin:0 0 12px;padding:14px}.datastore-grid>.raid-panel>#ioPressurePanel{margin:0 0 12px}@media(max-width:900px){ #ioPressurePanel{max-width:none}}
</style>
<script>
document.addEventListener('DOMContentLoaded',()=>{const root=document.querySelector('#datastore');if(!root)return;const grid=root.querySelector('.datastore-grid');if(grid&&!document.querySelector('#ioPressurePanel')){const panel=document.createElement('section');panel.id='ioPressurePanel';panel.className='datastore-panel io-pressure-panel';panel.innerHTML='<div class="datastore-panel-head"><div><h3>I/O-Auslastung</h3><p>Festplatten-Druck aus Linux PSI</p></div><strong id="ioPressureLabel">Wird geladen …</strong></div><div class="io-pressure-bars"><div><span>Some</span><b id="ioPressureSome">—</b><div class="datastore-progress"><span id="ioPressureSomeBar"></span></div></div><div><span>Full</span><b id="ioPressureFull">—</b><div class="datastore-progress"><span id="ioPressureFullBar"></span></div></div></div>';grid.before(panel)}const pressure=async()=>{try{const data=await (await fetch('/api/datastore')).json();const io=data.io_pressure||{};const some=Number(io.some?.avg10)||0,full=Number(io.full?.avg10)||0;const level=Math.max(some,full),label=level>=20?'Hoch':level>=5?'Beobachten':'Normal';const set=(id,value)=>{const node=document.querySelector(id);if(node)node.textContent=value};set('#ioPressureLabel',io.available?`${label} · ${level.toFixed(1)}%`:'Nicht verfügbar');set('#ioPressureSome',`${some.toFixed(1)}%`);set('#ioPressureFull',`${full.toFixed(1)}%`);const a=document.querySelector('#ioPressureSomeBar'),b=document.querySelector('#ioPressureFullBar');if(a)a.style.width=`${Math.min(100,some)}%`;if(b)b.style.width=`${Math.min(100,full)}%`}catch(error){}};pressure();setInterval(pressure,5000);const disks=document.querySelector('#raidDisks');if(disks){const observer=new MutationObserver(()=>{if([...disks.querySelectorAll('.raid-disk')].some(row=>!row.querySelector('.raid-temperature')))setTimeout(pressure,0)});observer.observe(disks,{childList:true,subtree:true})}});
</script>
<script>
document.addEventListener('DOMContentLoaded',()=>{const root=document.querySelector('#datastore'),grid=root?.querySelector('.datastore-grid'),capacity=root?.querySelector('.datastore-capacity'),pressure=root?.querySelector('#ioPressurePanel');if(!grid||!capacity||!pressure)return;const categories=grid.children[0],raid=grid.children[1];if(categories&&!categories.contains(capacity))categories.prepend(capacity);if(raid&&!raid.contains(pressure))raid.prepend(pressure)});
</script>
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', function () {
navigator.serviceWorker.register('/sw.js?v=4', {scope: '/'}).catch(function () {});