diff --git a/src/tui/screens/monitor.py b/src/tui/screens/monitor.py index e0607dde..87f34cc8 100644 --- a/src/tui/screens/monitor.py +++ b/src/tui/screens/monitor.py @@ -2,6 +2,8 @@ import asyncio import re +import shutil +from pathlib import Path from typing import Literal, Any, Optional # Define button variant type @@ -20,6 +22,7 @@ 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.factory_reset_warning_modal import FactoryResetWarningModal from ..widgets.version_mismatch_warning_modal import VersionMismatchWarningModal from ..widgets.upgrade_instructions_modal import UpgradeInstructionsModal from ..widgets.diagnostics_notification import notify_with_diagnostics @@ -34,7 +37,7 @@ class MonitorScreen(Screen): ("s", "start", "Start Services"), ("t", "stop", "Stop Services"), ("u", "upgrade", "Upgrade"), - ("x", "reset", "Reset"), + ("x", "reset", "Factory Reset"), ("l", "logs", "View Logs"), ("g", "toggle_mode", "Toggle GPU/CPU"), ("j", "cursor_down", "Move Down"), @@ -63,13 +66,6 @@ class MonitorScreen(Screen): self._container_manager = self.app.container_manager return self._container_manager - def on_unmount(self) -> None: - """Clean up when the screen is unmounted.""" - if hasattr(self, "docling_manager"): - self.docling_manager.cleanup() - super().on_unmount() - self._follow_service = None - self._logs_buffer = [] def compose(self) -> ComposeResult: """Create the monitoring screen layout.""" @@ -164,6 +160,12 @@ class MonitorScreen(Screen): self.refresh_timer.stop() # Stop following logs if running self._stop_follow() + # Clean up docling manager + if hasattr(self, "docling_manager"): + self.docling_manager.cleanup() + super().on_unmount() + self._follow_service = None + self._logs_buffer = [] async def on_screen_resume(self) -> None: """Called when the screen is resumed (e.g., after a modal is closed).""" @@ -455,9 +457,17 @@ class MonitorScreen(Screen): self.operation_in_progress = False async def _reset_services(self) -> None: - """Reset services with progress updates.""" + """Factory reset: clear config and opensearch-data, then reset services.""" self.operation_in_progress = True try: + # Show factory reset warning modal first + should_continue = await self.app.push_screen_wait( + FactoryResetWarningModal() + ) + if not should_continue: + self.notify("Factory reset cancelled", severity="information") + return + # Check for flow backups before resetting if self._check_flow_backups(): # Show warning modal and wait for user decision @@ -465,13 +475,40 @@ class MonitorScreen(Screen): FlowBackupWarningModal(operation="reset") ) if not should_continue: - self.notify("Reset cancelled", severity="information") + self.notify("Factory reset cancelled", severity="information") return + # Clear config, opensearch-data folders, and conversations.json + try: + config_path = Path("config") + opensearch_data_path = Path("opensearch-data") + conversations_file = Path("conversations.json") + + if config_path.exists(): + shutil.rmtree(config_path) + # Recreate empty config directory + config_path.mkdir(parents=True, exist_ok=True) + + if opensearch_data_path.exists(): + shutil.rmtree(opensearch_data_path) + # Recreate empty opensearch-data directory + opensearch_data_path.mkdir(parents=True, exist_ok=True) + + if conversations_file.exists(): + conversations_file.unlink() + + except Exception as e: + self.notify( + f"Error clearing folders: {str(e)}", + severity="error", + timeout=10, + ) + return + # Show command output in modal dialog command_generator = self.container_manager.reset_services() modal = CommandOutputModal( - "Resetting Services", + "Factory Resetting Services", command_generator, on_complete=None, # We'll refresh in on_screen_resume instead ) @@ -774,7 +811,7 @@ class MonitorScreen(Screen): controls.mount( Button("Upgrade", variant="warning", id=f"upgrade-btn{suffix}") ) - controls.mount(Button("Reset", variant="error", id=f"reset-btn{suffix}")) + controls.mount(Button("Factory Reset", variant="error", id=f"reset-btn{suffix}")) except Exception as e: notify_with_diagnostics( diff --git a/src/tui/widgets/factory_reset_warning_modal.py b/src/tui/widgets/factory_reset_warning_modal.py new file mode 100644 index 00000000..dba6aea4 --- /dev/null +++ b/src/tui/widgets/factory_reset_warning_modal.py @@ -0,0 +1,102 @@ +"""Factory reset 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 FactoryResetWarningModal(ModalScreen[bool]): + """Modal dialog to warn about factory reset consequences.""" + + DEFAULT_CSS = """ + FactoryResetWarningModal { + align: center middle; + } + + #dialog { + width: 70; + height: auto; + border: solid #3f3f46; + background: #27272a; + padding: 0; + } + + #title { + background: #dc2626; + 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 compose(self) -> ComposeResult: + """Create the modal dialog layout.""" + with Container(id="dialog"): + yield Label("⚠ Factory Reset Warning", id="title") + yield Static( + "This action will permanently delete:\n\n" + "• All ingested knowledge and documents\n" + "• All conversation history\n" + "• All provider settings and configuration\n\n" + "This cannot be undone.\n\n" + "Do you want to continue?", + id="message" + ) + with Horizontal(id="button-row"): + yield Button("Cancel", id="cancel-btn") + yield Button("Factory Reset", id="continue-btn", variant="error") + + 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 +