implement dynamic danger zone analysis and target data list
This commit is contained in:
parent
eb029ac656
commit
217ec83e6d
5 changed files with 219 additions and 40 deletions
|
|
@ -49,8 +49,8 @@ class BackupPane(Container):
|
||||||
yield Label("", id="bp_lbl_backup")
|
yield Label("", id="bp_lbl_backup")
|
||||||
with Vertical(id="bp_actions"):
|
with Vertical(id="bp_actions"):
|
||||||
yield Button("Backup Server Profile", id="bp_backup_profile", disabled=True)
|
yield Button("Backup Server Profile", id="bp_backup_profile", disabled=True)
|
||||||
yield Button("Backup Channel Messages", id="bp_backup_msgs", disabled=True)
|
yield Button("Backup Channel Messages", id="bp_backup_msgs", disabled=True, variant="primary")
|
||||||
yield Button("Update & Sync Backup", id="bp_backup_sync", disabled=True)
|
yield Button("Update & Sync Backup", id="bp_backup_sync", disabled=True, variant="success")
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self._validate()
|
self._validate()
|
||||||
|
|
@ -220,7 +220,7 @@ class BackupPane(Container):
|
||||||
modal_prog.set_status(f"Awaiting Confirmation to backup [bold]{len(selected_channels)}[/bold] channels...")
|
modal_prog.set_status(f"Awaiting Confirmation to backup [bold]{len(selected_channels)}[/bold] channels...")
|
||||||
modal_prog.show_info(f"[cyan]{msg}[/cyan]", f"Targets: {', '.join([c.name for c in selected_channels[:3]])}{'...' if len(selected_channels) > 3 else ''}")
|
modal_prog.show_info(f"[cyan]{msg}[/cyan]", f"Targets: {', '.join([c.name for c in selected_channels[:3]])}{'...' if len(selected_channels) > 3 else ''}")
|
||||||
|
|
||||||
choice = await modal_prog.phase_wait_confirm()
|
choice = await modal_prog.phase_wait_confirm(btn_start_label="Start Channel Backup", show_id=False)
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal_prog.dismiss()
|
modal_prog.dismiss()
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ class ConfigSelectionScreen(Screen):
|
||||||
with VerticalScroll(id="config_list_container"):
|
with VerticalScroll(id="config_list_container"):
|
||||||
yield ListView(id="config_list")
|
yield ListView(id="config_list")
|
||||||
with Horizontal(id="config_sel_actions"):
|
with Horizontal(id="config_sel_actions"):
|
||||||
yield Button("Create New Config", id="btn_new_config", variant="success")
|
yield Button("New Config", id="btn_new_config", variant="success")
|
||||||
yield Button("Exit", id="btn_exit", variant="error")
|
yield Button("Exit", id="btn_exit", variant="error")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
|
|
@ -264,7 +264,7 @@ class ConfigScreen(Screen):
|
||||||
|
|
||||||
yield Rule()
|
yield Rule()
|
||||||
with Horizontal(id="cfg_actions"):
|
with Horizontal(id="cfg_actions"):
|
||||||
yield Button("Save Configuration", variant="primary", id="btn_save")
|
yield Button("Save Configuration", variant="success", id="btn_save")
|
||||||
yield Button("Back", id="btn_back")
|
yield Button("Back", id="btn_back")
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
||||||
|
|
@ -330,10 +330,10 @@ class ConfigScreen(Screen):
|
||||||
# ──────────────────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class ReaperApp(App):
|
class ReaperApp(App):
|
||||||
theme = "dracula"
|
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.push_screen(ConfigSelectionScreen())
|
self.push_screen(ConfigSelectionScreen())
|
||||||
|
self.theme = "dracula"
|
||||||
|
|
||||||
|
|
||||||
def run_disco_reaper_tui():
|
def run_disco_reaper_tui():
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,10 @@ class ProgressScreen(Screen[None]):
|
||||||
margin: 2 0;
|
margin: 2 0;
|
||||||
background: $surface;
|
background: $surface;
|
||||||
}
|
}
|
||||||
#prog_header { height: auto; margin-bottom: 1; dock: top; }
|
#prog_header { height: 3; margin-bottom: 1; dock: top; align: left middle; }
|
||||||
#prog_status { text-style: bold; width: 1fr; content-align: left middle; }
|
#prog_status { text-style: bold; width: 1fr; content-align: left middle; }
|
||||||
#prog_timer { text-style: bold; width: 20; content-align: right middle; color: yellow; }
|
#prog_loader { width: 10; height: 1; margin: 0 1; }
|
||||||
|
#prog_timer { text-style: bold; width: 15; content-align: right middle; color: yellow; }
|
||||||
|
|
||||||
#prog_stats {
|
#prog_stats {
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|
@ -61,7 +62,7 @@ class ProgressScreen(Screen[None]):
|
||||||
|
|
||||||
#prog_log { height: 1fr; margin-bottom: 1; border: solid $primary; }
|
#prog_log { height: 1fr; margin-bottom: 1; border: solid $primary; }
|
||||||
#live_log { height: 10; margin-bottom: 1; border: solid yellow; }
|
#live_log { height: 10; margin-bottom: 1; border: solid yellow; }
|
||||||
#prog_loader { margin-bottom: 1; }
|
|
||||||
#prog_bar_container { height: auto; width: 100%; align: center middle; }
|
#prog_bar_container { height: auto; width: 100%; align: center middle; }
|
||||||
#prog_bar { margin-bottom: 1; width: 80%; }
|
#prog_bar { margin-bottom: 1; width: 80%; }
|
||||||
|
|
||||||
|
|
@ -79,6 +80,7 @@ class ProgressScreen(Screen[None]):
|
||||||
with Container(id="prog_dialog"):
|
with Container(id="prog_dialog"):
|
||||||
with Horizontal(id="prog_header"):
|
with Horizontal(id="prog_header"):
|
||||||
yield Label("Operation Status...", id="prog_status")
|
yield Label("Operation Status...", id="prog_status")
|
||||||
|
yield LoadingIndicator(id="prog_loader")
|
||||||
yield Label("00:00", id="prog_timer")
|
yield Label("00:00", id="prog_timer")
|
||||||
|
|
||||||
with Horizontal(id="prog_stats"):
|
with Horizontal(id="prog_stats"):
|
||||||
|
|
@ -86,7 +88,6 @@ class ProgressScreen(Screen[None]):
|
||||||
yield Label("Threads: 0", id="stat_threads", classes="stat_label")
|
yield Label("Threads: 0", id="stat_threads", classes="stat_label")
|
||||||
yield Label("Files: 0", id="stat_files", classes="stat_label")
|
yield Label("Files: 0", id="stat_files", classes="stat_label")
|
||||||
|
|
||||||
yield LoadingIndicator(id="prog_loader")
|
|
||||||
|
|
||||||
with Vertical(id="info_container"):
|
with Vertical(id="info_container"):
|
||||||
yield Label("No previous migration data.", id="info_migration_status", classes="info_label")
|
yield Label("No previous migration data.", id="info_migration_status", classes="info_label")
|
||||||
|
|
@ -212,7 +213,14 @@ class ProgressScreen(Screen[None]):
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def phase_wait_confirm(self, show_continue: bool = False):
|
async def phase_wait_confirm(
|
||||||
|
self,
|
||||||
|
show_continue: bool = False,
|
||||||
|
show_id: bool = True,
|
||||||
|
btn_start_label: str = "Start from First",
|
||||||
|
btn_continue_label: str = "Continue Migration",
|
||||||
|
btn_id_label: str = "Start from ID"
|
||||||
|
):
|
||||||
"""Phase 2: Wait for user confirmation after analysis."""
|
"""Phase 2: Wait for user confirmation after analysis."""
|
||||||
try: self.query_one("#prog_loader", LoadingIndicator).display = False
|
try: self.query_one("#prog_loader", LoadingIndicator).display = False
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
@ -220,6 +228,14 @@ class ProgressScreen(Screen[None]):
|
||||||
try: self.query_one("#prog_bar", ProgressBar).display = False
|
try: self.query_one("#prog_bar", ProgressBar).display = False
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
|
# Update button labels
|
||||||
|
try: self.query_one("#btn_start_first", Button).label = btn_start_label
|
||||||
|
except Exception: pass
|
||||||
|
try: self.query_one("#btn_continue", Button).label = btn_continue_label
|
||||||
|
except Exception: pass
|
||||||
|
try: self.query_one("#btn_start_id", Button).label = btn_id_label
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
# Show confirmation buttons
|
# Show confirmation buttons
|
||||||
try: self.query_one("#prog_actions_row1", Horizontal).display = True
|
try: self.query_one("#prog_actions_row1", Horizontal).display = True
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
@ -231,7 +247,10 @@ class ProgressScreen(Screen[None]):
|
||||||
try: self.query_one("#btn_start_first", Button).disabled = False
|
try: self.query_one("#btn_start_first", Button).disabled = False
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
try: self.query_one("#btn_start_id", Button).disabled = False
|
try:
|
||||||
|
btn_id = self.query_one("#btn_start_id", Button)
|
||||||
|
btn_id.disabled = not show_id
|
||||||
|
btn_id.display = show_id
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -391,8 +410,8 @@ class OptionSelectModal(ModalScreen[list[str]]):
|
||||||
with Vertical(id="opt_dialog"):
|
with Vertical(id="opt_dialog"):
|
||||||
yield Label(self._title, id="opt_title")
|
yield Label(self._title, id="opt_title")
|
||||||
with Horizontal(id="opt_batch_buttons"):
|
with Horizontal(id="opt_batch_buttons"):
|
||||||
yield Button("Select All", id="btn_opt_all")
|
yield Button("Select All", id="btn_opt_all", flat=True)
|
||||||
yield Button("Deselect All", id="btn_opt_none")
|
yield Button("Deselect All", id="btn_opt_none", flat=True)
|
||||||
|
|
||||||
with Vertical(id="opt_scroll"):
|
with Vertical(id="opt_scroll"):
|
||||||
for opt_id, label in self._options:
|
for opt_id, label in self._options:
|
||||||
|
|
|
||||||
|
|
@ -130,7 +130,7 @@ class ModeScreen(Screen):
|
||||||
with Horizontal(id="bottom_actions"):
|
with Horizontal(id="bottom_actions"):
|
||||||
if mode == "backup_transfer":
|
if mode == "backup_transfer":
|
||||||
yield Button("Switch to Migrate ⇄", id="btn_switch", variant="primary")
|
yield Button("Switch to Migrate ⇄", id="btn_switch", variant="primary")
|
||||||
yield Button("Configuration", id="btn_config")
|
yield Button("Configuration", id="btn_config", variant="success")
|
||||||
yield Button("Exit", id="btn_exit", variant="error")
|
yield Button("Exit", id="btn_exit", variant="error")
|
||||||
|
|
||||||
yield Footer()
|
yield Footer()
|
||||||
|
|
|
||||||
|
|
@ -107,9 +107,9 @@ class ShuttlePane(Container):
|
||||||
with Vertical(id="sp_actions"):
|
with Vertical(id="sp_actions"):
|
||||||
yield Button("Clone Server Template", id="sp_clone", disabled=True)
|
yield Button("Clone Server Template", id="sp_clone", disabled=True)
|
||||||
yield Button("Sync Server Settings", id="sp_sync", disabled=True)
|
yield Button("Sync Server Settings", id="sp_sync", disabled=True)
|
||||||
yield Button("Migrate Message History", id="sp_messages", disabled=True)
|
yield Button("Migrate Message History", id="sp_messages", disabled=True, variant="primary")
|
||||||
yield Rule()
|
yield Rule()
|
||||||
yield Button("Danger Zone ⚠", id="sp_danger", variant="error", disabled=True)
|
yield Button("Danger Zone ⚠", id="sp_danger", variant="error", disabled=True, flat=True)
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self._rebuild_engine()
|
self._rebuild_engine()
|
||||||
|
|
@ -281,9 +281,9 @@ class ShuttlePane(Container):
|
||||||
|
|
||||||
def _open_clone_menu(self):
|
def _open_clone_menu(self):
|
||||||
options = [
|
options = [
|
||||||
("sub_clone_roles", "Clone Roles & Role Permissions"),
|
("sub_clone_roles", "Roles & Role Permissions"),
|
||||||
("sub_clone_channels", "Clone Channels & Categories"),
|
("sub_clone_channels", "Server Structure [Channels & Categories]"),
|
||||||
("sub_sync_perms", "Sync Channel & Category Permissions"),
|
("sub_sync_perms", "Channel & Category Permissions"),
|
||||||
]
|
]
|
||||||
def on_result(choices):
|
def on_result(choices):
|
||||||
if choices:
|
if choices:
|
||||||
|
|
@ -316,7 +316,11 @@ class ShuttlePane(Container):
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
try:
|
try:
|
||||||
modal.set_status(f"Awaiting Confirmation for {len(selections)} Operations...")
|
modal.set_status(f"Awaiting Confirmation for {len(selections)} Operations...")
|
||||||
choice = await modal.phase_wait_confirm()
|
choice = await modal.phase_wait_confirm(
|
||||||
|
btn_start_label="Start Cloning",
|
||||||
|
btn_id_label="Force Copy",
|
||||||
|
show_id=True
|
||||||
|
)
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
self._open_clone_menu()
|
self._open_clone_menu()
|
||||||
|
|
@ -326,22 +330,25 @@ class ShuttlePane(Container):
|
||||||
self.app.switch_screen("config_selection")
|
self.app.switch_screen("config_selection")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
force_mode = (choice == "btn_start_id")
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||||
modal.phase_progress()
|
modal.phase_progress()
|
||||||
await self.engine.start_connections()
|
await self.engine.start_connections()
|
||||||
self.engine.is_running = True
|
self.engine.is_running = True
|
||||||
|
|
||||||
|
results = {}
|
||||||
for sel in selections:
|
for sel in selections:
|
||||||
if not self.engine.is_running: break
|
if not self.engine.is_running: break
|
||||||
if sel == "sub_clone_roles":
|
if sel == "sub_clone_roles":
|
||||||
await self._logic_clone_roles(modal)
|
results["roles"] = await self._logic_clone_roles(modal, force=force_mode)
|
||||||
elif sel == "sub_clone_channels":
|
elif sel == "sub_clone_channels":
|
||||||
await self._logic_clone_channels(modal)
|
results["channels"] = await self._logic_clone_channels(modal, force=force_mode)
|
||||||
elif sel == "sub_sync_perms":
|
elif sel == "sub_sync_perms":
|
||||||
await self._logic_sync_permissions(modal)
|
results["perms"] = await self._logic_sync_permissions(modal)
|
||||||
|
|
||||||
modal.phase_report("Clone Template Complete")
|
modal.phase_report("Clone Template Complete")
|
||||||
modal.write("[bold green]All selected cloning operations finished.[/bold green]")
|
report = self._format_clone_report(results)
|
||||||
|
modal.write(report)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
modal.write(f"[bold red]Error: {e}[/bold red]")
|
modal.write(f"[bold red]Error: {e}[/bold red]")
|
||||||
modal.phase_report("Batch Operation", "error")
|
modal.phase_report("Batch Operation", "error")
|
||||||
|
|
@ -356,7 +363,11 @@ class ShuttlePane(Container):
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
try:
|
try:
|
||||||
modal.set_status("Awaiting Confirmation to Sync Server Settings...")
|
modal.set_status("Awaiting Confirmation to Sync Server Settings...")
|
||||||
choice = await modal.phase_wait_confirm()
|
choice = await modal.phase_wait_confirm(
|
||||||
|
btn_start_label="Start Syncing",
|
||||||
|
btn_id_label="Force Sync",
|
||||||
|
show_id=True
|
||||||
|
)
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
self._open_sync_menu()
|
self._open_sync_menu()
|
||||||
|
|
@ -366,27 +377,30 @@ class ShuttlePane(Container):
|
||||||
self.app.switch_screen("config_selection")
|
self.app.switch_screen("config_selection")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
force_mode = (choice == "btn_start_id")
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||||
modal.phase_progress()
|
modal.phase_progress()
|
||||||
await self.engine.start_connections()
|
await self.engine.start_connections()
|
||||||
self.engine.is_running = True
|
self.engine.is_running = True
|
||||||
|
|
||||||
|
results = {}
|
||||||
# Separate asset sync from metadata sync
|
# Separate asset sync from metadata sync
|
||||||
asset_types = []
|
asset_types = []
|
||||||
if "sub_emoji" in selections: asset_types.append("Emoji")
|
if "sub_emoji" in selections: asset_types.append("Emoji")
|
||||||
if "sub_sticker" in selections: asset_types.append("Sticker")
|
if "sub_sticker" in selections: asset_types.append("Sticker")
|
||||||
if asset_types:
|
if asset_types:
|
||||||
await self._logic_copy_assets(modal, asset_types)
|
results["assets"] = await self._logic_copy_assets(modal, asset_types, force=force_mode)
|
||||||
|
|
||||||
meta_comps = []
|
meta_comps = []
|
||||||
if "sub_name" in selections: meta_comps.append("name")
|
if "sub_name" in selections: meta_comps.append("name")
|
||||||
if "sub_icon" in selections: meta_comps.append("icon")
|
if "sub_icon" in selections: meta_comps.append("icon")
|
||||||
if "sub_banner" in selections: meta_comps.append("banner")
|
if "sub_banner" in selections: meta_comps.append("banner")
|
||||||
if meta_comps:
|
if meta_comps:
|
||||||
await self._logic_sync_metadata(modal, meta_comps)
|
results["metadata"] = await self._logic_sync_metadata(modal, meta_comps)
|
||||||
|
|
||||||
modal.phase_report("Sync Settings Complete")
|
modal.phase_report("Sync Settings Complete")
|
||||||
modal.write("[bold green]All selected synchronization operations finished.[/bold green]")
|
report = self._format_sync_report(results)
|
||||||
|
modal.write(report)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
modal.write(f"[bold red]Error: {e}[/bold red]")
|
modal.write(f"[bold red]Error: {e}[/bold red]")
|
||||||
modal.phase_report("Batch Operation", "error")
|
modal.phase_report("Batch Operation", "error")
|
||||||
|
|
@ -396,7 +410,7 @@ class ShuttlePane(Container):
|
||||||
|
|
||||||
# ── logic blocks (internal) ────────────────────────────────────────
|
# ── logic blocks (internal) ────────────────────────────────────────
|
||||||
|
|
||||||
async def _logic_clone_channels(self, modal: ProgressScreen):
|
async def _logic_clone_channels(self, modal: ProgressScreen, force: bool = False):
|
||||||
if self.target_platform == "fluxer":
|
if self.target_platform == "fluxer":
|
||||||
from src.fluxer.clone_server import sync_channel_state, migrate_channels
|
from src.fluxer.clone_server import sync_channel_state, migrate_channels
|
||||||
else:
|
else:
|
||||||
|
|
@ -412,7 +426,7 @@ class ShuttlePane(Container):
|
||||||
modal.set_status(f"[{color}]{status}: {item_name}[/{color}]")
|
modal.set_status(f"[{color}]{status}: {item_name}[/{color}]")
|
||||||
modal.set_progress(current, total)
|
modal.set_progress(current, total)
|
||||||
|
|
||||||
cloned_info = await migrate_channels(self.engine, progress_callback=update_progress, force=False)
|
cloned_info = await migrate_channels(self.engine, progress_callback=update_progress, force=force)
|
||||||
if cloned_info and cloned_info.get("structure"):
|
if cloned_info and cloned_info.get("structure"):
|
||||||
lines = ["Successfully cloned channels and categories:"]
|
lines = ["Successfully cloned channels and categories:"]
|
||||||
cats = sorted(cloned_info["structure"].keys(), key=lambda x: (x == "No Category", x))
|
cats = sorted(cloned_info["structure"].keys(), key=lambda x: (x == "No Category", x))
|
||||||
|
|
@ -422,8 +436,9 @@ class ShuttlePane(Container):
|
||||||
lines.append(f"- **{cat_name}**")
|
lines.append(f"- **{cat_name}**")
|
||||||
for n in sorted(ch_names): lines.append(f" - {n}")
|
for n in sorted(ch_names): lines.append(f" - {n}")
|
||||||
await log_audit_event(self.engine, "Channels Cloned", "\n".join(lines))
|
await log_audit_event(self.engine, "Channels Cloned", "\n".join(lines))
|
||||||
|
return cloned_info
|
||||||
|
|
||||||
async def _logic_clone_roles(self, modal: ProgressScreen):
|
async def _logic_clone_roles(self, modal: ProgressScreen, force: bool = False):
|
||||||
roles_mod = fluxer_roles if self.target_platform == "fluxer" else stoat_roles
|
roles_mod = fluxer_roles if self.target_platform == "fluxer" else stoat_roles
|
||||||
modal.set_status("Processing Roles...")
|
modal.set_status("Processing Roles...")
|
||||||
await roles_mod.sync_roles_state(self.engine)
|
await roles_mod.sync_roles_state(self.engine)
|
||||||
|
|
@ -432,9 +447,10 @@ class ShuttlePane(Container):
|
||||||
modal.set_status(f"[cyan]Copying Role: {name}[/cyan]")
|
modal.set_status(f"[cyan]Copying Role: {name}[/cyan]")
|
||||||
modal.set_progress(current, total)
|
modal.set_progress(current, total)
|
||||||
|
|
||||||
cloned = await roles_mod.migrate_roles(self.engine, progress_callback=update, force=False)
|
cloned = await roles_mod.migrate_roles(self.engine, progress_callback=update, force=force)
|
||||||
if cloned:
|
if cloned:
|
||||||
await log_audit_event(self.engine, "Roles Cloned", "Successfully cloned roles:\n" + "\n".join(f"- {r}" for r in cloned))
|
await log_audit_event(self.engine, "Roles Cloned", "Successfully cloned roles:\n" + "\n".join(f"- {r}" for r in cloned))
|
||||||
|
return cloned
|
||||||
|
|
||||||
async def _logic_sync_permissions(self, modal: ProgressScreen):
|
async def _logic_sync_permissions(self, modal: ProgressScreen):
|
||||||
roles_mod = fluxer_roles if self.target_platform == "fluxer" else stoat_roles
|
roles_mod = fluxer_roles if self.target_platform == "fluxer" else stoat_roles
|
||||||
|
|
@ -454,8 +470,9 @@ class ShuttlePane(Container):
|
||||||
lines.append(f"- **{cat_name}**")
|
lines.append(f"- **{cat_name}**")
|
||||||
for n in sorted(ch_names): lines.append(f" - {n}")
|
for n in sorted(ch_names): lines.append(f" - {n}")
|
||||||
await log_audit_event(self.engine, "Permissions Synced", "\n".join(lines))
|
await log_audit_event(self.engine, "Permissions Synced", "\n".join(lines))
|
||||||
|
return synced
|
||||||
|
|
||||||
async def _logic_copy_assets(self, modal: ProgressScreen, types_to_include: list[str]):
|
async def _logic_copy_assets(self, modal: ProgressScreen, types_to_include: list[str], force: bool = False):
|
||||||
asset_mod = stoat_emoji_stickers if self.target_platform == "stoat" else fluxer_emoji_stickers
|
asset_mod = stoat_emoji_stickers if self.target_platform == "stoat" else fluxer_emoji_stickers
|
||||||
modal.set_status("Processing Assets...")
|
modal.set_status("Processing Assets...")
|
||||||
await asset_mod.sync_assets_state(self.engine)
|
await asset_mod.sync_assets_state(self.engine)
|
||||||
|
|
@ -464,7 +481,7 @@ class ShuttlePane(Container):
|
||||||
modal.set_status(f"[cyan]Copying {item_type}: {name}[/cyan]")
|
modal.set_status(f"[cyan]Copying {item_type}: {name}[/cyan]")
|
||||||
modal.set_progress(current, total)
|
modal.set_progress(current, total)
|
||||||
|
|
||||||
cloned = await asset_mod.migrate_emojis(self.engine, progress_callback=update, types_to_include=types_to_include, force=False)
|
cloned = await asset_mod.migrate_emojis(self.engine, progress_callback=update, types_to_include=types_to_include, force=force)
|
||||||
if cloned and (cloned.get("Emoji") or cloned.get("Sticker")):
|
if cloned and (cloned.get("Emoji") or cloned.get("Sticker")):
|
||||||
lines = []
|
lines = []
|
||||||
if cloned.get("Emoji"):
|
if cloned.get("Emoji"):
|
||||||
|
|
@ -472,6 +489,7 @@ class ShuttlePane(Container):
|
||||||
if cloned.get("Sticker"):
|
if cloned.get("Sticker"):
|
||||||
lines.append("Stickers cloned:"); lines.extend([f"- {n}" for n in cloned["Sticker"]])
|
lines.append("Stickers cloned:"); lines.extend([f"- {n}" for n in cloned["Sticker"]])
|
||||||
await log_audit_event(self.engine, "Assets Cloned", "\n".join(lines))
|
await log_audit_event(self.engine, "Assets Cloned", "\n".join(lines))
|
||||||
|
return cloned
|
||||||
|
|
||||||
async def _logic_sync_metadata(self, modal: ProgressScreen, components: list[str]):
|
async def _logic_sync_metadata(self, modal: ProgressScreen, components: list[str]):
|
||||||
meta_mod = fluxer_metadata if self.target_platform == "fluxer" else stoat_metadata
|
meta_mod = fluxer_metadata if self.target_platform == "fluxer" else stoat_metadata
|
||||||
|
|
@ -496,6 +514,67 @@ class ShuttlePane(Container):
|
||||||
ext = "gif" if cloned["banner"].startswith(b"GIF") else "png"
|
ext = "gif" if cloned["banner"].startswith(b"GIF") else "png"
|
||||||
files.append({"filename": f"banner.{ext}", "data": cloned["banner"]})
|
files.append({"filename": f"banner.{ext}", "data": cloned["banner"]})
|
||||||
await log_audit_event(self.engine, "Profile Synced", "\n".join(lines), files=files)
|
await log_audit_event(self.engine, "Profile Synced", "\n".join(lines), files=files)
|
||||||
|
return cloned
|
||||||
|
|
||||||
|
# ── report formatting ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _format_sync_report(self, results: dict) -> str:
|
||||||
|
lines = ["[bold green]Synchronization Report[/bold green]\n"]
|
||||||
|
|
||||||
|
meta = results.get("metadata", {})
|
||||||
|
if meta:
|
||||||
|
lines.append("[bold cyan]Server Profile:[/bold cyan]")
|
||||||
|
if "name" in meta: lines.append(f"- Name: [white]{meta['name']}[/white]")
|
||||||
|
if "icon" in meta: lines.append("- Icon")
|
||||||
|
if "banner" in meta: lines.append("- Banner")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
assets = results.get("assets", {})
|
||||||
|
emojis = assets.get("Emoji", {})
|
||||||
|
if emojis:
|
||||||
|
lines.append("[bold cyan]Emojis:[/bold cyan]")
|
||||||
|
for name, eid in emojis.items():
|
||||||
|
lines.append(f"- {name} ([dim]{eid}[/dim])")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
stickers = assets.get("Sticker", {})
|
||||||
|
if stickers:
|
||||||
|
lines.append("[bold cyan]Stickers:[/bold cyan]")
|
||||||
|
for name, sid in stickers.items():
|
||||||
|
lines.append(f"- {name} ([dim]{sid}[/dim])")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if not meta and not emojis and not stickers:
|
||||||
|
lines.append("[yellow]No items were synchronized.[/yellow]")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
def _format_clone_report(self, results: dict) -> str:
|
||||||
|
lines = ["[bold green]Cloning Template Report[/bold green]\n"]
|
||||||
|
|
||||||
|
roles = results.get("roles", [])
|
||||||
|
if roles:
|
||||||
|
lines.append(f"[bold cyan]Roles ({len(roles)}):[/bold cyan]")
|
||||||
|
for r in sorted(roles): lines.append(f"- {r}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
channels = results.get("channels", {})
|
||||||
|
structure = channels.get("structure", {})
|
||||||
|
if structure:
|
||||||
|
lines.append("[bold cyan]Server Structure:[/bold cyan]")
|
||||||
|
cats = sorted(structure.keys(), key=lambda x: (x == "No Category", x))
|
||||||
|
for cat in cats:
|
||||||
|
chans = structure[cat]
|
||||||
|
if cat in channels.get("categories_created", []) or chans:
|
||||||
|
lines.append(f"[bold]{cat}[/bold]")
|
||||||
|
for ch in sorted(chans):
|
||||||
|
lines.append(f" - {ch}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
if not roles and not structure:
|
||||||
|
lines.append("[yellow]No items were cloned.[/yellow]")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
# ── (5) message migration ─────────────────────────────────────────────
|
# ── (5) message migration ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -602,7 +681,13 @@ class ShuttlePane(Container):
|
||||||
modal.set_status(f"Awaiting Confirmation to migrate Discord [cyan]#{source_channel.name}[/cyan] → {platform_name} [green]#{target_channel.get('name')}[/green]")
|
modal.set_status(f"Awaiting Confirmation to migrate Discord [cyan]#{source_channel.name}[/cyan] → {platform_name} [green]#{target_channel.get('name')}[/green]")
|
||||||
|
|
||||||
# Phase 2: Confirmation
|
# Phase 2: Confirmation
|
||||||
choice = await modal.phase_wait_confirm(show_continue=has_previous)
|
choice = await modal.phase_wait_confirm(
|
||||||
|
show_continue=has_previous,
|
||||||
|
show_id=True,
|
||||||
|
btn_start_label="Start Migration",
|
||||||
|
btn_continue_label="Continue Migration",
|
||||||
|
btn_id_label="Start from ID"
|
||||||
|
)
|
||||||
logger.info(f"User confirmation choice: {choice}")
|
logger.info(f"User confirmation choice: {choice}")
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
|
|
@ -704,16 +789,91 @@ class ShuttlePane(Container):
|
||||||
self.run_batch_danger(selections)
|
self.run_batch_danger(selections)
|
||||||
self.app.push_screen(OptionSelectModal("⚠ DANGER ZONE ⚠", options), on_result)
|
self.app.push_screen(OptionSelectModal("⚠ DANGER ZONE ⚠", options), on_result)
|
||||||
|
|
||||||
|
async def _analyze_danger_zone(self, selections: list[str]) -> dict:
|
||||||
|
"""Analyzes the target server to find items that will be deleted."""
|
||||||
|
results = {}
|
||||||
|
writer = self.engine.fluxer_writer if self.target_platform == "fluxer" else self.engine.writer
|
||||||
|
|
||||||
|
for sel in selections:
|
||||||
|
if sel == "dz_del_channels":
|
||||||
|
chans = await writer.get_channels()
|
||||||
|
# filter reaper logs
|
||||||
|
chans = [c for c in chans if str(c.get("name", "")).lower() not in ["reaper-logs", "reaper_logs"]]
|
||||||
|
results["channels"] = [c.get("name", "Unknown") for c in chans]
|
||||||
|
|
||||||
|
elif sel == "dz_del_roles":
|
||||||
|
roles = []
|
||||||
|
if self.target_platform == "fluxer":
|
||||||
|
role_data = await writer.client.get_guild_roles(writer.community_id)
|
||||||
|
roles = [r.get("name", "Unknown") for r in role_data if not r.get("managed") and r.get("name") != "@everyone"]
|
||||||
|
else:
|
||||||
|
server = await writer._get_server()
|
||||||
|
# Stoat roles are in a dict .roles
|
||||||
|
for r_id, role in server.roles.items():
|
||||||
|
if str(r_id) != writer.community_id:
|
||||||
|
roles.append(getattr(role, "name", "Unknown Role"))
|
||||||
|
results["roles"] = roles
|
||||||
|
|
||||||
|
elif sel == "dz_del_assets":
|
||||||
|
assets = []
|
||||||
|
if self.target_platform == "fluxer":
|
||||||
|
emojis = await writer.client.get_guild_emojis(writer.community_id)
|
||||||
|
stickers = await writer.client.get_guild_stickers(writer.community_id)
|
||||||
|
assets.extend([e.get("name", "Unknown Emoji") for e in emojis])
|
||||||
|
assets.extend([s.get("name", "Unknown Sticker") for s in stickers])
|
||||||
|
else:
|
||||||
|
server = await writer._get_server()
|
||||||
|
emojis = await server.fetch_emojis()
|
||||||
|
assets.extend([getattr(e, "name", "Unknown Emoji") for e in emojis])
|
||||||
|
results["assets"] = assets
|
||||||
|
|
||||||
|
elif sel == "dz_reset_perms":
|
||||||
|
# For reset perms, we just list channels affected.
|
||||||
|
if "channels" not in results:
|
||||||
|
chans = await writer.get_channels()
|
||||||
|
chans = [c for c in chans if str(c.get("name", "")).lower() not in ["reaper-logs", "reaper_logs"]]
|
||||||
|
results["channels_perms"] = [c.get("name", "Unknown") for c in chans]
|
||||||
|
else:
|
||||||
|
results["channels_perms"] = results["channels"]
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
@work(exclusive=True)
|
@work(exclusive=True)
|
||||||
async def run_batch_danger(self, selections: list[str]) -> None:
|
async def run_batch_danger(self, selections: list[str]) -> None:
|
||||||
modal = ProgressScreen()
|
modal = ProgressScreen()
|
||||||
self.app.push_screen(modal)
|
self.app.push_screen(modal)
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
try:
|
try:
|
||||||
|
modal.set_status("Analyzing Target Server...")
|
||||||
|
modal.write("[bold cyan]Analyzing target server for destructive impact...[/bold cyan]")
|
||||||
|
|
||||||
|
await self.engine.start_target_only()
|
||||||
|
analysis = await self._analyze_danger_zone(selections)
|
||||||
|
|
||||||
modal.set_status(f"Awaiting Confirmation for {len(selections)} Destructive Operations...")
|
modal.set_status(f"Awaiting Confirmation for {len(selections)} Destructive Operations...")
|
||||||
modal.write("[bold red]WARNING: THIS WILL DELETE DATA PERMANENTLY! MUST PROCEED TO CONTINUE.[/bold red]")
|
modal.write("[bold red]WARNING: THIS WILL DELETE DATA PERMANENTLY! MUST PROCEED TO CONTINUE.[/bold red]")
|
||||||
|
modal.write("")
|
||||||
|
modal.write("[bold cyan]Scheduled for Destruction/Reset:[/bold cyan]")
|
||||||
|
|
||||||
choice = await modal.phase_wait_confirm()
|
if "channels" in analysis:
|
||||||
|
names = analysis["channels"]
|
||||||
|
modal.write(f"- [red]Channels ({len(names)}):[/red] {', '.join(names[:10])}{'...' if len(names) > 10 else ''}")
|
||||||
|
|
||||||
|
if "roles" in analysis:
|
||||||
|
names = analysis["roles"]
|
||||||
|
modal.write(f"- [red]Roles ({len(names)}):[/red] {', '.join(names[:10])}{'...' if len(names) > 10 else ''}")
|
||||||
|
|
||||||
|
if "assets" in analysis:
|
||||||
|
names = analysis["assets"]
|
||||||
|
modal.write(f"- [red]Emojis/Stickers ({len(names)}):[/red] {', '.join(names[:10])}{'...' if len(names) > 10 else ''}")
|
||||||
|
|
||||||
|
if "channels_perms" in analysis and "channels" not in analysis:
|
||||||
|
names = analysis["channels_perms"]
|
||||||
|
modal.write(f"- [red]Permission Resets on {len(names)} Channels[/red]")
|
||||||
|
|
||||||
|
modal.write("")
|
||||||
|
|
||||||
|
choice = await modal.phase_wait_confirm(btn_start_label="WIPE ALL DATA", show_id=False)
|
||||||
if choice == "btn_back":
|
if choice == "btn_back":
|
||||||
modal.dismiss()
|
modal.dismiss()
|
||||||
self._open_danger_menu()
|
self._open_danger_menu()
|
||||||
|
|
@ -726,7 +886,7 @@ class ShuttlePane(Container):
|
||||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||||
modal.phase_progress()
|
modal.phase_progress()
|
||||||
self.engine.is_running = True
|
self.engine.is_running = True
|
||||||
await self.engine.start_target_only()
|
# Connection already started above
|
||||||
|
|
||||||
for sel in selections:
|
for sel in selections:
|
||||||
if not self.engine.is_running: break
|
if not self.engine.is_running: break
|
||||||
|
|
@ -742,7 +902,7 @@ class ShuttlePane(Container):
|
||||||
modal.phase_report("Danger Zone Operations Complete")
|
modal.phase_report("Danger Zone Operations Complete")
|
||||||
modal.write("[bold green]All selected destructive operations finished.[/bold green]")
|
modal.write("[bold green]All selected destructive operations finished.[/bold green]")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
modal.write(f"[bold red]Error: {e}[/bold red]")
|
modal.write(f"[bold red]Error during analysis: {e}[/bold red]")
|
||||||
modal.phase_report("Danger Zone Batch", "error")
|
modal.phase_report("Danger Zone Batch", "error")
|
||||||
finally:
|
finally:
|
||||||
self.engine.is_running = False
|
self.engine.is_running = False
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue