diff --git a/app.py b/app.py index 8dcd795..5b0716f 100644 --- a/app.py +++ b/app.py @@ -240,6 +240,45 @@ def _flatten_lsblk(nodes): result.extend(_flatten_lsblk(node.get("children"))) return result +def format_transfer_rate(value): + """Convert mdstat rates such as 133628K/sec into a readable value.""" + if not value or value == "—": + return "—" + match = re.match(r"([\d.]+)\s*([KMGTP]?)(?:B)?/sec", value, re.IGNORECASE) + if not match: + return value + amount = float(match.group(1)) + unit = match.group(2).upper() + multiplier = {"": 1, "K": 1024, "M": 1024 ** 2, "G": 1024 ** 3, "T": 1024 ** 4, "P": 1024 ** 5}.get(unit, 1) + bytes_per_second = amount * multiplier + units = ((1024 ** 3, "GB/s"), (1024 ** 2, "MB/s"), (1024, "KB/s")) + for threshold, label in units: + if bytes_per_second >= threshold: + return f"{bytes_per_second / threshold:.1f} {label}" + return f"{bytes_per_second:.0f} B/s" + +def format_remaining_time(value): + """Convert mdstat values such as 450.1min into a compact German duration.""" + if not value or value == "—": + return "—" + match = re.match(r"([\d.]+)(min|sec|hours?|days?)", value, re.IGNORECASE) + if not match: + return value + amount = float(match.group(1)) + unit = match.group(2).lower() + total_minutes = amount / 60 if unit.startswith("sec") else amount if unit.startswith("min") else amount * 60 if unit.startswith("hour") else amount * 1440 + total_minutes = max(0, round(total_minutes)) + days, remainder = divmod(total_minutes, 1440) + hours, minutes = divmod(remainder, 60) + parts = [] + if days: + parts.append(f"{days} T") + if hours or days: + parts.append(f"{hours} Std.") + if minutes and len(parts) < 2: + parts.append(f"{minutes} Min.") + return " ".join(parts) or "< 1 Min." + def raid_status(): mdstat, mdstat_error = "", "" try: @@ -249,7 +288,15 @@ def raid_status(): array_name = Path(RAID_DEVICE).name array_line = next((line for line in mdstat.splitlines() if line.startswith(f"{array_name} :")), "") profile_match = re.search(r"(raid\d+)", array_line) - members_match = re.search(r"\[(\d+)/(\d+)\]\s+\[([^\]]+)\]", mdstat) + # Match the member count on this RAID's own line. Searching all of + # /proc/mdstat can accidentally pick a different array's [x/y] value. + members_match = re.search(r"\[(\d+)/(\d+)\]\s+\[([^\]]+)\]", array_line) + mdstat_members = [] + for member_name, slot in re.findall(r"\b([A-Za-z0-9][A-Za-z0-9._-]*)\[(\d+)\]", array_line): + member_suffix = re.search(rf"\b{re.escape(member_name)}\[{re.escape(slot)}\]\(([^)]+)\)", array_line) + member_state = member_suffix.group(1).lower() if member_suffix else "active" + member_status = "failed" if "f" in member_state else ("spare" if "s" in member_state else "active") + mdstat_members.append({"device": f"/dev/{member_name}", "slot": slot, "state": member_state, "status": member_status}) progress_match = re.search(r"(?:recovery|resync|reshape|check)\s*=\s*([\d.]+)%", mdstat, re.IGNORECASE) finish_match = re.search(r"finish=([^\s]+)", mdstat) speed_match = re.search(r"speed=([^\s]+)", mdstat) @@ -272,6 +319,10 @@ def raid_status(): if match: state_text, device = match.group(2), match.group(3) devices.append({"device": device, "slot": match.group(1), "state": state_text, "status": "active" if "active" in state_text.lower() else ("spare" if "spare" in state_text.lower() else "failed")}) + known_devices = {Path(disk["device"]).name for disk in devices} + for disk in mdstat_members: + if Path(disk["device"]).name not in known_devices: + devices.append(disk) lsblk_output, _ = _read_command(["lsblk", "-J", "-b", "-o", "NAME,SIZE,MODEL,SERIAL,TYPE,PATH"]) try: block_devices = {item.get("path") or f"/dev/{item.get('name')}": item for item in _flatten_lsblk(json.loads(lsblk_output).get("blockdevices", []))} @@ -308,11 +359,16 @@ def raid_status(): status = "ok" else: status = "failed" + mdstat_active = int(members_match.group(1)) if members_match else 0 + mdstat_total = int(members_match.group(2)) if members_match else 0 + active_devices = mdstat_active or detail.get("active_devices", 0) + raid_devices = mdstat_total or detail.get("raid_devices", 0) or len(devices) + failed_devices = detail.get("failed_devices", 0) or max(0, raid_devices - active_devices) return {"device": RAID_DEVICE, "status": status, "label": {"ok": "OK", "degraded": "DEGRADED", "failed": "FAILED", "rebuilding": "REBUILDING"}.get(status, "UNKNOWN"), - "level": detail.get("level", "—"), "raid_devices": detail.get("raid_devices") or (int(members_match.group(2)) if members_match else 0), - "active_devices": detail.get("active_devices") or (int(members_match.group(1)) if members_match else 0), "failed_devices": detail.get("failed_devices", 0), + "level": detail.get("level", "—"), "raid_devices": raid_devices, + "active_devices": active_devices, "failed_devices": failed_devices, "devices": devices, "recovery": {"active": rebuilding, "percent": float(progress_match.group(1)) if progress_match else 0, - "finish": finish_match.group(1) if finish_match else "—", "speed": speed_match.group(1) if speed_match else "—"}, + "finish": format_remaining_time(finish_match.group(1)) if finish_match else "—", "speed": format_transfer_rate(speed_match.group(1)) if speed_match else "—"}, "error": None if array_line or mdadm_output else (mdadm_error or mdstat_error or "RAID-Status nicht verfügbar"), "updated_at": time.time()} def datastore_snapshot(): diff --git a/static/sw.js b/static/sw.js index c42edf5..c2352da 100644 --- a/static/sw.js +++ b/static/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'media-dashboard-shell-v1'; +const CACHE_NAME = 'media-max-shell-v2'; const SHELL = ['/static/media-mark.svg', '/static/manifest.json']; self.addEventListener('install', event => { diff --git a/templates/index.html b/templates/index.html index 5f387ea..0b1861d 100644 --- a/templates/index.html +++ b/templates/index.html @@ -1,4 +1,5 @@ +