127 lines
4.2 KiB
Python
127 lines
4.2 KiB
Python
"""Recovery modal screen for the CLI board."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from textual.app import ComposeResult
|
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
from textual.screen import ModalScreen
|
|
from textual.widgets import Button, Label, Static
|
|
|
|
from opc.plugins.cli_board.services.recovery import RecoveryStatus
|
|
|
|
|
|
@dataclass
|
|
class RecoveryAction:
|
|
action: str # "resume" | "cancel" | "dismiss"
|
|
parent_task_id: str = ""
|
|
|
|
|
|
class RecoveryScreen(ModalScreen[RecoveryAction | None]):
|
|
"""Show interrupted company runtimes with resume/cancel options."""
|
|
|
|
DEFAULT_CSS = """
|
|
RecoveryScreen {
|
|
align: center middle;
|
|
}
|
|
|
|
.recovery-dialog {
|
|
width: 88;
|
|
max-width: 90%;
|
|
height: auto;
|
|
max-height: 80%;
|
|
border: solid $primary;
|
|
background: $surface;
|
|
padding: 1 2;
|
|
}
|
|
|
|
.recovery-title {
|
|
text-style: bold;
|
|
margin-bottom: 1;
|
|
}
|
|
|
|
.recovery-runtime {
|
|
margin-bottom: 1;
|
|
padding: 1;
|
|
border: round $secondary;
|
|
}
|
|
|
|
.recovery-actions {
|
|
align-horizontal: right;
|
|
height: auto;
|
|
margin-top: 1;
|
|
}
|
|
|
|
.recovery-empty {
|
|
color: $text-muted;
|
|
margin: 1;
|
|
}
|
|
"""
|
|
|
|
BINDINGS = [("escape", "dismiss_screen", "Close")]
|
|
|
|
def __init__(self, status: RecoveryStatus) -> None:
|
|
super().__init__()
|
|
self.status = status
|
|
|
|
def compose(self) -> ComposeResult:
|
|
with Vertical(classes="recovery-dialog"):
|
|
yield Static("Interrupted Runtimes", classes="recovery-title")
|
|
|
|
if not self.status.interrupted:
|
|
yield Static("No interrupted runtimes found.", classes="recovery-empty")
|
|
else:
|
|
with VerticalScroll():
|
|
for wf in self.status.interrupted:
|
|
with Vertical(classes="recovery-runtime"):
|
|
# Runtime header
|
|
active = wf.parent_task_id in set(self.status.active_recoveries)
|
|
status_label = " (recovering...)" if active else ""
|
|
yield Label(f"{wf.title}{status_label}")
|
|
yield Static(
|
|
f" Profile: {wf.profile or 'unknown'} "
|
|
f"Interrupted: {wf.interrupted_at[:19] if wf.interrupted_at else '?'}"
|
|
)
|
|
|
|
# Work-item summary
|
|
done = sum(1 for s in wf.work_items if s.status == "done")
|
|
total = len(wf.work_items)
|
|
failed = sum(1 for s in wf.work_items if s.interrupted)
|
|
yield Static(
|
|
f" Work items: {done}/{total} done, {failed} interrupted"
|
|
)
|
|
|
|
if not active:
|
|
with Horizontal():
|
|
yield Button(
|
|
"Resume",
|
|
id=f"resume-{wf.parent_task_id}",
|
|
variant="primary",
|
|
)
|
|
yield Button(
|
|
"Cancel",
|
|
id=f"cancel-{wf.parent_task_id}",
|
|
variant="error",
|
|
)
|
|
|
|
with Horizontal(classes="recovery-actions"):
|
|
yield Button("Close", id="close-recovery")
|
|
|
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
btn_id = event.button.id or ""
|
|
if btn_id == "close-recovery":
|
|
self.dismiss(None)
|
|
return
|
|
if btn_id.startswith("resume-"):
|
|
task_id = btn_id[len("resume-"):]
|
|
self.dismiss(RecoveryAction(action="resume", parent_task_id=task_id))
|
|
return
|
|
if btn_id.startswith("cancel-"):
|
|
task_id = btn_id[len("cancel-"):]
|
|
self.dismiss(RecoveryAction(action="cancel", parent_task_id=task_id))
|
|
return
|
|
|
|
def action_dismiss_screen(self) -> None:
|
|
self.dismiss(None)
|