Source code for exlab_wizard.ui.pages.problems

"""Problems tab (Frontend Spec §11).

Filter chips (Severity, Class, State, Scope), table of findings, and a
footer status bar (*"Showing N of M findings · Last audit: HH:MM:SS ·
Next refresh in 23s"*).

The override-and-allow-sync dialog (§11.5) is implemented here as well:
the operator picks a reason (10--500 chars after trim), an optional
expiry, ticks the acknowledgement checkbox, and submits.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any

from exlab_wizard.constants import Tier
from exlab_wizard.logging import get_logger
from exlab_wizard.ui.components import filter_chips

_log = get_logger(__name__)

# Problem classes per Backend §8.1.1-§8.1.5.
PROBLEM_CLASSES: tuple[str, ...] = (
    "Placeholder",
    "Illegal char",
    "Mode mismatch",
    "Orphan",
    "Missing field",
)

# Override reason length policy (Frontend §11.5).
OVERRIDE_REASON_MIN = 10
OVERRIDE_REASON_MAX = 500


[docs] @dataclass(frozen=True) class Finding: """One row in the Problems table.""" finding_id: str severity: str # "hard" | "soft" rule_class: str path: str matched_token: str run_label: str | None equipment: str detected_at: str state: str # "Active" | "Override active" | "Marked known" | "Synced under prior policy"
[docs] @dataclass class ProblemsPageState: """Mutable filter state.""" severity_chips: filter_chips.ChipState = field( default_factory=lambda: filter_chips.ChipState(active={"hard"}) ) class_chips: filter_chips.ChipState = field( default_factory=lambda: filter_chips.ChipState(active=set(PROBLEM_CLASSES)) ) state_chips: filter_chips.ChipState = field( default_factory=lambda: filter_chips.ChipState(active={"Active"}) ) scope: str = "all" # "all" | "<equipment_id>" | "staging" search: str = ""
[docs] def severity_chip_definitions() -> tuple[filter_chips.ChipDefinition, ...]: """Severity chips per Frontend §11.1.""" return ( filter_chips.ChipDefinition(chip_id="hard", label="Hard", default_on=True), filter_chips.ChipDefinition(chip_id="soft", label="Soft", default_on=False), )
[docs] def class_chip_definitions() -> tuple[filter_chips.ChipDefinition, ...]: """One chip per problem class (Frontend §11.1).""" return tuple( filter_chips.ChipDefinition(chip_id=name, label=name, default_on=True) for name in PROBLEM_CLASSES )
[docs] def state_chip_definitions() -> tuple[filter_chips.ChipDefinition, ...]: """State chips (Frontend §11.1).""" return ( filter_chips.ChipDefinition(chip_id="Active", label="Active", default_on=True), filter_chips.ChipDefinition( chip_id="Override active", label="Override active", default_on=False ), filter_chips.ChipDefinition(chip_id="Marked known", label="Marked known", default_on=False), filter_chips.ChipDefinition( chip_id="Synced under prior policy", label="Synced under prior policy", default_on=False, ), )
[docs] def filter_findings( findings: list[Finding], state: ProblemsPageState, ) -> list[Finding]: """Filter ``findings`` against the active chip / search state.""" out: list[Finding] = [] for finding in findings: if not filter_chips.is_active(state.severity_chips, finding.severity): continue if not filter_chips.is_active(state.class_chips, finding.rule_class): continue if not filter_chips.is_active(state.state_chips, finding.state): continue if state.scope != "all" and state.scope != "staging" and finding.equipment != state.scope: continue if state.search and state.search.lower() not in finding.path.lower(): continue out.append(finding) return out
[docs] def empty_state_text( state: ProblemsPageState, *, soft_findings_hidden_count: int = 0, ) -> str: """Compute the empty-state copy per Frontend §11.4.""" if not filter_chips.is_active(state.severity_chips, "soft") and soft_findings_hidden_count > 0: return ( f"No active problems. ({soft_findings_hidden_count} soft-tier findings " "hidden by filter.)" ) if state.search or state.scope != "all": return "No findings match the current filters." return "No active problems."
[docs] def validate_override_reason(reason: str) -> tuple[bool, str | None]: """Validate an override reason per Frontend §11.5. Returns ``(ok, error_message)``. ``ok=False`` when the reason is too short or too long after trimming; the message names the failed bound. """ trimmed = reason.strip() if len(trimmed) < OVERRIDE_REASON_MIN: return False, (f"Reason must be at least {OVERRIDE_REASON_MIN} characters.") if len(trimmed) > OVERRIDE_REASON_MAX: return False, (f"Reason must be at most {OVERRIDE_REASON_MAX} characters.") return True, None
[docs] def near_limit(reason: str) -> bool: """Return ``True`` when the counter should turn warning-tier (last 10).""" trimmed = reason.strip() return len(trimmed) >= OVERRIDE_REASON_MAX - 10
[docs] def render_problems_page( *, findings: list[Finding], state: ProblemsPageState | None = None, on_override: Callable[[str], None] | None = None, on_revoke_override: Callable[[str], None] | None = None, ) -> Any: """Render the Problems tab content.""" s = state or ProblemsPageState() visible = filter_findings(findings, s) payload = { "visible": visible, "total": len(findings), "empty_text": empty_state_text(s), "filter_chips": { "severity": severity_chip_definitions(), "class": class_chip_definitions(), "state": state_chip_definitions(), }, } try: from nicegui import ui except Exception: return payload container = ( ui.column().classes("w-full").props('data-testid="problems-table"').style("gap: 0.5rem;") ) with container: with ui.row().classes("items-center w-full").style("gap: 0.5rem;"): ui.label("Severity").style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "color: var(--color-muted); " "letter-spacing: 0.08em; " "text-transform: uppercase;" ) filter_chips.filter_chips( severity_chip_definitions(), state=s.severity_chips, ) with ui.row().classes("items-center w-full").style("gap: 0.5rem;"): ui.label("Class").style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "color: var(--color-muted); " "letter-spacing: 0.08em; " "text-transform: uppercase;" ) filter_chips.filter_chips( class_chip_definitions(), state=s.class_chips, ) with ui.row().classes("items-center w-full").style("gap: 0.5rem;"): ui.label("State").style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "color: var(--color-muted); " "letter-spacing: 0.08em; " "text-transform: uppercase;" ) filter_chips.filter_chips( state_chip_definitions(), state=s.state_chips, ) if not visible: ui.label(empty_state_text(s)).props('data-testid="problems-empty"').style( "color: var(--color-muted); font-family: var(--font-body); padding: 1rem 0;" ) else: for idx, finding in enumerate(visible): color_var = "--color-warning" if finding.severity == Tier.HARD else "--color-muted" with ( ui.row() .classes("items-center w-full") .props(f'data-testid="problems-row-{idx}"') .style( f"border-left: 4px solid var({color_var}); " "padding: 0.5rem 0.75rem; " "border-bottom: 1px solid var(--color-rule); " "gap: 0.5rem;" ) ): ui.label(finding.rule_class).style( "font-family: var(--font-mono); font-size: var(--text-xs);" ) ui.label(finding.path).style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "color: var(--color-body);" ) ui.label(finding.state).props(f'data-testid="problems-row-{idx}-state"').style( "font-family: var(--font-mono); font-size: var(--text-xs); " "color: var(--color-muted);" ) if ( finding.severity == Tier.HARD and finding.state == "Active" and on_override is not None ): ui.button( "Override and allow sync", on_click=lambda _evt, fid=finding.finding_id: on_override(fid), ).props(f'flat data-testid="problems-row-{idx}-override"') if finding.state == "Override active" and on_revoke_override is not None: ui.button( "Revoke override", on_click=lambda _evt, fid=finding.finding_id: on_revoke_override(fid), ).props(f'flat data-testid="problems-row-{idx}-revoke"') ui.label( f"Showing {len(visible)} of {len(findings)} findings · Last audit: --", ).style( "font-family: var(--font-mono); " "font-size: var(--text-xs); " "color: var(--color-muted); " "padding: 0.5rem 0;" ) return container