feat: add sysfs serial fallback for RAID disks and responsive disk grid layout with improved styling

Add _device_serial() to read disk serials from /sys/class/block/{name}/device/serial and /sys/block/{name}/device/serial when lsblk cannot expose them in Docker containers. Update raid_status() to use sysfs fallback before defaulting to "—" for missing serials. Add .raid-disks 2-column grid with 6px gap, .raid-disk 7px/9px padding, .raid-disk small 1px top margin, and .raid-smart 85% opacity. Add mobile
This commit is contained in:
2026-08-16 12:11:47 +02:00
parent 0c922d4f5d
commit f7638dd5d5
2 changed files with 15 additions and 2 deletions
+14 -1
View File
@@ -240,6 +240,18 @@ def _flatten_lsblk(nodes):
result.extend(_flatten_lsblk(node.get("children"))) result.extend(_flatten_lsblk(node.get("children")))
return result return result
def _device_serial(device):
"""Read a disk serial from sysfs when lsblk cannot expose it in Docker."""
name = Path(device).name
for path in (Path("/sys/class/block") / name / "device/serial", Path("/sys/block") / name / "device/serial"):
try:
value = path.read_text().strip()
if value:
return value
except OSError:
continue
return ""
def format_transfer_rate(value): def format_transfer_rate(value):
"""Convert mdstat rates such as 133628K/sec into a readable value.""" """Convert mdstat rates such as 133628K/sec into a readable value."""
if not value or value == "": if not value or value == "":
@@ -340,7 +352,8 @@ def raid_status():
info = host_items[0] if host_items else {} info = host_items[0] if host_items else {}
except (ValueError, AttributeError): except (ValueError, AttributeError):
info = {} info = {}
disk.update({"size": format_bytes(info.get("size") or 0) if info.get("size") else "", "model": info.get("model") or "", "serial": info.get("serial") or ""}) serial = 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 ""})
if SMART_ENABLED and shutil.which("smartctl"): if SMART_ENABLED and shutil.which("smartctl"):
smart_device = disk["device"].replace("/dev/", f"{HOST_DEVICE_PATH.rstrip('/')}/", 1) smart_device = disk["device"].replace("/dev/", f"{HOST_DEVICE_PATH.rstrip('/')}/", 1)
smart_output, _ = _read_command(["smartctl", "-H", smart_device], timeout=6) smart_output, _ = _read_command(["smartctl", "-H", smart_device], timeout=6)
+1 -1
View File
@@ -195,4 +195,4 @@ document.querySelectorAll('.service-logo img').forEach(img=>{img.style.width='20
setInterval(()=>{document.querySelectorAll('.service-logo svg').forEach(svg=>{svg.style.width='19px';svg.style.height='19px';svg.style.fill='none';svg.style.stroke='currentColor';svg.style.strokeWidth='1.8';svg.style.strokeLinecap='round';svg.style.strokeLinejoin='round'});document.querySelectorAll('.service-logo img').forEach(img=>{img.style.width='20px';img.style.height='20px';img.style.objectFit='contain'})},500); setInterval(()=>{document.querySelectorAll('.service-logo svg').forEach(svg=>{svg.style.width='19px';svg.style.height='19px';svg.style.fill='none';svg.style.stroke='currentColor';svg.style.strokeWidth='1.8';svg.style.strokeLinecap='round';svg.style.strokeLinejoin='round'});document.querySelectorAll('.service-logo img').forEach(img=>{img.style.width='20px';img.style.height='20px';img.style.objectFit='contain'})},500);
const settingsBackdrop=document.querySelector('#settingsBackdrop');const closeSettings=()=>settingsBackdrop.classList.add('hidden');document.querySelector('#settingsOpen').addEventListener('click',()=>{loadPreferences();settingsBackdrop.classList.remove('hidden')});document.querySelector('#settingsClose').addEventListener('click',closeSettings);document.querySelector('#settingsCancel').addEventListener('click',closeSettings);settingsBackdrop.addEventListener('click',event=>{if(event.target===settingsBackdrop)closeSettings()});document.querySelector('#settingsSave').addEventListener('click',()=>{localStorage.setItem('preferred-audio',document.querySelector('#preferredAudio').value);localStorage.setItem('preferred-subs',document.querySelector('#preferredSubs').value);loadPreferences();closeSettings()}); const settingsBackdrop=document.querySelector('#settingsBackdrop');const closeSettings=()=>settingsBackdrop.classList.add('hidden');document.querySelector('#settingsOpen').addEventListener('click',()=>{loadPreferences();settingsBackdrop.classList.remove('hidden')});document.querySelector('#settingsClose').addEventListener('click',closeSettings);document.querySelector('#settingsCancel').addEventListener('click',closeSettings);settingsBackdrop.addEventListener('click',event=>{if(event.target===settingsBackdrop)closeSettings()});document.querySelector('#settingsSave').addEventListener('click',()=>{localStorage.setItem('preferred-audio',document.querySelector('#preferredAudio').value);localStorage.setItem('preferred-subs',document.querySelector('#preferredSubs').value);loadPreferences();closeSettings()});
</script> </script>
<style>.nav-icon{width:15px;height:15px;margin-right:5px;vertical-align:-3px;object-fit:contain}.storage-icon img{width:20px;height:20px;object-fit:contain}</style> <style>.nav-icon{width:15px;height:15px;margin-right:5px;vertical-align:-3px;object-fit:contain}.storage-icon img{width:20px;height:20px;object-fit:contain}.raid-disks{grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}.raid-disk{padding:7px 9px}.raid-disk small{margin-top:1px}.raid-smart{opacity:.85}@media(max-width:700px){.raid-disks{grid-template-columns:1fr}}</style>