""" Jinja2 template engine + filter/helper registration. Import `templates` from here — do not create additional Jinja2 instances. """ from datetime import datetime, timezone from fastapi.templating import Jinja2Templates templates = Jinja2Templates(directory="app/templates") # --------------------------------------------------------------------------- # Template filters # --------------------------------------------------------------------------- def _format_bytes(value: int | None) -> str: if value is None: return "—" tb = value / 1_000_000_000_000 if tb >= 1: return f"{tb:.0f} TB" gb = value / 1_000_000_000 return f"{gb:.0f} GB" def _format_eta(seconds: int | None) -> str: if not seconds or seconds <= 0: return "" h = seconds // 3600 m = (seconds % 3600) // 60 if h > 0: return f"~{h}h {m}m" if m else f"~{h}h" return f"~{m}m" if m else "<1m" def _temp_class(celsius: int | None) -> str: if celsius is None: return "" from app.config import settings if celsius < settings.temp_warn_c: return "temp-cool" if celsius < settings.temp_crit_c: return "temp-warm" return "temp-hot" def _format_dt(iso: str | None) -> str: if not iso: return "—" try: dt = datetime.fromisoformat(iso) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) local = dt.astimezone() return local.strftime("%H:%M:%S") except Exception: return iso def _format_dt_full(iso: str | None) -> str: """Date + time for history tables.""" if not iso: return "—" try: dt = datetime.fromisoformat(iso) if dt.tzinfo is None: dt = dt.replace(tzinfo=timezone.utc) local = dt.astimezone() return local.strftime("%Y-%m-%d %H:%M:%S") except Exception: return iso def _format_duration(seconds: float | int | None) -> str: if seconds is None or seconds < 0: return "—" seconds = int(seconds) h = seconds // 3600 m = (seconds % 3600) // 60 s = seconds % 60 if h > 0: return f"{h}h {m}m {s}s" if m > 0: return f"{m}m {s}s" return f"{s}s" # --------------------------------------------------------------------------- # Template globals # --------------------------------------------------------------------------- def _drive_status(drive: dict) -> str: short = (drive.get("smart_short") or {}).get("state", "idle") long_ = (drive.get("smart_long") or {}).get("state", "idle") health = drive.get("smart_health", "UNKNOWN") if "running" in (short, long_): return "running" if short == "failed" or long_ == "failed" or health == "FAILED": return "failed" if "passed" in (short, long_): return "passed" return "idle" def _format_elapsed(iso: str | None) -> str: """Human-readable elapsed time since an ISO timestamp (e.g. '2h 34m').""" if not iso: return "" try: start = datetime.fromisoformat(iso) if start.tzinfo is None: start = start.replace(tzinfo=timezone.utc) elapsed = int((datetime.now(timezone.utc) - start).total_seconds()) if elapsed < 0: return "" h = elapsed // 3600 m = (elapsed % 3600) // 60 s = elapsed % 60 if h > 0: return f"{h}h {m}m" if m > 0: return f"{m}m {s}s" return f"{s}s" except Exception: return "" # Register filters templates.env.filters["format_bytes"] = _format_bytes templates.env.filters["format_eta"] = _format_eta templates.env.filters["temp_class"] = _temp_class templates.env.filters["format_dt"] = _format_dt templates.env.filters["format_dt_full"] = _format_dt_full templates.env.filters["format_duration"] = _format_duration templates.env.filters["format_elapsed"] = _format_elapsed templates.env.globals["drive_status"] = _drive_status from app.config import settings as _settings templates.env.globals["app_version"] = _settings.app_version