better labels and handling of usb/sd card drives
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)}})}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user