diff --git a/src/core/configuration.py b/src/core/configuration.py index 156c949..bc6c5cd 100644 --- a/src/core/configuration.py +++ b/src/core/configuration.py @@ -6,7 +6,7 @@ from pydantic import BaseModel, Field class MigrationSettings(BaseModel): batch_size: int = Field(default=100) rate_limit_delay_seconds: int = Field(default=2) - log_level: str = Field(default="INFO") + log_level: str = Field(default="DEBUG") class AppConfig(BaseModel): discord_bot_token: str diff --git a/src/ui/backup_ops.py b/src/ui/backup_ops.py index 74a709c..e9f79ea 100644 --- a/src/ui/backup_ops.py +++ b/src/ui/backup_ops.py @@ -126,7 +126,7 @@ class BackupPane(Container): @work(exclusive=True) async def run_backup_profile(self) -> None: - modal = ProgressScreen() + modal = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal) await asyncio.sleep(0.1) modal.phase_progress() @@ -164,7 +164,7 @@ class BackupPane(Container): @work(exclusive=True) async def run_backup_messages(self) -> None: - modal_prog = ProgressScreen() + modal_prog = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal_prog) await asyncio.sleep(0.1) @@ -223,7 +223,7 @@ class BackupPane(Container): selected_channels = [c for c in eligible_channels if c.id in selected_ids] # Phase 2: Confirmation - modal_prog = ProgressScreen() # Re-instantiate to avoid Textual re-push UI freeze + modal_prog = ProgressScreen(log_level=self.config.migration.log_level) # Re-instantiate to avoid Textual re-push UI freeze self.app.push_screen(modal_prog) await asyncio.sleep(0.1) @@ -318,7 +318,7 @@ class BackupPane(Container): @work(exclusive=True) async def run_backup_sync(self) -> None: - modal_prog = ProgressScreen() + modal_prog = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal_prog) await asyncio.sleep(0.1) modal_prog.phase_progress() diff --git a/src/ui/modals.py b/src/ui/modals.py index 5d7e25c..5389e94 100644 --- a/src/ui/modals.py +++ b/src/ui/modals.py @@ -116,7 +116,9 @@ class ProgressScreen(Screen[None]): yield Button("Cancel", id="btn_cancel", variant="error") yield Footer() - def on_mount(self): + def __init__(self, log_level: str = "INFO", *args, **kwargs): + super().__init__(*args, **kwargs) + self.log_level = log_level.upper() self.confirm_future = None self.cancel_callback = None self.start_time = time.time() @@ -132,10 +134,19 @@ class ProgressScreen(Screen[None]): # Intercept Python logs and pipe to the #live_log self.log_handler = UILogHandler(self.write_live) + + # Set level based on config + level = getattr(logging, self.log_level, logging.INFO) + # Attach to root logger - logging.getLogger().addHandler(self.log_handler) + root_logger = logging.getLogger() + root_logger.addHandler(self.log_handler) + root_logger.setLevel(level) + # Also let's capture discord.py logs specifically if they aren't propagating - logging.getLogger("discord").addHandler(self.log_handler) + discord_logger = logging.getLogger("discord") + discord_logger.addHandler(self.log_handler) + discord_logger.setLevel(level) def on_unmount(self): # Detach log handler when UI is cleanly removed @@ -513,8 +524,13 @@ class ChannelPickerScreen(Screen[tuple]): def _render_pane(self, channels, categories, pane_id, prefix): cat_grouped: dict[int | None, list] = {} + seen_ids = set() # Prevent duplicate widget IDs for c in channels: cat_id = getattr(c, "category_id", None) if not isinstance(c, dict) else c.get("parent_id") + cid = c.get("id") if isinstance(c, dict) else c.id + if cid in seen_ids: + continue + seen_ids.add(cid) cat_grouped.setdefault(cat_id, []).append(c) with VerticalScroll(classes="split_pane", id=pane_id): diff --git a/src/ui/shuttle_ops.py b/src/ui/shuttle_ops.py index de7043a..6c53728 100644 --- a/src/ui/shuttle_ops.py +++ b/src/ui/shuttle_ops.py @@ -142,7 +142,8 @@ class ShuttlePane(Container): return f"Reaper-{self.cfg_name}" def _rebuild_engine(self): - self.engine = MigrationContext(self.config, self.target_platform) + source = "backup" if self.config.tool_mode == "backup_transfer" else "live" + self.engine = MigrationContext(self.config, self.target_platform, source_mode=source) # ── labels ──────────────────────────────────────────────────────────── @@ -339,7 +340,7 @@ class ShuttlePane(Container): @work(exclusive=True) async def run_batch_clone(self, selections: list[str]) -> None: - modal = ProgressScreen() + modal = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal) await asyncio.sleep(0.1) connections_started = False @@ -450,7 +451,7 @@ class ShuttlePane(Container): @work(exclusive=True) async def run_batch_sync(self, selections: list[str]) -> None: - modal = ProgressScreen() + modal = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal) await asyncio.sleep(0.1) try: @@ -680,7 +681,7 @@ class ShuttlePane(Container): migrate_mod = fluxer_migrate if self.target_platform == "fluxer" else stoat_migrate platform_name = self.target_platform.capitalize() - modal = ProgressScreen() + modal = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal) await asyncio.sleep(0.1) @@ -738,7 +739,7 @@ class ShuttlePane(Container): has_previous = bool(last_migrated) # Analyze - modal = ProgressScreen() + modal = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal) await asyncio.sleep(0.1) modal.set_status("Analyzing channel...") @@ -918,7 +919,7 @@ class ShuttlePane(Container): @work(exclusive=True) async def run_batch_danger(self, selections: list[str]) -> None: - modal = ProgressScreen() + modal = ProgressScreen(log_level=self.config.migration.log_level) self.app.push_screen(modal) await asyncio.sleep(0.1) target_started = False