complete backup structure refactor
This commit is contained in:
parent
bed2668ad2
commit
a30d02eb2e
7 changed files with 216 additions and 39 deletions
|
|
@ -289,7 +289,7 @@ Reconstructing the hierarchy requires specific pointer logic:
|
|||
|
||||
1. **Forums**:
|
||||
- Read `message_backup/{forum_id}/messages.json`.
|
||||
- Each message in this file is a `Thread_starter_message`.
|
||||
- Each message in this file is a `ThreadStarter`.
|
||||
- The `messageID` of the starter message *is usually* the same as the `thread_id`.
|
||||
- To load the full thread, open `message_backup/{forum_id}/{thread_id}/thread_messages.json`.
|
||||
2. **Regular Threads**:
|
||||
|
|
|
|||
|
|
@ -501,7 +501,6 @@ class BackupMessage:
|
|||
"Default": MessageType.default,
|
||||
"Reply": MessageType.reply,
|
||||
"ThreadStarter": MessageType.thread_starter_message,
|
||||
"Thread_starter_message": MessageType.thread_starter_message,
|
||||
"Forward": MessageType.default,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -78,15 +78,17 @@ class MigrationContext:
|
|||
|
||||
@staticmethod
|
||||
def _find_backup_path(server_id: str) -> Path:
|
||||
"""Searches workspace for a DISCORD_BACKUP-{server_id} directory."""
|
||||
"""Searches workspace for a DISCORD_BACKUP-{server_id} directory. Creates it if missing."""
|
||||
for d in Path(".").rglob(f"DISCORD_BACKUP-{server_id}"):
|
||||
if d.is_dir():
|
||||
logger.info(f"Found backup directory: {d}")
|
||||
return d
|
||||
raise FileNotFoundError(
|
||||
f"No backup found for server {server_id}. "
|
||||
f"Expected a directory named DISCORD_BACKUP-{server_id}"
|
||||
)
|
||||
|
||||
# If not found, create it in the current directory
|
||||
new_path = Path(".") / f"DISCORD_BACKUP-{server_id}"
|
||||
logger.info(f"No existing backup directory found for {server_id}. Creating new one: {new_path}")
|
||||
new_path.mkdir(exist_ok=True)
|
||||
return new_path
|
||||
|
||||
async def validate_all(self) -> Dict[str, Any]:
|
||||
"""Returns connection validation status as a dictionary."""
|
||||
|
|
|
|||
|
|
@ -612,9 +612,12 @@ class DiscordExporter:
|
|||
})
|
||||
|
||||
# Determine message type (Override if it's a thread starter or forward)
|
||||
msg_type = str(msg.type).split(".")[-1].capitalize()
|
||||
if msg.thread:
|
||||
raw_repr = str(msg.type).lower()
|
||||
|
||||
if "thread_starter" in raw_repr or msg.thread:
|
||||
msg_type = "ThreadStarter"
|
||||
else:
|
||||
msg_type = raw_repr.split(".")[-1].capitalize()
|
||||
|
||||
# Check for forwarded flags (newer discord.py feature)
|
||||
try:
|
||||
|
|
@ -733,7 +736,7 @@ class DiscordExporter:
|
|||
"../../users/avatars" # Two levels up from {forum_id}/{thread_id}/
|
||||
)
|
||||
# Override type and add title for forum starter messages
|
||||
msg_data["type"] = "Thread_starter_message"
|
||||
msg_data["type"] = "ThreadStarter"
|
||||
msg_data["title"] = thread.name
|
||||
|
||||
# Store applied tag IDs (as strings) — names are resolvable via the forum's available_tags
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ from datetime import datetime
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Vertical, VerticalScroll
|
||||
from textual.containers import Container, Vertical, VerticalScroll, Horizontal
|
||||
from textual.widgets import Button, Label, Rule
|
||||
from textual import work
|
||||
|
||||
|
|
@ -30,8 +30,14 @@ class BackupPane(Container):
|
|||
DEFAULT_CSS = """
|
||||
BackupPane { height: auto; width: 100%; }
|
||||
BackupPane #bp_info {
|
||||
height: auto; border: tall cyan; padding: 1; margin-bottom: 1;
|
||||
height: auto; border: tall cyan; padding: 1; margin-bottom: 1; layout: vertical;
|
||||
}
|
||||
#bp_info_split { height: auto; layout: horizontal; width: 100%; margin-bottom: 1; }
|
||||
.info_pane { width: 1fr; height: auto; }
|
||||
.info_pane Label { width: 100%; }
|
||||
.pane_header { text-style: bold; color: $accent; margin-bottom: 1; }
|
||||
#bp_lbl_backup { text-style: bold; margin-top: 1; }
|
||||
|
||||
BackupPane #bp_actions { height: auto; }
|
||||
BackupPane #bp_actions Button { width: 100%; margin-bottom: 1; }
|
||||
"""
|
||||
|
|
@ -47,9 +53,17 @@ class BackupPane(Container):
|
|||
def compose(self) -> ComposeResult:
|
||||
with VerticalScroll():
|
||||
with Vertical(id="bp_info"):
|
||||
yield Label("Loading...", id="bp_lbl_server")
|
||||
yield Label("", id="bp_lbl_bot")
|
||||
yield Label("", id="bp_lbl_backup")
|
||||
with Horizontal(id="bp_info_split"):
|
||||
with Vertical(classes="info_pane"):
|
||||
yield Label("Discord", classes="pane_header")
|
||||
yield Label("Server: -", id="bp_lbl_server")
|
||||
yield Label("Source: -", id="bp_lbl_bot")
|
||||
with Vertical(classes="info_pane", id="bp_target_pane"):
|
||||
# Hidden in backup mode
|
||||
pass
|
||||
|
||||
yield Rule()
|
||||
yield Label("Status: -", id="bp_lbl_backup")
|
||||
with Vertical(id="bp_actions"):
|
||||
yield Button("Backup Server Profile", id="bp_backup_profile", disabled=True)
|
||||
yield Button("Backup Channel Messages", id="bp_backup_msgs", disabled=True, variant="primary")
|
||||
|
|
@ -105,11 +119,18 @@ class BackupPane(Container):
|
|||
return None
|
||||
|
||||
def _update_ui(self, server_text, bot_text, backup_text, enabled):
|
||||
self.query_one("#bp_lbl_server", Label).update(f"Source Server: {server_text}")
|
||||
self.query_one("#bp_lbl_bot", Label).update(f"Bot: {bot_text}")
|
||||
self.query_one("#bp_lbl_backup", Label).update(backup_text)
|
||||
for bid in ("#bp_backup_profile", "#bp_backup_msgs", "#bp_backup_sync"):
|
||||
self.query_one(bid, Button).disabled = not enabled
|
||||
try:
|
||||
self.query_one("#bp_lbl_server", Label).update(f"Server: {server_text}")
|
||||
self.query_one("#bp_lbl_bot", Label).update(f"Source: {bot_text}")
|
||||
self.query_one("#bp_lbl_backup", Label).update(f"Status: {backup_text}")
|
||||
|
||||
# Hide target pane in backup
|
||||
self.query_one("#bp_target_pane").display = False
|
||||
|
||||
for bid in ("#bp_backup_profile", "#bp_backup_msgs", "#bp_backup_sync"):
|
||||
self.query_one(bid, Button).disabled = not enabled
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── button routing ────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -136,6 +157,35 @@ class BackupPane(Container):
|
|||
await self.engine.discord_reader.start()
|
||||
await self.exporter.setup()
|
||||
|
||||
# Gather and print summary
|
||||
server = getattr(self.engine.discord_reader, 'current_server', None)
|
||||
if server:
|
||||
modal.write(f"[bold cyan]Server Profile to Backup:[/bold cyan]")
|
||||
modal.write(f" Name: [green]{server.name}[/green]")
|
||||
modal.write(f" Icon: [green]{'Present' if server.icon else 'None'}[/green]")
|
||||
modal.write(f" Roles: [green]{len(server.roles)}[/green]")
|
||||
modal.write(f" Emojis: [green]{len(server.emojis)}[/green]")
|
||||
modal.write(f" Channels: [green]{len(server.channels)}[/green]")
|
||||
modal.write("")
|
||||
|
||||
modal.show_info("[bold cyan]Profile Backup Ready[/bold cyan]", f"Overview: {len(server.channels) if server else '?'} Channels, {len(server.roles) if server else '?'} Roles")
|
||||
modal.set_status("Awaiting Confirmation to Backup Profile...")
|
||||
|
||||
choice = await modal.phase_wait_confirm(
|
||||
btn_start_label="Start Backup",
|
||||
show_id=False
|
||||
)
|
||||
if choice in ("btn_back", "btn_main_menu"):
|
||||
modal.dismiss()
|
||||
self.engine.is_running = False
|
||||
await self.engine.close_connections()
|
||||
if choice == "btn_main_menu":
|
||||
self.app.switch_screen("config_selection")
|
||||
return
|
||||
|
||||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||
modal.phase_progress()
|
||||
modal.set_status("Exporting Server Structure...")
|
||||
modal.write("[yellow]Backing up server profile & skeleton...[/yellow]")
|
||||
await self.exporter.export_metadata()
|
||||
await self.exporter.download_server_assets()
|
||||
|
|
@ -227,18 +277,30 @@ class BackupPane(Container):
|
|||
self.app.push_screen(modal_prog)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
msg = "Backup Channels" if not force_overwrite else "Overwriting existing backups"
|
||||
target_preview = ", ".join([c.name for c in selected_channels[:3]])
|
||||
if len(selected_channels) > 3:
|
||||
target_preview += "..."
|
||||
new_channels = [c for c in selected_channels if c.id not in backed_up_ids]
|
||||
existing_channels = [c for c in selected_channels if c.id in backed_up_ids]
|
||||
|
||||
server = getattr(self.engine.discord_reader, 'current_server', None)
|
||||
if server:
|
||||
modal_prog.write(f"[bold cyan]Server Profile:[/bold cyan]")
|
||||
modal_prog.write(f" Name: [green]{server.name}[/green]")
|
||||
modal_prog.write(f" Icon: [green]{'Present' if server.icon else 'None'}[/green]")
|
||||
modal_prog.write("")
|
||||
|
||||
modal_prog.set_status(f"Confirm to proceed with Backup of [bold]{len(selected_channels)}[/bold] channels")
|
||||
modal_prog.show_info(f"[cyan]{msg}[/cyan]", f"Targets: {target_preview}")
|
||||
modal_prog.show_info(f"[cyan]Backup Channels[/cyan]", f"{len(new_channels)} new, {len(existing_channels)} existing")
|
||||
|
||||
# Show full target list in the bottom log
|
||||
modal_prog.write("[bold]Target Channels:[/bold]")
|
||||
for idx, c in enumerate(selected_channels):
|
||||
modal_prog.write(f" {idx+1}. #{c.name}")
|
||||
# Show categorized channel lists in the bottom log
|
||||
if new_channels:
|
||||
modal_prog.write("[bold green]New Backups to be created:[/bold green]")
|
||||
for idx, c in enumerate(new_channels):
|
||||
modal_prog.write(f" {idx+1}. #{c.name}")
|
||||
|
||||
if existing_channels:
|
||||
action = "Overwritten" if force_overwrite else "Updated"
|
||||
modal_prog.write(f"[bold yellow]\nExisting backups to be {action}:[/bold yellow]")
|
||||
for idx, c in enumerate(existing_channels):
|
||||
modal_prog.write(f" {idx+1}. #{c.name}")
|
||||
|
||||
choice = await modal_prog.phase_wait_confirm(btn_start_label="Start Channel Backup", show_id=False)
|
||||
if choice == "btn_back":
|
||||
|
|
@ -328,6 +390,36 @@ class BackupPane(Container):
|
|||
await self.engine.discord_reader.start()
|
||||
await self.exporter.setup()
|
||||
|
||||
# Gather and print summary
|
||||
server = getattr(self.engine.discord_reader, 'current_server', None)
|
||||
if server:
|
||||
modal_prog.write(f"[bold cyan]Server Profile to Sync:[/bold cyan]")
|
||||
modal_prog.write(f" Name: [green]{server.name}[/green]")
|
||||
modal_prog.write(f" Icon: [green]{'Present' if server.icon else 'None'}[/green]")
|
||||
modal_prog.write(f" Roles: [green]{len(server.roles)}[/green]")
|
||||
modal_prog.write(f" Emojis: [green]{len(server.emojis)}[/green]")
|
||||
modal_prog.write(f" Channels: [green]{len(server.channels)}[/green]")
|
||||
modal_prog.write("\n[dim]This operation will update the profile and scan existing baked-up channels for new messages.[/dim]")
|
||||
modal_prog.write("")
|
||||
|
||||
modal_prog.show_info("[bold green]Sync Ready[/bold green]", f"Overview: {len(server.channels) if server else '?'} Channels")
|
||||
modal_prog.set_status("Awaiting Confirmation to Sync Profile and Messages...")
|
||||
|
||||
choice = await modal_prog.phase_wait_confirm(
|
||||
btn_start_label="Start Sync",
|
||||
show_id=False
|
||||
)
|
||||
if choice in ("btn_back", "btn_main_menu"):
|
||||
modal_prog.dismiss()
|
||||
self.engine.is_running = False
|
||||
await self.engine.close_connections()
|
||||
if choice == "btn_main_menu":
|
||||
self.app.switch_screen("config_selection")
|
||||
return
|
||||
|
||||
modal_prog.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||
modal_prog.phase_progress()
|
||||
modal_prog.set_status("Updating structure...")
|
||||
modal_prog.write("Updating structure...")
|
||||
await self.exporter.export_metadata()
|
||||
await self.exporter.download_server_assets()
|
||||
|
|
|
|||
|
|
@ -67,8 +67,8 @@ class ProgressScreen(Screen[None]):
|
|||
#prog_bar { margin-bottom: 1; width: 80%; }
|
||||
#prog_item_status { margin-bottom: 1; text-style: bold; color: cyan; width: 100%; text-align: center; }
|
||||
|
||||
#info_container { height: auto; layout: vertical; border: solid $secondary; padding: 1; margin-bottom: 1; display: none; }
|
||||
.info_label { text-style: bold; content-align: center middle; width: 100%; color: $secondary-lighten-2; }
|
||||
#info_container { height: auto; layout: vertical; border: solid cyan; padding: 1; margin-bottom: 1; display: none; }
|
||||
.info_label { text-style: bold; content-align: center middle; width: 100%; color: cyan; }
|
||||
|
||||
#prog_actions { height: auto; margin-top: 1; dock: bottom; margin-bottom: 0; layout: vertical; }
|
||||
.action_row { height: auto; layout: horizontal; }
|
||||
|
|
@ -91,8 +91,8 @@ class ProgressScreen(Screen[None]):
|
|||
|
||||
|
||||
with Vertical(id="info_container"):
|
||||
yield Label("No previous migration data.", id="info_migration_status", classes="info_label")
|
||||
yield Label("New items detected.", id="info_new_items", classes="info_label")
|
||||
yield Label("", id="info_migration_status", classes="info_label")
|
||||
yield Label("", id="info_new_items", classes="info_label")
|
||||
|
||||
# Progress bar moved inside info container
|
||||
with Center(id="prog_bar_container"):
|
||||
|
|
@ -258,6 +258,9 @@ class ProgressScreen(Screen[None]):
|
|||
try: self.query_one("#prog_bar", ProgressBar).display = False
|
||||
except Exception: pass
|
||||
|
||||
try: self.query_one("#prog_timer", Label).display = False
|
||||
except Exception: pass
|
||||
|
||||
# Update button labels
|
||||
try: self.query_one("#btn_start_first", Button).label = btn_start_label
|
||||
except Exception: pass
|
||||
|
|
@ -314,6 +317,11 @@ class ProgressScreen(Screen[None]):
|
|||
try: self.query_one("#prog_loader", LoadingIndicator).display = True
|
||||
except Exception: pass
|
||||
|
||||
# Show timer and reset it for the operation
|
||||
try: self.query_one("#prog_timer", Label).display = True
|
||||
except Exception: pass
|
||||
self.start_time = time.time()
|
||||
|
||||
try: self.query_one("#info_migration_status", Label).display = False
|
||||
except Exception: pass
|
||||
|
||||
|
|
|
|||
|
|
@ -83,12 +83,13 @@ class ShuttlePane(Container):
|
|||
DEFAULT_CSS = """
|
||||
ShuttlePane { height: auto; width: 100%; }
|
||||
ShuttlePane #sp_info {
|
||||
height: auto; border: tall cyan; padding: 1; margin-bottom: 1;
|
||||
height: auto; border: tall cyan; padding: 1; margin-bottom: 1; layout: vertical;
|
||||
}
|
||||
#sp_info_split { height: auto; layout: horizontal; }
|
||||
#sp_info_split { height: auto; layout: horizontal; width: 100%; margin-bottom: 1; }
|
||||
.info_pane { width: 1fr; height: auto; }
|
||||
.info_pane Label { width: 100%; }
|
||||
.pane_header { text-style: bold; color: $accent; margin-bottom: 1; }
|
||||
#sp_lbl_status { text-style: bold; margin-top: 1; }
|
||||
|
||||
ShuttlePane #sp_actions { height: auto; }
|
||||
ShuttlePane #sp_actions Button { width: 100%; margin-bottom: 1; }
|
||||
|
|
@ -340,6 +341,7 @@ class ShuttlePane(Container):
|
|||
self.run_batch_sync(choices)
|
||||
self.app.push_screen(OptionSelectModal("Sync Server Settings", options), on_result)
|
||||
|
||||
|
||||
# ── batch workers ──────────────────────────────────────────────────
|
||||
|
||||
@work(exclusive=True)
|
||||
|
|
@ -357,11 +359,29 @@ class ShuttlePane(Container):
|
|||
except Exception as e:
|
||||
logger.warning(f"Could not pre-connect for Clone preview: {e}")
|
||||
|
||||
# Show info container early
|
||||
modal.show_info("[bold cyan]Clone Template Ready[/bold cyan]", f"{len(selections)} categories/roles selected.")
|
||||
|
||||
modal.set_status(f"Awaiting Confirmation for {len(selections)} Operations...")
|
||||
|
||||
# Fetch and display live preview with presence highlighting
|
||||
preview = await self._fetch_clone_preview(selections) if connections_started else {}
|
||||
|
||||
if connections_started:
|
||||
src_server = getattr(self.engine.discord_reader, 'current_server', None)
|
||||
tgt_server_info = await self.engine.writer.validate()
|
||||
tgt_server_name = tgt_server_info.get("community_name", "target community")
|
||||
|
||||
if src_server:
|
||||
modal.write(f"[bold cyan]Source Server Profile:[/bold cyan]")
|
||||
modal.write(f" Name: [green]{src_server.name}[/green]")
|
||||
modal.write(f" Icon: [green]{'Present' if src_server.icon else 'None'}[/green]")
|
||||
modal.write(f" Roles: [green]{len(src_server.roles)}[/green]")
|
||||
modal.write(f" Emojis: [green]{len(src_server.emojis)}[/green]")
|
||||
modal.write(f" Channels: [green]{len(src_server.channels)}[/green]")
|
||||
modal.write("")
|
||||
modal.write(f"[bold cyan]Target Community:[/bold cyan] [green]{tgt_server_name}[/green]\n")
|
||||
|
||||
if "roles" in preview:
|
||||
roles = preview["roles"]
|
||||
modal.write(f"[bold cyan]Roles to be Cloned ({len(roles)}):[/bold cyan]")
|
||||
|
|
@ -459,6 +479,33 @@ class ShuttlePane(Container):
|
|||
self.app.push_screen(modal)
|
||||
await asyncio.sleep(0.1)
|
||||
try:
|
||||
# Connect early to get metadata
|
||||
modal.set_status("Connecting to Source and Target Servers for Preview...")
|
||||
connections_started = False
|
||||
try:
|
||||
await self.engine.start_connections()
|
||||
connections_started = True
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not pre-connect for Sync preview: {e}")
|
||||
|
||||
if connections_started:
|
||||
src_server = getattr(self.engine.discord_reader, 'current_server', None)
|
||||
tgt_server_info = await self.engine.writer.validate()
|
||||
tgt_server_name = tgt_server_info.get("community_name", "target community")
|
||||
|
||||
if src_server:
|
||||
modal.write(f"[bold cyan]Source Server Profile:[/bold cyan]")
|
||||
modal.write(f" Name: [green]{src_server.name}[/green]")
|
||||
modal.write(f" Icon: [green]{'Present' if src_server.icon else 'None'}[/green]")
|
||||
modal.write(f" Roles: [green]{len(src_server.roles)}[/green]")
|
||||
modal.write(f" Emojis: [green]{len(src_server.emojis)}[/green]")
|
||||
modal.write(f" Channels: [green]{len(src_server.channels)}[/green]")
|
||||
modal.write("")
|
||||
modal.write(f"[bold cyan]Target Community:[/bold cyan] [green]{tgt_server_name}[/green]\n")
|
||||
|
||||
# Show info container
|
||||
modal.show_info("[bold yellow]Sync Ready[/bold yellow]", "Comparing server configurations...")
|
||||
|
||||
modal.set_status("Awaiting Confirmation to Sync Server Settings...")
|
||||
choice = await modal.phase_wait_confirm(
|
||||
btn_start_label="Start Syncing",
|
||||
|
|
@ -478,7 +525,8 @@ class ShuttlePane(Container):
|
|||
modal.cancel_callback = lambda: setattr(self.engine, "is_running", False)
|
||||
modal.phase_progress()
|
||||
modal.set_status("Syncing Server Settings")
|
||||
await self.engine.start_connections()
|
||||
if not connections_started:
|
||||
await self.engine.start_connections()
|
||||
self.engine.is_running = True
|
||||
|
||||
results = {}
|
||||
|
|
@ -690,6 +738,9 @@ class ShuttlePane(Container):
|
|||
await asyncio.sleep(0.1)
|
||||
|
||||
try:
|
||||
# Show info container
|
||||
modal.show_info("[bold cyan]Message Migration Ready[/bold cyan]", "Checking channel permissions...")
|
||||
|
||||
modal.set_status("Fetching channels...")
|
||||
await self.engine.start_connections()
|
||||
|
||||
|
|
@ -752,6 +803,18 @@ class ShuttlePane(Container):
|
|||
modal = ProgressScreen(log_level=self.config.migration.log_level)
|
||||
self.app.push_screen(modal)
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
src_server = getattr(self.engine.discord_reader, 'current_server', None)
|
||||
tgt_server_info = await self.engine.writer.validate()
|
||||
tgt_server_name = tgt_server_info.get("community_name", "target community")
|
||||
|
||||
if src_server:
|
||||
modal.write(f"[bold cyan]Source Server Profile:[/bold cyan]")
|
||||
modal.write(f" Name: [green]{src_server.name}[/green]")
|
||||
modal.write(f" Icon: [green]{'Present' if src_server.icon else 'None'}[/green]")
|
||||
modal.write("")
|
||||
modal.write(f"[bold cyan]Target Community:[/bold cyan] [green]{tgt_server_name}[/green]\n")
|
||||
|
||||
modal.set_status("Analyzing channel...")
|
||||
modal.show_stats()
|
||||
|
||||
|
|
@ -942,9 +1005,19 @@ class ShuttlePane(Container):
|
|||
except Exception as e:
|
||||
logger.warning(f"Could not pre-connect for DZ preview: {e}")
|
||||
|
||||
if target_started:
|
||||
tgt_server_info = await self.engine.writer.validate()
|
||||
tgt_server_name = tgt_server_info.get("community_name", "target community")
|
||||
modal.write(f"[bold red]Target Community:[/bold red] [green]{tgt_server_name}[/green]")
|
||||
modal.write(f"[bold red]WARNING: THE ACTIONS BELOW WILL DELETE DATA PERMANENTLY IN: {tgt_server_name}![/bold red]")
|
||||
modal.write("")
|
||||
else:
|
||||
modal.write("[bold red]WARNING: THIS WILL DELETE DATA PERMANENTLY! MUST PROCEED TO CONTINUE.[/bold red]")
|
||||
modal.write("")
|
||||
|
||||
# Show info container
|
||||
modal.show_info("[bold red]Danger Zone Ready[/bold red]", "Fetching target entities...")
|
||||
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("")
|
||||
|
||||
# Fetch and display live item names from target server
|
||||
preview = await self._fetch_dz_preview(selections) if target_started else {}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue