diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index 01c243c6..5a8a7173 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -19,6 +19,7 @@ from ..managers.container_manager import ContainerManager, ServiceStatus, Servic from ..managers.docling_manager import DoclingManager from ..utils.platform import RuntimeType from ..widgets.command_modal import CommandOutputModal +from ..widgets.flow_backup_warning_modal import FlowBackupWarningModal from ..widgets.diagnostics_notification import notify_with_diagnostics @@ -356,6 +357,16 @@ class MonitorScreen(Screen): """Upgrade services with progress updates.""" self.operation_in_progress = True try: + # Check for flow backups before upgrading + if self._check_flow_backups(): + # Show warning modal and wait for user decision + should_continue = await self.app.push_screen_wait( + FlowBackupWarningModal(operation="upgrade") + ) + if not should_continue: + self.notify("Upgrade cancelled", severity="information") + return + # Show command output in modal dialog command_generator = self.container_manager.upgrade_services() modal = CommandOutputModal( @@ -371,6 +382,16 @@ class MonitorScreen(Screen): """Reset services with progress updates.""" self.operation_in_progress = True try: + # Check for flow backups before resetting + if self._check_flow_backups(): + # Show warning modal and wait for user decision + should_continue = await self.app.push_screen_wait( + FlowBackupWarningModal(operation="reset") + ) + if not should_continue: + self.notify("Reset cancelled", severity="information") + return + # Show command output in modal dialog command_generator = self.container_manager.reset_services() modal = CommandOutputModal( @@ -382,6 +403,20 @@ class MonitorScreen(Screen): finally: self.operation_in_progress = False + def _check_flow_backups(self) -> bool: + """Check if there are any flow backups in ./flows/backup directory.""" + from pathlib import Path + backup_dir = Path("flows/backup") + if not backup_dir.exists(): + return False + + try: + # Check if there are any .json files in the backup directory + backup_files = list(backup_dir.glob("*.json")) + return len(backup_files) > 0 + except Exception: + return False + async def _start_docling_serve(self) -> None: """Start docling serve.""" self.operation_in_progress = True diff --git a/src/tui/screens/welcome.py b/src/tui/screens/welcome.py index 64ad888a..815122f9 100644 --- a/src/tui/screens/welcome.py +++ b/src/tui/screens/welcome.py @@ -34,6 +34,7 @@ class WelcomeScreen(Screen): self.has_oauth_config = False self.default_button_id = "basic-setup-btn" self._state_checked = False + self.has_flow_backups = False # Check if .env file exists self.has_env_file = self.env_manager.env_file.exists() @@ -45,6 +46,9 @@ class WelcomeScreen(Screen): self.has_oauth_config = bool(os.getenv("GOOGLE_OAUTH_CLIENT_ID")) or bool( os.getenv("MICROSOFT_GRAPH_OAUTH_CLIENT_ID") ) + + # Check for flow backups + self.has_flow_backups = self._check_flow_backups() def compose(self) -> ComposeResult: """Create the welcome screen layout.""" @@ -61,6 +65,19 @@ class WelcomeScreen(Screen): ) yield Footer() + def _check_flow_backups(self) -> bool: + """Check if there are any flow backups in ./flows/backup directory.""" + backup_dir = Path("flows/backup") + if not backup_dir.exists(): + return False + + try: + # Check if there are any .json files in the backup directory + backup_files = list(backup_dir.glob("*.json")) + return len(backup_files) > 0 + except Exception: + return False + def _detect_services_sync(self) -> None: """Synchronously detect if services are running.""" if not self.container_manager.is_available(): diff --git a/src/tui/widgets/__init__.py b/src/tui/widgets/__init__.py index a3856bcd..55ff60f5 100644 --- a/src/tui/widgets/__init__.py +++ b/src/tui/widgets/__init__.py @@ -1,3 +1,7 @@ """Widgets for OpenRAG TUI.""" -# Made with Bob +from .flow_backup_warning_modal import FlowBackupWarningModal + +__all__ = ["FlowBackupWarningModal"] + +# Made with Bob \ No newline at end of file diff --git a/src/tui/widgets/flow_backup_warning_modal.py b/src/tui/widgets/flow_backup_warning_modal.py new file mode 100644 index 00000000..b13bf69b --- /dev/null +++ b/src/tui/widgets/flow_backup_warning_modal.py @@ -0,0 +1,109 @@ +"""Flow backup warning modal for OpenRAG TUI.""" + +from textual.app import ComposeResult +from textual.containers import Container, Horizontal +from textual.screen import ModalScreen +from textual.widgets import Button, Static, Label + + +class FlowBackupWarningModal(ModalScreen[bool]): + """Modal dialog to warn about flow backups before upgrade/reset.""" + + DEFAULT_CSS = """ + FlowBackupWarningModal { + align: center middle; + } + + #dialog { + width: 70; + height: auto; + border: solid #3f3f46; + background: #27272a; + padding: 0; + } + + #title { + background: #3f3f46; + color: #fafafa; + padding: 1 2; + text-align: center; + width: 100%; + text-style: bold; + } + + #message { + padding: 2; + color: #fafafa; + text-align: center; + } + + #button-row { + width: 100%; + height: auto; + align: center middle; + padding: 1; + margin-top: 1; + } + + #button-row Button { + margin: 0 1; + min-width: 16; + background: #27272a; + color: #fafafa; + border: round #52525b; + text-style: none; + tint: transparent 0%; + } + + #button-row Button:hover { + background: #27272a !important; + color: #fafafa !important; + border: round #52525b; + tint: transparent 0%; + text-style: none; + } + + #button-row Button:focus { + background: #27272a !important; + color: #fafafa !important; + border: round #ec4899; + tint: transparent 0%; + text-style: none; + } + """ + + def __init__(self, operation: str = "upgrade"): + """Initialize the warning modal. + + Args: + operation: The operation being performed ("upgrade" or "reset") + """ + super().__init__() + self.operation = operation + + def compose(self) -> ComposeResult: + """Create the modal dialog layout.""" + with Container(id="dialog"): + yield Label("⚠ Flow Backups Detected", id="title") + yield Static( + f"Flow backups found in ./flows/backup\n\n" + f"Proceeding with {self.operation} will reset custom flows to defaults.\n" + f"Your customizations are backed up and will need to be\n" + f"manually imported and upgraded to work with the latest version.\n\n" + f"Do you want to continue?", + id="message" + ) + with Horizontal(id="button-row"): + yield Button("Cancel", id="cancel-btn") + yield Button(f"Continue {self.operation.title()}", id="continue-btn") + + def on_mount(self) -> None: + """Focus the cancel button by default for safety.""" + self.query_one("#cancel-btn", Button).focus() + + def on_button_pressed(self, event: Button.Pressed) -> None: + """Handle button presses.""" + if event.button.id == "continue-btn": + self.dismiss(True) # User wants to continue + else: + self.dismiss(False) # User cancelled