SXXXXXXX_FlightMonitor/flightmonitor/gui/dialogs/maintenance_window.py

308 lines
14 KiB
Python

# FlightMonitor/gui/dialogs/maintenance_window.py
"""
Provides a maintenance window for managing stored application data,
such as daily recordings, scan sessions, and caches.
"""
import tkinter as tk
from tkinter import ttk, messagebox
from typing import Optional, Any, List
from datetime import datetime, timezone
from flightmonitor.utils.logger import get_logger
module_logger = get_logger(__name__)
class MaintenanceWindow(tk.Toplevel):
"""
A Toplevel window for data maintenance tasks.
"""
def __init__(self, parent: tk.Tk, controller: Any):
"""
Initializes the MaintenanceWindow.
Args:
parent: The parent window (the root Tk instance).
controller: The application controller to delegate actions.
"""
super().__init__(parent)
self.title("Data Maintenance")
self.controller = controller
self.geometry("800x600")
self.minsize(700, 500)
# --- Tkinter Variables for dynamic labels ---
self.db_size_var = tk.StringVar(value="Calculating...")
self.cache_size_var = tk.StringVar(value="Calculating...")
# --- Widget References ---
self.recordings_tree: Optional[ttk.Treeview] = None
self.sessions_tree: Optional[ttk.Treeview] = None
main_frame = ttk.Frame(self, padding="10")
main_frame.pack(fill=tk.BOTH, expand=True)
notebook = ttk.Notebook(main_frame)
notebook.pack(fill=tk.BOTH, expand=True)
self._create_recordings_tab(notebook)
self._create_sessions_tab(notebook)
self._create_general_tab(notebook)
self.protocol("WM_DELETE_WINDOW", self.destroy)
self.center_window()
self.after(100, self._populate_all_tabs)
module_logger.info("MaintenanceWindow initialized.")
def _populate_all_tabs(self):
"""Initial population of all data tabs."""
self._populate_recordings_tab()
self._populate_sessions_tab()
self._populate_general_tab()
def _create_recordings_tab(self, notebook: ttk.Notebook):
"""Creates the tab for managing daily recording database files."""
container = ttk.Frame(notebook, padding="10")
notebook.add(container, text="Daily Recordings")
container.rowconfigure(1, weight=1)
container.columnconfigure(0, weight=1)
controls_frame = ttk.Frame(container)
controls_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(
controls_frame, text="Refresh List", command=self._populate_recordings_tab
).pack(side=tk.LEFT)
ttk.Button(
controls_frame,
text="Delete Selected Day(s)",
command=self._delete_selected_recordings,
).pack(side=tk.LEFT, padx=5)
tree_frame = ttk.Frame(container)
tree_frame.grid(row=1, column=0, sticky="nsew")
tree_frame.rowconfigure(0, weight=1)
tree_frame.columnconfigure(0, weight=1)
self.recordings_tree = ttk.Treeview(
tree_frame,
columns=("date", "size", "flights", "positions"),
show="headings",
)
self.recordings_tree.grid(row=0, column=0, sticky="nsew")
self.recordings_tree.heading("date", text="Date")
self.recordings_tree.column("date", width=120, anchor=tk.W)
self.recordings_tree.heading("size", text="File Size")
self.recordings_tree.column("size", width=100, anchor=tk.E)
self.recordings_tree.heading("flights", text="Flights Count")
self.recordings_tree.column("flights", width=100, anchor=tk.E)
self.recordings_tree.heading("positions", text="Positions Count")
self.recordings_tree.column("positions", width=120, anchor=tk.E)
vsb = ttk.Scrollbar(
tree_frame, orient="vertical", command=self.recordings_tree.yview
)
vsb.grid(row=0, column=1, sticky="ns")
self.recordings_tree.configure(yscrollcommand=vsb.set)
def _populate_recordings_tab(self):
"""Fetches and displays the daily recording summaries."""
if not self.recordings_tree or not self.controller:
return
for item in self.recordings_tree.get_children():
self.recordings_tree.delete(item)
summaries = self.controller.get_daily_recordings_summary()
for summary in summaries:
date = summary.get("date", "N/A")
size_mb = summary.get("file_size_mb", 0)
flights = summary.get("flights_count", 0)
positions = summary.get("positions_count", 0)
self.recordings_tree.insert(
"", "end", values=(date, f"{size_mb:.2f} MB", flights, positions), iid=date
)
def _delete_selected_recordings(self):
"""Deletes the selected daily database files."""
selected_items = self.recordings_tree.selection()
if not selected_items:
messagebox.showwarning("No Selection", "Please select one or more dates to delete.", parent=self)
return
dates_to_delete = [self.recordings_tree.item(item, "values")[0] for item in selected_items]
msg = f"Are you sure you want to permanently delete the data for the following {len(dates_to_delete)} day(s)?\n\n- "
msg += "\n- ".join(dates_to_delete)
if messagebox.askyesno("Confirm Deletion", msg, parent=self):
deleted_count = 0
for date_str in dates_to_delete:
if self.controller.delete_daily_recording(date_str):
deleted_count += 1
messagebox.showinfo("Deletion Complete", f"Successfully deleted data for {deleted_count} day(s).", parent=self)
self._populate_recordings_tab()
def _create_sessions_tab(self, notebook: ttk.Notebook):
"""Creates the tab for managing explicit scan sessions."""
container = ttk.Frame(notebook, padding="10")
notebook.add(container, text="Scan Sessions")
container.rowconfigure(1, weight=1)
container.columnconfigure(0, weight=1)
controls_frame = ttk.Frame(container)
controls_frame.grid(row=0, column=0, sticky="ew", pady=(0, 10))
ttk.Button(
controls_frame, text="Refresh List", command=self._populate_sessions_tab
).pack(side=tk.LEFT)
ttk.Button(
controls_frame,
text="Delete Selected Session(s)",
command=self._delete_selected_sessions,
).pack(side=tk.LEFT, padx=5)
tree_frame = ttk.Frame(container)
tree_frame.grid(row=1, column=0, sticky="nsew")
tree_frame.rowconfigure(0, weight=1)
tree_frame.columnconfigure(0, weight=1)
self.sessions_tree = ttk.Treeview(
tree_frame,
columns=("id", "start_time", "end_time", "status", "bbox"),
show="headings",
)
self.sessions_tree.grid(row=0, column=0, sticky="nsew")
self.sessions_tree.heading("id", text="ID")
self.sessions_tree.column("id", width=40, anchor=tk.E)
self.sessions_tree.heading("start_time", text="Start Time (UTC)")
self.sessions_tree.column("start_time", width=150, anchor=tk.W)
self.sessions_tree.heading("end_time", text="End Time (UTC)")
self.sessions_tree.column("end_time", width=150, anchor=tk.W)
self.sessions_tree.heading("status", text="Status")
self.sessions_tree.column("status", width=80, anchor=tk.W)
self.sessions_tree.heading("bbox", text="Bounding Box")
self.sessions_tree.column("bbox", width=250, stretch=True)
vsb = ttk.Scrollbar(
tree_frame, orient="vertical", command=self.sessions_tree.yview
)
vsb.grid(row=0, column=1, sticky="ns")
self.sessions_tree.configure(yscrollcommand=vsb.set)
def _populate_sessions_tab(self):
"""Fetches and displays the explicit scan sessions."""
if not self.sessions_tree or not self.controller:
return
for item in self.sessions_tree.get_children():
self.sessions_tree.delete(item)
sessions = self.controller.get_all_scan_sessions()
for session in sessions:
scan_id = session.get("scan_id")
start_str = datetime.fromtimestamp(session.get("start_timestamp", 0), timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
end_str = datetime.fromtimestamp(session.get("end_timestamp", 0), timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
bbox_str = f"[{session.get('lat_min', 0):.2f}, {session.get('lon_min', 0):.2f}] - [{session.get('lat_max', 0):.2f}, {session.get('lon_max', 0):.2f}]"
self.sessions_tree.insert(
"", "end", values=(
scan_id,
start_str,
end_str,
session.get("status", "N/A"),
bbox_str
), iid=scan_id
)
def _delete_selected_sessions(self):
"""Deletes the selected scan sessions."""
selected_items = self.sessions_tree.selection()
if not selected_items:
messagebox.showwarning("No Selection", "Please select one or more sessions to delete.", parent=self)
return
ids_to_delete = [int(item) for item in selected_items]
msg = f"Are you sure you want to permanently delete the following {len(ids_to_delete)} session(s)?\n\n- Session IDs: {', '.join(map(str, ids_to_delete))}"
if messagebox.askyesno("Confirm Deletion", msg, parent=self):
deleted_count = 0
for scan_id in ids_to_delete:
if self.controller.delete_scan_session(scan_id):
deleted_count += 1
messagebox.showinfo("Deletion Complete", f"Successfully deleted {deleted_count} session(s).", parent=self)
self._populate_sessions_tab()
def _create_general_tab(self, notebook: ttk.Notebook):
"""Creates the tab for general cleanup tasks."""
container = ttk.Frame(notebook, padding="10")
notebook.add(container, text="General Maintenance")
status_frame = ttk.LabelFrame(container, text="Storage & Cache Status", padding=10)
status_frame.pack(fill=tk.X, pady=(0, 10))
status_frame.columnconfigure(1, weight=1)
ttk.Label(status_frame, text="Aircraft DB Size:").grid(row=0, column=0, sticky="w", padx=(0, 5))
ttk.Label(status_frame, textvariable=self.db_size_var).grid(row=0, column=1, sticky="w")
ttk.Label(status_frame, text="Map Cache Size:").grid(row=1, column=0, sticky="w", padx=(0, 5), pady=(5,0))
ttk.Label(status_frame, textvariable=self.cache_size_var).grid(row=1, column=1, sticky="w", pady=(5,0))
ttk.Button(status_frame, text="Refresh Sizes", command=self._populate_general_tab).grid(row=0, column=2, rowspan=2, padx=(20,0))
ac_db_frame = ttk.LabelFrame(container, text="Aircraft Database Actions", padding=10)
ac_db_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(ac_db_frame, text="Clear Entire Aircraft Database...", command=self._clear_aircraft_db).pack(pady=5, anchor="w")
ttk.Button(ac_db_frame, text="Clear Scan History...", command=self._clear_scan_history).pack(pady=5, anchor="w")
map_cache_frame = ttk.LabelFrame(container, text="Map Cache Actions", padding=10)
map_cache_frame.pack(fill=tk.X, pady=(0, 10))
ttk.Button(map_cache_frame, text="Clear Map Tile Cache...", command=self._clear_map_cache).pack(pady=5, anchor="w")
def _populate_general_tab(self):
"""Fetches and displays the storage and cache sizes."""
if not self.controller:
return
db_size_str = self.controller.get_aircraft_database_size_info()
self.db_size_var.set(db_size_str)
cache_size_str = self.controller.get_map_tile_cache_size_info()
self.cache_size_var.set(cache_size_str)
def _clear_aircraft_db(self):
if messagebox.askyesno("Confirm Action", "Are you sure you want to delete ALL records from the aircraft database? This action cannot be undone.", parent=self):
if self.controller and self.controller.clear_aircraft_database():
messagebox.showinfo("Success", "Aircraft database has been cleared.", parent=self)
self._populate_general_tab() # Refresh size display
else:
messagebox.showerror("Error", "Failed to clear the aircraft database.", parent=self)
def _clear_scan_history(self):
if messagebox.askyesno("Confirm Action", "Are you sure you want to delete ALL scan history records? This will not delete daily data.", parent=self):
if self.controller and self.controller.clear_scan_history():
messagebox.showinfo("Success", "Scan history has been cleared.", parent=self)
else:
messagebox.showerror("Error", "Failed to clear scan history.", parent=self)
def _clear_map_cache(self):
if messagebox.askyesno("Confirm Action", "Are you sure you want to delete all cached map tiles? They will be re-downloaded as needed.", parent=self):
if self.controller and self.controller.clear_map_tile_cache():
messagebox.showinfo("Success", "Map tile cache has been cleared.", parent=self)
self._populate_general_tab() # Refresh size display
else:
messagebox.showerror("Error", "Failed to clear the map tile cache.", parent=self)
def center_window(self):
"""Centers the window on the screen."""
self.update_idletasks()
width = self.winfo_width()
height = self.winfo_height()
x = (self.winfo_screenwidth() // 2) - (width // 2)
y = (self.winfo_screenheight() // 2) - (height // 2)
self.geometry(f"{width}x{height}+{x}+{y}")