Source code for exlab_wizard.ui.components.sync_status_icon

"""Sync-status icon component (Frontend Spec §3.2, §10.5.1).

Seven distinct visual states with a fixed color mapping:

* ``pending``               -- ``--color-muted``
* ``retrying`` (with N/M)   -- ``--color-info``
* ``synced``                -- ``--color-success``
* ``cleaned``               -- ``--color-success``
* ``failed``                -- ``--color-danger``
* ``blocked_by_validation`` -- ``--color-warning``
* ``override_active``       -- ``--color-info``

The component returns a dict suitable for a NiceGUI icon factory; the
layout (icon + optional ``(N/M)`` retry counter) is the caller's concern
so the icon can be embedded in a tree row, a detail-pane title bar, or a
staging-panel row identically.
"""

from __future__ import annotations

from typing import Any, Final, Literal

from exlab_wizard.constants import SyncStatus
from exlab_wizard.logging import get_logger

_log = get_logger(__name__)


# UI-only icon kinds: ``retrying`` and ``override_active`` are derived
# in the UI from the SyncStatus + sync-job state, not wire enum values.
type SyncStatusIconExtraKind = Literal["retrying", "override_active"]
type SyncStatusOrIcon = SyncStatus | SyncStatusIconExtraKind | str

STATUS_RETRYING: Final[str] = "retrying"
STATUS_OVERRIDE: Final[str] = "override_active"


_STATUS_TO_PROPS: dict[str, dict[str, str]] = {
    SyncStatus.PENDING.value: {
        "icon_name": "schedule",
        "color_var": "--color-muted",
        "tooltip": "Queued for sync",
    },
    STATUS_RETRYING: {
        "icon_name": "history",
        "color_var": "--color-info",
        "tooltip": "Retrying with backoff",
    },
    SyncStatus.SYNCED.value: {
        "icon_name": "check_circle",
        "color_var": "--color-success",
        "tooltip": "Synced and verified at NAS",
    },
    SyncStatus.CLEANED.value: {
        "icon_name": "cloud_done",
        "color_var": "--color-success",
        "tooltip": "Synced and locally cleaned; data on NAS only",
    },
    SyncStatus.FAILED.value: {
        "icon_name": "error",
        "color_var": "--color-danger",
        "tooltip": "Sync failed; retry budget exhausted",
    },
    SyncStatus.BLOCKED_BY_VALIDATION.value: {
        "icon_name": "warning",
        "color_var": "--color-warning",
        "tooltip": "Hard-tier validation finding gates sync",
    },
    STATUS_OVERRIDE: {
        "icon_name": "lock_open",
        "color_var": "--color-info",
        "tooltip": "Sync allowed under operator override",
    },
}


[docs] def sync_status_props( status: SyncStatusOrIcon, *, retry_n: int | None = None, retry_m: int | None = None, ) -> dict[str, Any]: """Compute icon + tooltip + retry-counter for a sync status. The ``retry_n``/``retry_m`` annotations are rendered only when ``status == "retrying"`` (Frontend §10.5.1). """ key = status.value if isinstance(status, SyncStatus) else str(status) if key not in _STATUS_TO_PROPS: raise ValueError( f"unknown sync status {status!r}: must be one of {sorted(_STATUS_TO_PROPS)}", ) base = dict(_STATUS_TO_PROPS[key]) base["status"] = key if key == STATUS_RETRYING and retry_n is not None and retry_m is not None: base["retry_label"] = f"({retry_n}/{retry_m})" base["tooltip"] = f"Retry {retry_n} of {retry_m}, awaiting backoff" else: base["retry_label"] = "" return base
[docs] def sync_status_icon( status: SyncStatusOrIcon, *, retry_n: int | None = None, retry_m: int | None = None, ) -> Any: """Build a NiceGUI row containing the icon and optional retry counter.""" props = sync_status_props(status, retry_n=retry_n, retry_m=retry_m) try: from nicegui import ui except Exception: return props row = ui.row().classes("items-center").style("gap: 0.25rem;") with row: ui.icon(props["icon_name"]).style( f"color: var({props['color_var']}); font-size: 1rem;" ).tooltip(props["tooltip"]) if props["retry_label"]: ui.label(props["retry_label"]).style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " f"color: var({props['color_var']});" ) return row