better labels and handling of usb/sd card drives

This commit is contained in:
2026-04-12 19:07:01 -04:00
parent b71dfff47d
commit 5e84e75fea
2 changed files with 40 additions and 3 deletions

View File

@@ -247,7 +247,7 @@ def collect_smart(device: str):
'user_capacity' : data.get('user_capacity', {}).get('bytes', 0),
'rotation_rate' : data.get('rotation_rate', 0),
'form_factor' : data.get('form_factor', {}).get('name', ''),
'protocol' : data.get('device', {}).get('protocol', 'Unknown'),
'protocol' : data.get('device', {}).get('protocol', '') or '',
'sata_version' : data.get('sata_version', {}).get('string', ''),
'smart_attributes' : [],
'sas_error_counters' : None,
@@ -276,6 +276,35 @@ def collect_smart(device: str):
return info
def collect_udev_info(device: str):
'''
Query udevadm for device properties as a fallback when smartctl lacks data
(e.g. USB flash drives that aren't in smartctl's drivedb).
:param device: Block device path (e.g. /dev/sde)
'''
out, _, rc = run_cmd(['udevadm', 'info', '--query=property', f'--name={device}'], timeout=5)
if rc != 0:
return {}
props = {}
for line in out.splitlines():
if '=' in line:
k, v = line.split('=', 1)
props[k] = v
info = {}
vendor = (props.get('ID_VENDOR') or props.get('ID_USB_VENDOR') or '').replace('_', ' ').strip()
model = (props.get('ID_MODEL') or props.get('ID_USB_MODEL') or '').replace('_', ' ').strip()
if vendor and model:
info['model_family'] = f'{vendor} {model}'
elif vendor:
info['model_family'] = vendor
bus = (props.get('ID_BUS') or '').lower()
if bus:
info['protocol'] = bus.upper()
return info
def collect_disks():
'''Enumerate physical disks via lsblk and collect SMART data in parallel.'''
@@ -315,6 +344,14 @@ def collect_disks():
logging.warning('SMART failed for %s: %s', disk['name'], e)
for d in devs:
if not d.get('model_family') or not d.get('protocol'):
udev = collect_udev_info(d['path'])
if not d.get('model_family') and udev.get('model_family'):
d['model_family'] = udev['model_family']
if not d.get('protocol') and udev.get('protocol'):
d['protocol'] = udev['protocol']
if not d.get('protocol') and d.get('transport'):
d['protocol'] = d['transport'].upper()
d['health_score'] = compute_health_score(d)
with lock:

View File

@@ -343,13 +343,13 @@ function renderDisks(disks){
if(!disks||!disks.length){el.innerHTML='<tr><td colspan="11" class="empty">No disks detected</td></tr>';badge.textContent='0';return}
badge.textContent=disks.length;
el.innerHTML=disks.map((d,i)=>{
const proto=(d.protocol||d.transport||'').toUpperCase();
const proto=((d.transport&&d.transport.toLowerCase()!=='unknown'?d.transport:d.protocol)||'').toUpperCase();
let dtc='dtb';if(proto.includes('SAS')||proto.includes('SCSI'))dtc+=' dtb-sas';else if(proto.includes('NVME'))dtc+=' dtb-nv';
const ht=d.health===true?'PASSED':d.health===false?'FAILED':'N/A';
const hs=d.health_score!=null?d.health_score:'-';
const hasSmart=d.health===true||d.health===false||(d.smart_attributes&&d.smart_attributes.length)||(d.sas_error_counters);
const model=d.device_model||d.model||'—',family=d.model_family||'—',serial=d.serial_number||d.serial||'—';
return`<tr class="${hasSmart?'disk-row':''}" ${hasSmart?`onclick="toggleDiskDetail(this,${i})"`:''} style="${hasSmart?'':'cursor:default;opacity:.7'}"><td title="/dev/${d.name}">/dev/${d.name}</td><td title="${model}">${model.substring(0,30)}</td><td title="${family}">${family.substring(0,30)}</td><td title="${serial}" style="font-family:var(--mono);font-size:.68rem">${serial}</td><td class="r" title="${Br(d.user_capacity||d.size)}">${Br(d.user_capacity||d.size)}</td><td class="r ${tc(d.temperature)}" title="${d.temperature!=null?d.temperature+'°C / '+cToF(d.temperature)+'°F':'—'}">${d.temperature!=null?cToF(d.temperature)+'°F':'—'}</td><td class="r" title="${fmtH(d.power_on_hours)}">${fmtH(d.power_on_hours)}</td><td class="r">${d.rotation_rate||'—'}</td><td>${fmtForm(d.form_factor)}</td><td title="${proto||'?'}"><span class="${dtc}">${proto||'?'}</span></td><td><span class="score-badge" style="background:${scoreBg(hs)};color:${scoreCol(hs)}" title="${scoreLabel(hs)} — SMART ${ht}">${hs}/100</span></td></tr>`}).join('');
return`<tr class="${hasSmart?'disk-row':''}" ${hasSmart?`onclick="toggleDiskDetail(this,${i})"`:''} style="${hasSmart?'':'cursor:default;opacity:.7'}"><td title="/dev/${d.name}">/dev/${d.name}</td><td title="${model}">${model}</td><td title="${family}">${family}</td><td title="${serial}" style="font-family:var(--mono);font-size:.68rem">${serial}</td><td class="r" title="${Br(d.user_capacity||d.size)}">${Br(d.user_capacity||d.size)}</td><td class="r ${tc(d.temperature)}" title="${d.temperature!=null?d.temperature+'°C / '+cToF(d.temperature)+'°F':'—'}">${d.temperature!=null?cToF(d.temperature)+'°F':'—'}</td><td class="r" title="${fmtH(d.power_on_hours)}">${fmtH(d.power_on_hours)}</td><td class="r">${d.rotation_rate||'—'}</td><td>${fmtForm(d.form_factor)}</td><td title="${proto||'?'}"><span class="${dtc}">${proto||'?'}</span></td><td><span class="score-badge" style="background:${scoreBg(hs)};color:${scoreCol(hs)}" title="${scoreLabel(hs)} — SMART ${ht}">${hs}/100</span></td></tr>`}).join('');
if(_expanded.disks.size){disks.forEach((d,i)=>{if(_expanded.disks.has(d.name)){const rows=el.querySelectorAll('.disk-row');if(rows[i])toggleDiskDetail(rows[i],i)}})}
}