"""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