add backup delete option
This commit is contained in:
parent
fa0865ef85
commit
6f94a00714
4 changed files with 266 additions and 25 deletions
|
|
@ -464,39 +464,41 @@ class BackupDatabase:
|
||||||
INSERT OR REPLACE INTO media_pool (hash, local_path, size, mime_type, first_seen_url)
|
INSERT OR REPLACE INTO media_pool (hash, local_path, size, mime_type, first_seen_url)
|
||||||
VALUES (?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?)
|
||||||
""", (file_hash, str(local_path), size, mime_type, url))
|
""", (file_hash, str(local_path), size, mime_type, url))
|
||||||
|
|
||||||
def get_stats_by_channel(self) -> Dict[int, Dict[str, Any]]:
|
def get_stats_by_channel(self) -> Dict[int, Dict[str, Any]]:
|
||||||
"""Returns aggregate stats for all channels with backups."""
|
"""Returns aggregate stats for all channels with backups."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
# 1. Message counts (aggregating threads into parent channel)
|
||||||
msg_rows = self._conn.execute("""
|
msg_rows = self._conn.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(t.parent_id, m.channel_id) as channel_id,
|
COALESCE(t.parent_id, m.channel_id) as agg_channel_id,
|
||||||
COUNT(m.id) as msg_count
|
COUNT(m.id) as msg_count
|
||||||
FROM messages m
|
FROM messages m
|
||||||
LEFT JOIN threads t ON m.channel_id = t.id
|
LEFT JOIN threads t ON m.channel_id = t.id
|
||||||
GROUP BY channel_id
|
GROUP BY agg_channel_id
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
|
|
||||||
|
# 2. Thread counts
|
||||||
thread_rows = self._conn.execute("""
|
thread_rows = self._conn.execute("""
|
||||||
SELECT parent_id, COUNT(*) as thread_count
|
SELECT parent_id, COUNT(*) as thread_count
|
||||||
FROM threads
|
FROM threads
|
||||||
GROUP BY parent_id
|
GROUP BY parent_id
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
|
|
||||||
|
# 3. Attachment counts and sizes
|
||||||
att_rows = self._conn.execute("""
|
att_rows = self._conn.execute("""
|
||||||
SELECT
|
SELECT
|
||||||
COALESCE(t.parent_id, m.channel_id) as channel_id,
|
COALESCE(t.parent_id, m.channel_id) as agg_channel_id,
|
||||||
COUNT(a.id) as att_count,
|
COUNT(a.id) as att_count,
|
||||||
SUM(a.size) as total_size
|
SUM(a.size) as total_size
|
||||||
FROM attachments a
|
FROM attachments a
|
||||||
JOIN messages m ON a.message_id = m.id
|
JOIN messages m ON a.message_id = m.id
|
||||||
LEFT JOIN threads t ON m.channel_id = t.id
|
LEFT JOIN threads t ON m.channel_id = t.id
|
||||||
GROUP BY channel_id
|
GROUP BY agg_channel_id
|
||||||
""").fetchall()
|
""").fetchall()
|
||||||
|
|
||||||
stats = {}
|
stats = {}
|
||||||
for r in msg_rows:
|
for r in msg_rows:
|
||||||
cid = parse_snowflake(r["channel_id"])
|
cid = parse_snowflake(r["agg_channel_id"])
|
||||||
if cid is None: continue
|
if cid is None: continue
|
||||||
stats[cid] = {
|
stats[cid] = {
|
||||||
"message_count": r["msg_count"],
|
"message_count": r["msg_count"],
|
||||||
|
|
@ -513,7 +515,7 @@ class BackupDatabase:
|
||||||
stats[cid]["thread_count"] = r["thread_count"]
|
stats[cid]["thread_count"] = r["thread_count"]
|
||||||
|
|
||||||
for r in att_rows:
|
for r in att_rows:
|
||||||
cid = parse_snowflake(r["channel_id"])
|
cid = parse_snowflake(r["agg_channel_id"])
|
||||||
if cid is None: continue
|
if cid is None: continue
|
||||||
if cid not in stats:
|
if cid not in stats:
|
||||||
stats[cid] = {"message_count": 0, "thread_count": 0, "attachment_count": 0, "total_size": 0}
|
stats[cid] = {"message_count": 0, "thread_count": 0, "attachment_count": 0, "total_size": 0}
|
||||||
|
|
@ -696,6 +698,83 @@ class BackupDatabase:
|
||||||
|
|
||||||
return msg_list
|
return msg_list
|
||||||
|
|
||||||
|
def delete_channel_messages(self, channel_id: Union[str, int]):
|
||||||
|
"""Deletes all messages and related metadata for a specific channel and its threads."""
|
||||||
|
cid = str(channel_id)
|
||||||
|
with self._lock:
|
||||||
|
# 1. Identify all channel IDs involved (parent + all threads)
|
||||||
|
target_ids = [cid]
|
||||||
|
thread_rows = self._conn.execute("SELECT id FROM threads WHERE parent_id = ?", (cid,)).fetchall()
|
||||||
|
for tr in thread_rows:
|
||||||
|
target_ids.append(tr["id"])
|
||||||
|
|
||||||
|
placeholders_chans = ",".join(["?"] * len(target_ids))
|
||||||
|
|
||||||
|
# 2. Get all message IDs for these channels
|
||||||
|
msg_ids = [r["id"] for r in self._conn.execute(
|
||||||
|
f"SELECT id FROM messages WHERE channel_id IN ({placeholders_chans})", target_ids
|
||||||
|
).fetchall()]
|
||||||
|
|
||||||
|
if not msg_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
placeholders_msgs = ",".join(["?"] * len(msg_ids))
|
||||||
|
|
||||||
|
# 3. Delete related metadata
|
||||||
|
self._conn.execute(f"DELETE FROM attachments WHERE message_id IN ({placeholders_msgs})", msg_ids)
|
||||||
|
self._conn.execute(f"DELETE FROM embeds WHERE message_id IN ({placeholders_msgs})", msg_ids)
|
||||||
|
self._conn.execute(f"DELETE FROM reactions WHERE message_id IN ({placeholders_msgs})", msg_ids)
|
||||||
|
self._conn.execute(f"DELETE FROM message_stickers WHERE message_id IN ({placeholders_msgs})", msg_ids)
|
||||||
|
|
||||||
|
# 4. Delete messages
|
||||||
|
self._conn.execute(f"DELETE FROM messages WHERE channel_id IN ({placeholders_chans})", target_ids)
|
||||||
|
|
||||||
|
# 5. Delete thread metadata (optional but consistent)
|
||||||
|
self._conn.execute(f"DELETE FROM threads WHERE parent_id = ?", (cid,))
|
||||||
|
|
||||||
|
self._conn.commit()
|
||||||
|
logger.info(f"Deleted messages, metadata, and threads for channel {cid}")
|
||||||
|
|
||||||
|
def purge_unused_media(self, backup_root: Path) -> int:
|
||||||
|
"""Removes media files from disk and DB that are no longer referenced by any message."""
|
||||||
|
purged_count = 0
|
||||||
|
with self._lock:
|
||||||
|
# 1. Find all hashes in use
|
||||||
|
used_hashes = set()
|
||||||
|
for r in self._conn.execute("SELECT DISTINCT local_hash FROM attachments WHERE local_hash IS NOT NULL").fetchall():
|
||||||
|
used_hashes.add(r[0])
|
||||||
|
for r in self._conn.execute("SELECT DISTINCT local_hash FROM message_stickers WHERE local_hash IS NOT NULL").fetchall():
|
||||||
|
used_hashes.add(r[0])
|
||||||
|
|
||||||
|
# 2. Get all hashes in pool
|
||||||
|
all_media = self._conn.execute("SELECT hash, local_path FROM media_pool").fetchall()
|
||||||
|
|
||||||
|
to_delete = []
|
||||||
|
for m in all_media:
|
||||||
|
m_hash = m["hash"]
|
||||||
|
if m_hash not in used_hashes:
|
||||||
|
to_delete.append(dict(m))
|
||||||
|
|
||||||
|
if not to_delete:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# 3. Delete from filesystem and DB
|
||||||
|
for m in to_delete:
|
||||||
|
try:
|
||||||
|
file_path = backup_root / m["local_path"]
|
||||||
|
if file_path.exists():
|
||||||
|
file_path.unlink()
|
||||||
|
|
||||||
|
self._conn.execute("DELETE FROM media_pool WHERE hash = ?", (m["hash"],))
|
||||||
|
purged_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to purge media {m['hash']}: {e}")
|
||||||
|
|
||||||
|
self._conn.commit()
|
||||||
|
logger.info(f"Purged {purged_count} unused media files")
|
||||||
|
|
||||||
|
return purged_count
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Commits any pending writes and closes the connection."""
|
"""Commits any pending writes and closes the connection."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
|
|
|
||||||
|
|
@ -703,7 +703,7 @@ class DiscordExporter:
|
||||||
if tmp_path and tmp_path.exists():
|
if tmp_path and tmp_path.exists():
|
||||||
try: tmp_path.unlink()
|
try: tmp_path.unlink()
|
||||||
except: pass
|
except: pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def export_threads(self, channel_id: int, progress_callback=None, force=False, accumulated_count=0, accumulated_threads=0, accumulated_files=0, after_id: int | None = None):
|
async def export_threads(self, channel_id: int, progress_callback=None, force=False, accumulated_count=0, accumulated_threads=0, accumulated_files=0, after_id: int | None = None):
|
||||||
"""Exports active and archived threads for a channel to SQLite."""
|
"""Exports active and archived threads for a channel to SQLite."""
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,10 @@ from datetime import datetime
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
from textual.app import ComposeResult
|
from textual.app import ComposeResult
|
||||||
from textual.screen import Screen
|
from textual.screen import Screen, ModalScreen
|
||||||
from textual.containers import Container, Vertical, VerticalScroll, Horizontal
|
from textual.containers import Container, Vertical, VerticalScroll, Horizontal, Grid
|
||||||
from textual.widgets import Button, Label, Rule, Tree
|
from textual.widgets import Button, Label, Rule, Tree, Static
|
||||||
from textual import work
|
from textual import work, on
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
from rich.style import Style
|
from rich.style import Style
|
||||||
|
|
||||||
|
|
@ -227,7 +227,7 @@ class BackupStatsScreen(Screen[None]):
|
||||||
|
|
||||||
# Add a header row to the tree root, purely for visual columns
|
# Add a header row to the tree root, purely for visual columns
|
||||||
# Using depth=4 for header to compensate for root node toggle position delta
|
# Using depth=4 for header to compensate for root node toggle position delta
|
||||||
header_text = self._format_tree_row("NAME", "MESSAGES", "THREADS", "FILES", "SIZE", depth=-1)
|
header_text = self._format_tree_row("NAME", "MESSAGES", "THREADS", "FILES", "SIZE", "MANAGE", depth=-1)
|
||||||
header_text.stylize("bold")
|
header_text.stylize("bold")
|
||||||
self.stats_tree.root.set_label(header_text)
|
self.stats_tree.root.set_label(header_text)
|
||||||
self.stats_tree.show_root = True
|
self.stats_tree.show_root = True
|
||||||
|
|
@ -238,6 +238,25 @@ class BackupStatsScreen(Screen[None]):
|
||||||
if event.button.id == "btn_back":
|
if event.button.id == "btn_back":
|
||||||
self.dismiss()
|
self.dismiss()
|
||||||
|
|
||||||
|
@on(Tree.NodeSelected)
|
||||||
|
def on_node_selected(self, event: Tree.NodeSelected) -> None:
|
||||||
|
"""Handle selection of a channel node to open management modal."""
|
||||||
|
node_data = event.node.data
|
||||||
|
if node_data and node_data.get("is_channel") and node_data.get("is_backed_up"):
|
||||||
|
self.app.push_screen(
|
||||||
|
ManageBackupModal(
|
||||||
|
node_data["name"],
|
||||||
|
node_data["id"],
|
||||||
|
node_data["stats"],
|
||||||
|
self.target_dir
|
||||||
|
),
|
||||||
|
callback=self._on_modal_close
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_modal_close(self, changed: bool) -> None:
|
||||||
|
if changed:
|
||||||
|
self.load_data()
|
||||||
|
|
||||||
def _format_size(self, size_bytes: int) -> str:
|
def _format_size(self, size_bytes: int) -> str:
|
||||||
if size_bytes == 0:
|
if size_bytes == 0:
|
||||||
return "0 B"
|
return "0 B"
|
||||||
|
|
@ -249,7 +268,7 @@ class BackupStatsScreen(Screen[None]):
|
||||||
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
return f"{size_bytes / (1024 * 1024):.2f} MB"
|
||||||
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
|
||||||
|
|
||||||
def _format_tree_row(self, name: str, msgs, threads, files, size, depth=0) -> Text:
|
def _format_tree_row(self, name: str, msgs, threads, files, size, manage="", depth=0) -> Text:
|
||||||
"""Pads and aligns columns for the tree view to simulate a table."""
|
"""Pads and aligns columns for the tree view to simulate a table."""
|
||||||
# Textual Tree indents child nodes. Standard indent is 4 characters.
|
# Textual Tree indents child nodes. Standard indent is 4 characters.
|
||||||
# To maintain vertical alignment of values, we subtract the indentation from the name column width.
|
# To maintain vertical alignment of values, we subtract the indentation from the name column width.
|
||||||
|
|
@ -263,11 +282,16 @@ class BackupStatsScreen(Screen[None]):
|
||||||
col_thd = str(threads).rjust(12)
|
col_thd = str(threads).rjust(12)
|
||||||
col_file = str(files).rjust(12)
|
col_file = str(files).rjust(12)
|
||||||
col_size = str(size).rjust(15)
|
col_size = str(size).rjust(15)
|
||||||
return Text(f"{col_name}{col_msg}{col_thd}{col_file}{col_size}")
|
col_manage = str(manage).rjust(10)
|
||||||
|
return Text(f"{col_name}{col_msg}{col_thd}{col_file}{col_size}{col_manage}")
|
||||||
|
|
||||||
@work(exclusive=True)
|
@work(exclusive=True)
|
||||||
async def load_data(self) -> None:
|
async def load_data(self) -> None:
|
||||||
try:
|
try:
|
||||||
|
# Clear old nodes
|
||||||
|
while self.stats_tree.root.children:
|
||||||
|
self.stats_tree.root.children[0].remove()
|
||||||
|
|
||||||
# Initialize BackupReader
|
# Initialize BackupReader
|
||||||
reader = BackupReader(self.target_dir)
|
reader = BackupReader(self.target_dir)
|
||||||
await reader.start()
|
await reader.start()
|
||||||
|
|
@ -365,12 +389,15 @@ class BackupStatsScreen(Screen[None]):
|
||||||
is_bu = ch.id in backed_up_channel_ids
|
is_bu = ch.id in backed_up_channel_ids
|
||||||
|
|
||||||
chan_nodes_data.append({
|
chan_nodes_data.append({
|
||||||
|
"id": ch.id,
|
||||||
"name": f"# {ch.name}",
|
"name": f"# {ch.name}",
|
||||||
"msgs": stats["message_count"] if is_bu else "NA",
|
"msgs": stats["message_count"] if is_bu else "NA",
|
||||||
"threads": stats["thread_count"] if is_bu else "NA",
|
"threads": stats["thread_count"] if is_bu else "NA",
|
||||||
"files": stats["attachment_count"] if is_bu else "NA",
|
"files": stats["attachment_count"] if is_bu else "NA",
|
||||||
"size": stats["total_size"] if is_bu else 0,
|
"size": stats["total_size"] if is_bu else 0,
|
||||||
"is_backed_up": is_bu
|
"is_backed_up": is_bu,
|
||||||
|
"is_channel": True,
|
||||||
|
"stats": stats
|
||||||
})
|
})
|
||||||
|
|
||||||
c_msgs += stats["message_count"]
|
c_msgs += stats["message_count"]
|
||||||
|
|
@ -384,14 +411,16 @@ class BackupStatsScreen(Screen[None]):
|
||||||
|
|
||||||
for ch_data in chan_nodes_data:
|
for ch_data in chan_nodes_data:
|
||||||
size_str = self._format_size(ch_data['size']) if ch_data['is_backed_up'] else "NA"
|
size_str = self._format_size(ch_data['size']) if ch_data['is_backed_up'] else "NA"
|
||||||
ch_lbl = self._format_tree_row(ch_data['name'], ch_data['msgs'], ch_data['threads'], ch_data['files'], size_str, depth=2)
|
manage_icon = "🖍️" if ch_data['is_backed_up'] else ""
|
||||||
|
ch_lbl = self._format_tree_row(ch_data['name'], ch_data['msgs'], ch_data['threads'], ch_data['files'], size_str, manage_icon, depth=2)
|
||||||
|
|
||||||
if ch_data['is_backed_up']:
|
if ch_data['is_backed_up']:
|
||||||
ch_lbl.stylize("bold white")
|
ch_lbl.stylize("bold white")
|
||||||
else:
|
else:
|
||||||
ch_lbl.stylize("dim white")
|
ch_lbl.stylize("dim white")
|
||||||
|
|
||||||
node.add_leaf(ch_lbl)
|
newNode = node.add_leaf(ch_lbl)
|
||||||
|
newNode.data = ch_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
import traceback
|
import traceback
|
||||||
|
|
@ -403,3 +432,135 @@ class BackupStatsScreen(Screen[None]):
|
||||||
finally:
|
finally:
|
||||||
if 'reader' in locals():
|
if 'reader' in locals():
|
||||||
await reader.close()
|
await reader.close()
|
||||||
|
|
||||||
|
class ManageBackupModal(ModalScreen[bool]):
|
||||||
|
"""Modal for managing channel backup data."""
|
||||||
|
|
||||||
|
DEFAULT_CSS = """
|
||||||
|
ManageBackupModal {
|
||||||
|
align: center middle;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
#mb_container {
|
||||||
|
width: 60;
|
||||||
|
height: auto;
|
||||||
|
max-height: 80%;
|
||||||
|
border: thick $accent;
|
||||||
|
background: $surface;
|
||||||
|
padding: 1 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mb_title {
|
||||||
|
text-style: bold;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mb_info {
|
||||||
|
color: $text-muted;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mb_options {
|
||||||
|
margin-top: 1;
|
||||||
|
layout: vertical;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mb_options Button {
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mb_footer {
|
||||||
|
margin-top: 1;
|
||||||
|
layout: vertical;
|
||||||
|
content-align: center middle;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#mb_confirm_text {
|
||||||
|
color: $error;
|
||||||
|
text-style: bold italic;
|
||||||
|
margin-bottom: 1;
|
||||||
|
height: 1;
|
||||||
|
content-align: center middle;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#btn_cancel {
|
||||||
|
width: 16;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, channel_name: str, channel_id: int, stats: dict, target_dir: Path, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.channel_name = channel_name
|
||||||
|
self.channel_id = channel_id
|
||||||
|
self.stats = stats
|
||||||
|
self.target_dir = target_dir
|
||||||
|
self.primed_button = None # Tracks which button is awaiting second click
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
with Vertical(id="mb_container"):
|
||||||
|
yield Label(f"Manage Backup: {self.channel_name}", id="mb_title")
|
||||||
|
|
||||||
|
msgs = self.stats.get("message_count", 0)
|
||||||
|
files = self.stats.get("attachment_count", 0)
|
||||||
|
yield Label(f"Currently contains {msgs} messages and {files} attachments.", classes="mb_info")
|
||||||
|
|
||||||
|
yield Rule()
|
||||||
|
|
||||||
|
with Vertical(id="mb_options"):
|
||||||
|
yield Button("Delete All Messages", id="btn_delete_msgs", variant="warning")
|
||||||
|
yield Button("Delete Messages & Purge Unused Attachments", id="btn_delete_purge", variant="warning")
|
||||||
|
|
||||||
|
with Vertical(id="mb_footer"):
|
||||||
|
yield Label("", id="mb_confirm_text")
|
||||||
|
yield Button("Cancel", id="btn_cancel")
|
||||||
|
|
||||||
|
def on_button_pressed(self, event: Button.Pressed) -> None:
|
||||||
|
if event.button.id == "btn_cancel":
|
||||||
|
self.dismiss(False)
|
||||||
|
return
|
||||||
|
|
||||||
|
btn_id = event.button.id
|
||||||
|
if btn_id not in ("btn_delete_msgs", "btn_delete_purge"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.primed_button == btn_id:
|
||||||
|
# Second click, proceed
|
||||||
|
purge = (btn_id == "btn_delete_purge")
|
||||||
|
self.run_deletion(purge=purge)
|
||||||
|
else:
|
||||||
|
# First click, prime it
|
||||||
|
# Reset other buttons first
|
||||||
|
btn_msgs = self.query_one("#btn_delete_msgs", Button)
|
||||||
|
btn_purge = self.query_one("#btn_delete_purge", Button)
|
||||||
|
btn_msgs.variant = "warning"
|
||||||
|
btn_purge.variant = "warning"
|
||||||
|
|
||||||
|
# Prime current click
|
||||||
|
event.button.variant = "error"
|
||||||
|
self.primed_button = btn_id
|
||||||
|
self.query_one("#mb_confirm_text", Label).update("Click again to proceed")
|
||||||
|
|
||||||
|
@work(exclusive=True)
|
||||||
|
async def run_deletion(self, purge: bool = False) -> None:
|
||||||
|
try:
|
||||||
|
from src.core.backup_database import BackupDatabase
|
||||||
|
db_path = self.target_dir / "backup.db"
|
||||||
|
db = BackupDatabase(db_path)
|
||||||
|
|
||||||
|
# 1. Delete messages
|
||||||
|
db.delete_channel_messages(self.channel_id)
|
||||||
|
|
||||||
|
# 2. Optionally purge
|
||||||
|
if purge:
|
||||||
|
db.purge_unused_media(self.target_dir)
|
||||||
|
|
||||||
|
db.close()
|
||||||
|
self.dismiss(True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception("Failed to delete channel data")
|
||||||
|
self.app.notify(f"Error: {e}", severity="error")
|
||||||
|
|
|
||||||
|
|
@ -423,6 +423,7 @@ class OperationPane(Container):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in run_validate setup: {e}")
|
logger.error(f"Error in run_validate setup: {e}")
|
||||||
|
|
||||||
|
info = self._get_backup_info()
|
||||||
self.validation_results = {
|
self.validation_results = {
|
||||||
"discord_validating": True,
|
"discord_validating": True,
|
||||||
"target_validating": True,
|
"target_validating": True,
|
||||||
|
|
@ -435,11 +436,11 @@ class OperationPane(Container):
|
||||||
"target_permissions": {},
|
"target_permissions": {},
|
||||||
"target_error": None,
|
"target_error": None,
|
||||||
"discord_timeout": False, "target_timeout": False,
|
"discord_timeout": False, "target_timeout": False,
|
||||||
"backup_info_text": "",
|
"backup_info_text": f"Last backup: [cyan]{info}[/cyan]" if info else "",
|
||||||
}
|
}
|
||||||
self.tokens_valid = False
|
self.tokens_valid = False
|
||||||
self.permissions_complete = False
|
self.permissions_complete = False
|
||||||
self.has_backup = False
|
self.has_backup = bool(info)
|
||||||
|
|
||||||
# Check what we have
|
# Check what we have
|
||||||
has_d_token = bool(self.config.discord_bot_token)
|
has_d_token = bool(self.config.discord_bot_token)
|
||||||
|
|
@ -542,11 +543,11 @@ class OperationPane(Container):
|
||||||
|
|
||||||
if self.view_mode == "backup":
|
if self.view_mode == "backup":
|
||||||
self.tokens_valid = bool(discord_ok)
|
self.tokens_valid = bool(discord_ok)
|
||||||
if self.tokens_valid:
|
# Check for backup regardless of token validity
|
||||||
info = self._get_backup_info()
|
info = self._get_backup_info()
|
||||||
if info:
|
if info:
|
||||||
self.validation_results["backup_info_text"] = f"Last backup: [cyan]{info}[/cyan]"
|
self.validation_results["backup_info_text"] = f"Last backup: [cyan]{info}[/cyan]"
|
||||||
self.has_backup = True
|
self.has_backup = True
|
||||||
else:
|
else:
|
||||||
target_ok = v.get("target_token") and v.get("target_community")
|
target_ok = v.get("target_token") and v.get("target_community")
|
||||||
self.tokens_valid = bool(discord_ok and target_ok)
|
self.tokens_valid = bool(discord_ok and target_ok)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue