import sys
import os
import subprocess
import importlib.util
def verify_and_heal_dependencies():
"""
Checks for required packages and installs them if missing.
Bypasses the check if running in a PyInstaller bundle.
"""
# Bypass the check if running in a PyInstaller environment, as it's a sealed binary.
if hasattr(sys, '_MEIPASS'):
print("[*] HEAL_CHECK: PyInstaller environment detected. Skipping dependency check.")
return
print("[*] HEAL_CHECK: Verifying GUI runtime dependencies...")
REQUIRED_PACKAGES = {
"ollama": "ollama",
"httpx": "httpx",
"pydantic": "pydantic",
"psutil": "psutil",
"docker": "docker",
"humanize": "humanize",
"customtkinter": "customtkinter",
"typing_extensions": "typing-extensions",
"psycopg2": "psycopg2-binary", # For PostgreSQL connection
"pgvector": "pgvector", # For vector similarity search
"bs4": "beautifulsoup4" # For HTML parsing in URL ingest
}
for module_name, pip_name in REQUIRED_PACKAGES.items():
if importlib.util.find_spec(module_name) is None:
print(f"[WARN] HEAL_CHECK: Dependency '{module_name}' not found. Attempting auto-heal...")
try:
# Silently install the missing package into the local runtime
subprocess.check_call([sys.executable, "-m", "pip", "install", "--no-cache-dir", pip_name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
print(f"[OK] HEAL_CHECK: Successfully installed '{pip_name}'.")
except subprocess.CalledProcessError:
print(f"[CRITICAL] HEAL_CHECK: Failed to install '{pip_name}'. Please install it manually.")
verify_and_heal_dependencies()
import customtkinter as ctk # pyright: ignore[reportMissingTypeStubs]
import pywinstyles # pyright: ignore[reportMissingImports]
import theme
from typing import Callable, Any, Dict
class DevOpsDashboard(ctk.CTk):
"""
The main visual shell for the DevOps Dashboard application.
"""
def __init__(self):
super().__init__()
# 1. CONFIGURE MAIN WINDOW
self.original_title = "SW1SSKN1FE™ MK6 v4.0.77.0 // INTEGRITY SYSTEM TELEMETRY DECK"
self.title(self.original_title)
self.geometry("1280x800")
ctk.set_appearance_mode("dark")
self.configure(fg_color=theme.OLED_BLUE)
self.after(200, lambda: self.iconbitmap(os.path.join(os.path.dirname(os.path.abspath(__file__)), "star77.ico")) if os.path.exists(os.path.join(os.path.dirname(os.path.abspath(__file__)), "star77.ico")) else None)
# --- FONT INITIALIZATION (Must be after root window creation) ---
self.MONO_FONT = ctk.CTkFont(family=theme.FONT_FAMILY, size=12)
self.MONO_FONT_BOLD = ctk.CTkFont(family=theme.FONT_FAMILY, size=12, weight="bold")
self.TITLE_FONT = ctk.CTkFont(family=theme.FONT_FAMILY, size=18, weight="bold")
self.SUBTITLE_FONT = ctk.CTkFont(family=theme.FONT_FAMILY, size=16, weight="bold")
# --- MICA & FRAMELESS ---
self.overrideredirect(True)
self.after(10, self._apply_mica_style)
# --- LAYOUT CONFIG ---
self.grid_columnconfigure(0, weight=1)
self.grid_rowconfigure(1, weight=1) # Main content (tabs)
self.grid_rowconfigure(2, weight=0) # Console
self.grid_rowconfigure(3, weight=0) # Footer
# --- UI INITIALIZATION ---
self._build_title_bar()
self._build_slide_in_menu()
# The original `create_header_frame` is now the console, `create_body_frame` is the tab view.
# The order of creation matters for widget availability.
self.create_body_frame()
self.create_footer_frame()
self.create_header_frame()
# --- State for efficient table updates ---
self.container_rows = []
self.premium_tools_frame: ctk.CTkFrame | None = None
# --- Callback Placeholders ---
self.on_container_action = None
self.on_wu_context_action = None
# --- State for WU table updates ---
self.wu_rows = []
self._build_wu_context_menu()
# --- Callback for close button ---
self.on_exit_callback = None
# --- Placeholder for buttons configured later ---
self.run_indexer_button: ctk.CTkButton | None = None
self.run_pdf_ingest_button: ctk.CTkButton | None = None
self.run_search_button: ctk.CTkButton | None = None
self.run_url_ingest_button: ctk.CTkButton | None = None
self.export_search_button: ctk.CTkButton | None = None
self.on_search_result_clicked: Callable[[str, str], None] | None = None
def _apply_mica_style(self):
"""Applies the Mica effect to the window."""
try:
pywinstyles.apply_style(self, "mica")
self.append_log("[MICA_STYLE] Mica fluent design initialized.")
except Exception as e:
self.append_log(f"[MICA_STYLE_ERROR] Failed to apply Mica style: {e}")
def _build_title_bar(self):
"""Builds the custom title bar for the frameless window."""
self.title_bar = ctk.CTkFrame(self, height=30, fg_color=theme.FRAME_BLUE, corner_radius=0)
self.title_bar.grid(row=0, column=0, sticky="ew")
self.title_bar.grid_columnconfigure(1, weight=1)
self.menu_button = ctk.CTkButton(self.title_bar, text="☰", width=30, command=self.toggle_menu, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.menu_button.grid(row=0, column=0, padx=5, pady=5)
title_label = ctk.CTkLabel(self.title_bar, text=self.original_title, font=self.MONO_FONT)
title_label.grid(row=0, column=1, sticky="w", padx=10)
close_button = ctk.CTkButton(self.title_bar, text="✕", width=30, command=self.on_exit_clicked_wrapper, fg_color="#D32F2F", hover_color="#E53935")
close_button.grid(row=0, column=4, padx=(2, 5), pady=5)
maximize_button = ctk.CTkButton(self.title_bar, text="□", width=30, command=self.toggle_maximize)
maximize_button.grid(row=0, column=3, padx=2, pady=5)
minimize_button = ctk.CTkButton(self.title_bar, text="_", width=30, command=self.iconify)
minimize_button.grid(row=0, column=2, padx=2, pady=5)
self.title_bar.bind("<ButtonPress-1>", self.start_move)
self.title_bar.bind("<ButtonRelease-1>", self.stop_move)
self.title_bar.bind("<B1-Motion>", self.do_move)
title_label.bind("<ButtonPress-1>", self.start_move)
title_label.bind("<ButtonRelease-1>", self.stop_move)
title_label.bind("<B1-Motion>", self.do_move)
def _build_slide_in_menu(self):
"""Builds the frame for the slide-in menu."""
self.menu_width = 300
self.menu_visible = False
# Reverted invalid color format. Tkinter does not support 8-digit hex codes with an alpha channel.
# The previous color '#0D1117BF' caused a TclError.
self.menu_frame = ctk.CTkFrame(self, width=self.menu_width, fg_color=theme.FRAME_BLUE, corner_radius=0, border_width=1, border_color="gray20")
# Initially placed off-screen
self.menu_frame.place(x=-self.menu_width, y=30, relheight=1.0, anchor="nw")
self.menu_scroll_frame = ctk.CTkScrollableFrame(self.menu_frame, fg_color="transparent", label_text="Command Menu", label_font=self.MONO_FONT_BOLD)
self.menu_scroll_frame.pack(expand=True, fill="both", padx=5, pady=5)
def on_exit_clicked_wrapper(self):
if self.on_exit_callback:
self.on_exit_callback()
def start_move(self, event):
self.x = event.x
self.y = event.y
def stop_move(self, event):
self.x = None
self.y = None
def do_move(self, event):
deltax = event.x - self.x
deltay = event.y - self.y
x = self.winfo_x() + deltax
y = self.winfo_y() + deltay
self.geometry(f"+{x}+{y}")
def toggle_maximize(self):
if self.wm_state() == 'zoomed':
self.wm_state('normal')
else:
self.wm_state('zoomed')
def toggle_menu(self):
if self.menu_visible:
self.menu_frame.place_forget()
else:
self.menu_frame.place(x=0, y=30, relheight=1.0, anchor="nw")
self.menu_visible = not self.menu_visible
def create_header_frame(self):
self.header_frame = ctk.CTkFrame(self, corner_radius=0, fg_color=theme.FRAME_BLUE, height=140)
self.header_frame.grid(row=2, column=0, sticky="ew", padx=10, pady=(5, 0))
# Configure header grid (2 rows, 1 column)
self.header_frame.grid_columnconfigure(0, weight=1)
self.header_frame.grid_rowconfigure(1, weight=1)
# --- Console Log ---
console_label = ctk.CTkLabel(self.header_frame, text="[O] SYSTEM TELEMETRY CONSOLE", font=self.MONO_FONT_BOLD, anchor="w")
console_label.grid(row=0, column=0, padx=20, pady=(5, 0), sticky="w")
self.console_log = ctk.CTkTextbox(self.header_frame, height=120, font=self.MONO_FONT)
self.console_log.grid(row=1, column=0, padx=20, pady=(5, 10), sticky="nsew")
self.console_log.insert("0.0", "LPEWB Console Initialized...\n")
self.console_log.configure(state="disabled")
def create_body_frame(self):
"""Creates the expanding central body with DB metrics and Docker containers."""
self.body_frame = ctk.CTkFrame(self, fg_color="transparent")
self.body_frame.grid(row=1, column=0, sticky="nsew", padx=10, pady=5)
self.body_frame.grid_columnconfigure(0, weight=1); self.body_frame.grid_rowconfigure(0, weight=1)
self.tab_view = ctk.CTkTabview(self.body_frame, fg_color=theme.FRAME_BLUE, anchor="w", segmented_button_selected_color=theme.NEON_MAGENTA, segmented_button_selected_hover_color=theme.NEON_MAGENTA_HOVER)
self.tab_view.grid(row=0, column=0, sticky="nsew")
# Create the tabs
self.tab_view.add("DOCKER")
self.tab_view.add("DB & SERVICES")
self.tab_view.add("WU MATRIX")
self.tab_view.add("CRAWL & INGEST")
# Populate each tab
self._build_docker_deck(self.tab_view.tab("DOCKER"))
self._build_database_deck(self.tab_view.tab("DB & SERVICES"))
self._build_wu_deck(self.tab_view.tab("WU MATRIX"))
self._build_ingest_deck(self.tab_view.tab("CRAWL & INGEST"))
# Set the default visible tab to ensure the view is not blank on launch
self.tab_view.set("DOCKER")
def _build_ingest_deck(self, tab_frame):
"""Constructs the UI for the Crawl & Ingest tab."""
tab_frame.grid_columnconfigure(1, weight=1)
tab_frame.grid_rowconfigure(5, weight=1) # Make the results console expand
title = ctk.CTkLabel(tab_frame, text="Crawl & Ingest Engine", font=self.SUBTITLE_FONT)
title.grid(row=0, column=0, columnspan=3, padx=15, pady=(15, 10), sticky="w")
# Source Directory
ctk.CTkLabel(tab_frame, text="Source Directory:", anchor="w", font=self.MONO_FONT).grid(row=1, column=0, padx=(15, 5), pady=10, sticky="w")
self.pdf_source_path_entry = ctk.CTkEntry(tab_frame, placeholder_text="Path to raw PDFs...", font=self.MONO_FONT)
self.pdf_source_path_entry.grid(row=1, column=1, padx=5, pady=10, sticky="ew")
self.pdf_browse_source_button = ctk.CTkButton(tab_frame, text="Browse...", font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.pdf_browse_source_button.grid(row=1, column=2, padx=(5, 15), pady=10)
# Destination Directory
ctk.CTkLabel(tab_frame, text="Destination Directory:", anchor="w", font=self.MONO_FONT).grid(row=2, column=0, padx=(15, 5), pady=10, sticky="w")
self.pdf_dest_path_entry = ctk.CTkEntry(tab_frame, placeholder_text="Path for cleaned text files...", font=self.MONO_FONT)
self.pdf_dest_path_entry.grid(row=2, column=1, padx=5, pady=10, sticky="ew")
self.pdf_browse_dest_button = ctk.CTkButton(tab_frame, text="Browse...", font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.pdf_browse_dest_button.grid(row=2, column=2, padx=(5, 15), pady=10)
# --- Action Buttons ---
pdf_button_frame = ctk.CTkFrame(tab_frame, fg_color="transparent")
pdf_button_frame.grid(row=3, column=1, columnspan=2, padx=(5, 15), pady=0, sticky="e")
self.pdf_archive_checkbox = ctk.CTkCheckBox(pdf_button_frame, text="Archive after processing", font=self.MONO_FONT)
self.pdf_archive_checkbox.pack(side="left", padx=10)
self.pdf_clear_paths_button = ctk.CTkButton(pdf_button_frame, text="Clear Paths", font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.pdf_clear_paths_button.pack(side="right")
# --- LLM Prompt Area for future Crawl/Analysis ---
prompt_frame = ctk.CTkFrame(tab_frame, fg_color="transparent")
prompt_frame.grid(row=4, column=0, columnspan=3, padx=15, pady=10, sticky="nsew")
prompt_frame.grid_columnconfigure(0, weight=1)
self.llm_prompt_entry = ctk.CTkTextbox(prompt_frame, height=120, font=self.MONO_FONT, wrap="word")
self.llm_prompt_entry.grid(row=0, column=0, sticky="nsew")
self.llm_prompt_entry.insert("0.0", "Enter prompt for crawling or data analysis here...")
llm_button_frame = ctk.CTkFrame(prompt_frame, fg_color="transparent")
llm_button_frame.grid(row=0, column=1, padx=(10, 0), sticky="ns")
self.llm_stop_button = ctk.CTkButton(llm_button_frame, text="Stop", font=self.MONO_FONT, state="disabled")
self.llm_stop_button.pack(pady=(0, 5), fill="x")
self.llm_repeat_button = ctk.CTkButton(llm_button_frame, text="Repeat", font=self.MONO_FONT)
self.llm_repeat_button.pack(pady=5, fill="x")
def _build_docker_deck(self, tab_frame):
"""Constructs the UI elements for the Docker container management tab."""
tab_frame.grid_columnconfigure(0, weight=1)
tab_frame.grid_rowconfigure(0, weight=1)
self.docker_table_frame = ctk.CTkScrollableFrame(tab_frame, label_text="Live Docker Containers", fg_color=theme.FRAME_BLUE)
self.docker_table_frame.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
# Configure columns inside the scrollable frame
self.docker_table_frame.grid_columnconfigure(0, weight=2) # Name
self.docker_table_frame.grid_columnconfigure(1, weight=1) # Status
self.docker_table_frame.grid_columnconfigure(2, weight=1) # Uptime
self.docker_table_frame.grid_columnconfigure(3, weight=3) # Image
self.docker_table_frame.grid_columnconfigure(4, weight=0) # Actions
headers = ["Container Name", "Status", "Uptime", "Image", "Actions"]
for i, header in enumerate(headers):
label = ctk.CTkLabel(self.docker_table_frame, text=header, font=self.MONO_FONT_BOLD, anchor="w")
label.grid(row=0, column=i, padx=15, pady=8, sticky="ew")
def _build_database_deck(self, tab_frame):
"""Constructs the UI elements for the Database and Core Services tab."""
tab_frame.grid_columnconfigure(0, weight=0)
tab_frame.grid_columnconfigure(1, weight=1)
tab_frame.grid_rowconfigure(8, weight=1) # Spacer row to push content up
db_title = ctk.CTkLabel(tab_frame, text="Database Telemetry", font=self.SUBTITLE_FONT)
db_title.grid(row=0, column=0, columnspan=2, padx=15, pady=(15, 10), sticky="w")
ctk.CTkLabel(tab_frame, text="DB Status:", anchor="w", font=self.MONO_FONT).grid(row=1, column=0, padx=(15, 5), pady=5, sticky="w")
self.db_status_label = ctk.CTkLabel(tab_frame, text="Checking...", anchor="w", font=self.MONO_FONT_BOLD)
self.db_status_label.grid(row=1, column=1, padx=5, pady=5, sticky="ew")
ctk.CTkLabel(tab_frame, text="pgvector:", anchor="w", font=self.MONO_FONT).grid(row=2, column=0, padx=(15, 5), pady=5, sticky="w")
self.pgvector_status_label = ctk.CTkLabel(tab_frame, text="Checking...", anchor="w", font=self.MONO_FONT)
self.pgvector_status_label.grid(row=2, column=1, padx=5, pady=5, sticky="ew")
ctk.CTkLabel(tab_frame, text="AI Memories:", anchor="w", font=self.MONO_FONT).grid(row=3, column=0, padx=(15, 5), pady=5, sticky="w")
self.record_count_label = ctk.CTkLabel(tab_frame, text="Checking...", anchor="w", font=self.MONO_FONT)
self.record_count_label.grid(row=3, column=1, padx=5, pady=5, sticky="ew")
separator = ctk.CTkFrame(tab_frame, height=2, fg_color="gray20")
separator.grid(row=4, column=0, columnspan=2, sticky="ew", padx=15, pady=15)
services_title = ctk.CTkLabel(tab_frame, text="Core Services", font=self.SUBTITLE_FONT)
services_title.grid(row=5, column=0, columnspan=2, padx=15, pady=(0, 10), sticky="w")
ctk.CTkLabel(tab_frame, text="Ollama Engine (11434):", anchor="w", font=self.MONO_FONT).grid(row=6, column=0, padx=(15, 5), pady=5, sticky="w")
self.ollama_status_label = ctk.CTkLabel(tab_frame, text="Checking...", anchor="w", font=self.MONO_FONT_BOLD)
self.ollama_status_label.grid(row=6, column=1, padx=5, pady=5, sticky="ew")
ctk.CTkLabel(tab_frame, text="Vertex AI (8001):", anchor="w", font=self.MONO_FONT).grid(row=7, column=0, padx=(15, 5), pady=5, sticky="w")
self.vertex_status_label = ctk.CTkLabel(tab_frame, text="Checking...", anchor="w", font=self.MONO_FONT_BOLD)
self.vertex_status_label.grid(row=7, column=1, padx=5, pady=5, sticky="ew")
def _build_wu_deck(self, tab_frame):
"""Constructs the placeholder UI for the Windows Update Matrix tab."""
tab_frame.grid_columnconfigure(0, weight=1)
tab_frame.grid_rowconfigure(1, weight=1) # Table should expand
tab_frame.grid_rowconfigure(2, weight=0) # Console is fixed height
self.wu_table_frame = ctk.CTkScrollableFrame(tab_frame, label_text="Available Updates", fg_color=theme.FRAME_BLUE)
self.wu_table_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew")
self.wu_table_frame.grid_columnconfigure(0, weight=0) # Checkbox
self.wu_table_frame.grid_columnconfigure(1, weight=3) # Title
self.wu_table_frame.grid_columnconfigure(2, weight=1) # KB
self.wu_table_frame.grid_columnconfigure(3, weight=0) # Size
headers = ["", "Title", "KB Article", "Size (MB)"]
for i, header in enumerate(headers):
label = ctk.CTkLabel(self.wu_table_frame, text=header, font=self.MONO_FONT_BOLD, anchor="w")
label.grid(row=0, column=i, padx=10, pady=5, sticky="ew")
self.wu_console = ctk.CTkTextbox(tab_frame, height=100, font=self.MONO_FONT)
self.wu_console.grid(row=2, column=0, padx=10, pady=(0, 10), sticky="ew")
self.wu_console.insert("0.0", "Windows Update Matrix standby...\n")
self.wu_console.configure(state="disabled")
def _build_ingest_deck(self, tab_frame: ctk.CTkFrame):
"""Constructs the UI for the Crawl & Ingest tab with sub-tabs for different ingestion types."""
tab_frame.grid_columnconfigure(0, weight=1)
tab_frame.grid_rowconfigure(0, weight=1)
# Create a sub-tab view inside the main "CRAWL & INGEST" tab
ingest_tab_view = ctk.CTkTabview(tab_frame, fg_color="transparent", anchor="w",
segmented_button_fg_color=theme.FRAME_BLUE,
segmented_button_selected_color=theme.NEON_MAGENTA,
segmented_button_selected_hover_color=theme.NEON_MAGENTA_HOVER)
ingest_tab_view.pack(expand=True, fill="both", padx=5, pady=5)
ingest_tab_view.add("Workspace Indexer")
ingest_tab_view.add("PDF Ingestion")
ingest_tab_view.add("Web URL Ingestion")
ingest_tab_view.add("Vector Search")
ingest_tab_view.set("Workspace Indexer")
# --- Populate Workspace Indexer Tab ---
self._build_workspace_indexer_sub_tab(ingest_tab_view.tab("Workspace Indexer"))
# --- Populate PDF Ingestion Tab ---
self._build_pdf_ingestion_sub_tab(ingest_tab_view.tab("PDF Ingestion"))
# --- Populate Web URL Ingestion Tab ---
self._build_url_ingestion_sub_tab(ingest_tab_view.tab("Web URL Ingestion"))
# --- Populate Vector Search Tab ---
self._build_vector_search_sub_tab(ingest_tab_view.tab("Vector Search"))
def _build_wu_context_menu(self):
"""Creates the context menu for the WU table. It is populated dynamically."""
import tkinter as tk
self.wu_context_menu = tk.Menu(self, tearoff=0)
def _show_wu_context_menu(self, event, update_info: dict):
"""Populates and displays the context menu for a Windows Update item."""
if not self.on_wu_context_action:
return
# Clear previous menu items to repopulate
self.wu_context_menu.delete(0, "end")
kb = update_info.get('KB', 'N/A')
self.wu_context_menu.add_command(label=f"Show Info for {kb}", command=lambda: self.on_wu_context_action(kb, 'info', update_info))
self.wu_context_menu.add_command(label=f"Download MSU for {kb}", command=lambda: self.on_wu_context_action(kb, 'download', update_info))
self.wu_context_menu.add_separator()
self.wu_context_menu.add_command(label=f"Install {kb}", command=lambda: self.on_wu_context_action(kb, 'install', update_info))
self.wu_context_menu.add_separator()
self.wu_context_menu.add_command(label=f"Hide {kb}", command=lambda: self.on_wu_context_action(kb, 'hide', update_info))
self.wu_context_menu.add_command(label=f"Unhide {kb}", command=lambda: self.on_wu_context_action(kb, 'unhide', update_info))
try:
self.wu_context_menu.tk_popup(event.x_root, event.y_root)
finally:
self.wu_context_menu.grab_release()
def _build_workspace_indexer_sub_tab(self, sub_tab_frame: ctk.CTkFrame):
"""Builds the UI for the Workspace Indexer sub-tab."""
sub_tab_frame.grid_columnconfigure(0, weight=1)
sub_tab_frame.grid_rowconfigure(1, weight=1)
# --- Top frame for info and actions ---
top_frame = ctk.CTkFrame(sub_tab_frame, fg_color="transparent")
top_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
top_frame.grid_columnconfigure(1, weight=1)
info_text = "Run a deep scan of the entire workspace. This process will read all relevant code and documentation files, generate vector embeddings, and store them in the AI's memory matrix for enhanced contextual awareness."
ctk.CTkLabel(top_frame, text=info_text, wraplength=700, justify="left", font=self.MONO_FONT).grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 10))
self.run_indexer_button = ctk.CTkButton(top_frame, text="Run Full Workspace Index", font=self.MONO_FONT_BOLD, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.run_indexer_button.grid(row=1, column=0, sticky="w")
# --- Console for indexer output ---
self.indexer_console = ctk.CTkTextbox(sub_tab_frame, font=self.MONO_FONT)
self.indexer_console.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="nsew")
self.indexer_console.insert("0.0", "Workspace Indexer standby...\n")
self.indexer_console.configure(state="disabled")
def _build_pdf_ingestion_sub_tab(self, sub_tab_frame: ctk.CTkFrame):
"""Builds the UI for the PDF Ingestion sub-tab."""
sub_tab_frame.grid_columnconfigure(1, weight=1)
sub_tab_frame.grid_rowconfigure(2, weight=1) # Make the console expand
# --- Path selection ---
path_frame = ctk.CTkFrame(sub_tab_frame, fg_color="transparent")
path_frame.grid(row=0, column=0, columnspan=3, padx=10, pady=10, sticky="ew")
path_frame.grid_columnconfigure(1, weight=1)
ctk.CTkLabel(path_frame, text="Source Directory:", anchor="w", font=self.MONO_FONT).grid(row=0, column=0, padx=(5, 5), pady=10, sticky="w")
self.pdf_source_path_entry = ctk.CTkEntry(path_frame, placeholder_text="Path to raw PDFs...", font=self.MONO_FONT)
self.pdf_source_path_entry.grid(row=0, column=1, padx=5, pady=10, sticky="ew")
self.pdf_browse_source_button = ctk.CTkButton(path_frame, text="Browse...", font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.pdf_browse_source_button.grid(row=0, column=2, padx=(5, 5), pady=10)
ctk.CTkLabel(path_frame, text="Destination Directory:", anchor="w", font=self.MONO_FONT).grid(row=1, column=0, padx=(5, 5), pady=10, sticky="w")
self.pdf_dest_path_entry = ctk.CTkEntry(path_frame, placeholder_text="Path for cleaned text files...", font=self.MONO_FONT)
self.pdf_dest_path_entry.grid(row=1, column=1, padx=5, pady=10, sticky="ew")
self.pdf_browse_dest_button = ctk.CTkButton(path_frame, text="Browse...", font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.pdf_browse_dest_button.grid(row=1, column=2, padx=(5, 5), pady=10)
# --- Action Buttons ---
action_frame = ctk.CTkFrame(sub_tab_frame, fg_color="transparent")
action_frame.grid(row=1, column=0, columnspan=3, padx=15, pady=0, sticky="ew")
self.run_pdf_ingest_button = ctk.CTkButton(action_frame, text="Run PDF Ingestion", font=self.MONO_FONT_BOLD, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.run_pdf_ingest_button.pack(side="left", padx=(0, 10))
self.pdf_archive_checkbox = ctk.CTkCheckBox(action_frame, text="Archive after processing", font=self.MONO_FONT)
self.pdf_archive_checkbox.pack(side="left", padx=10)
self.pdf_clear_paths_button = ctk.CTkButton(action_frame, text="Clear Paths", font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.pdf_clear_paths_button.pack(side="left", padx=10)
# --- Results Console ---
self.pdf_ingest_console = ctk.CTkTextbox(sub_tab_frame, font=self.MONO_FONT)
self.pdf_ingest_console.grid(row=2, column=0, columnspan=3, padx=10, pady=10, sticky="nsew")
self.pdf_ingest_console.insert("0.0", "PDF Ingestion Engine standby...\n")
self.pdf_ingest_console.configure(state="disabled")
def _build_url_ingestion_sub_tab(self, sub_tab_frame: ctk.CTkFrame):
"""Builds the UI for the Web URL Ingestion sub-tab."""
sub_tab_frame.grid_columnconfigure(0, weight=1)
sub_tab_frame.grid_rowconfigure(1, weight=1)
# --- Top frame for URL input and actions ---
top_frame = ctk.CTkFrame(sub_tab_frame, fg_color="transparent")
top_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
top_frame.grid_columnconfigure(0, weight=1)
self.url_ingest_entry = ctk.CTkEntry(top_frame, placeholder_text="Enter a single URL to scrape and ingest...", font=self.MONO_FONT)
self.url_ingest_entry.grid(row=0, column=0, sticky="ew", padx=(0, 10))
self.run_url_ingest_button = ctk.CTkButton(top_frame, text="Ingest URL", font=self.MONO_FONT_BOLD, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.run_url_ingest_button.grid(row=0, column=1, sticky="e")
# --- Console for URL ingest output ---
self.url_ingest_console = ctk.CTkTextbox(sub_tab_frame, font=self.MONO_FONT)
self.url_ingest_console.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="nsew")
self.url_ingest_console.insert("0.0", "Web URL Ingestion standby...\n")
self.url_ingest_console.configure(state="disabled")
def _build_vector_search_sub_tab(self, sub_tab_frame: ctk.CTkFrame):
"""Builds the UI for the Vector Search sub-tab."""
sub_tab_frame.grid_columnconfigure(0, weight=1)
sub_tab_frame.grid_rowconfigure(3, weight=1) # Results console should expand
# --- Top frame for search input and actions ---
top_frame = ctk.CTkFrame(sub_tab_frame, fg_color="transparent")
top_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ew")
top_frame.grid_columnconfigure(0, weight=1)
self.search_query_entry = ctk.CTkEntry(top_frame, placeholder_text="Enter your search query here...", font=self.MONO_FONT)
self.search_query_entry.grid(row=0, column=0, sticky="ew", padx=(0, 10))
self.run_search_button = ctk.CTkButton(top_frame, text="Search", font=self.MONO_FONT_BOLD, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.run_search_button.grid(row=0, column=1, sticky="e")
# --- Limit slider ---
limit_frame = ctk.CTkFrame(sub_tab_frame, fg_color="transparent")
limit_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew")
ctk.CTkLabel(limit_frame, text="Result Limit:", font=self.MONO_FONT).pack(side="left")
self.search_limit_slider = ctk.CTkSlider(limit_frame, from_=1, to=20, number_of_steps=19)
self.search_limit_slider.set(5)
self.search_limit_slider.pack(side="left", fill="x", expand=True, padx=10)
self.search_limit_label = ctk.CTkLabel(limit_frame, text="5", font=self.MONO_FONT)
self.search_limit_label.pack(side="left")
self.search_limit_slider.configure(command=lambda v: self.search_limit_label.configure(text=str(int(v))))
# --- Action Buttons ---
action_frame = ctk.CTkFrame(sub_tab_frame, fg_color="transparent")
action_frame.grid(row=2, column=0, padx=10, pady=(5,0), sticky="w")
self.export_search_button = ctk.CTkButton(action_frame, text="Export Results to CSV", font=self.MONO_FONT, state="disabled", fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
self.export_search_button.pack(side="left")
# --- Console for search results ---
self.search_results_console = ctk.CTkTextbox(sub_tab_frame, font=self.MONO_FONT)
self.search_results_console.grid(row=3, column=0, padx=10, pady=(0, 10), sticky="nsew")
self.search_results_console.insert("0.0", "Vector search results will appear here...\n")
self.search_results_console.configure(state="disabled")
# --- Configure tags for interactive results ---
self.search_results_console.tag_config("hyperlink", foreground=theme.NEON_MAGENTA, underline=True)
self.search_results_console.tag_bind("hyperlink", "<Enter>", lambda e: self.search_results_console.config(cursor="hand2"))
self.search_results_console.tag_bind("hyperlink", "<Leave>", lambda e: self.search_results_console.config(cursor=""))
def update_db_stats(self, status: str, pgvector_status: str, record_count: str):
"""Updates the labels in the Database Telemetry panel."""
status_color = "#2ECC71" if status.lower() == "connected" else "#E74C3C"
pgvector_color = "#2ECC71" if pgvector_status.lower() == "active" else "#F39C12"
self.db_status_label.configure(text=status, text_color=status_color)
self.pgvector_status_label.configure(text=pgvector_status, text_color=pgvector_color)
self.record_count_label.configure(text=str(record_count))
def update_core_services_status(self, ollama_status: str, vertex_status: str):
"""Updates the labels in the Core Services panel."""
ollama_color = "#2ECC71" if ollama_status.lower() == "online" else "#E74C3C"
vertex_color = "#2ECC71" if vertex_status.lower() == "online" else "#E74C3C"
self.ollama_status_label.configure(text=ollama_status, text_color=ollama_color)
self.vertex_status_label.configure(text=vertex_status, text_color=vertex_color)
def _on_wu_checkbox_toggle(self):
"""Enables or disables the install button based on selection."""
is_any_selected = any(row['checkbox'].get() for row in self.wu_rows)
new_state = "normal" if is_any_selected else "disabled"
self.wu_install_button.configure(state=new_state)
self.wu_hide_button.configure(state=new_state)
self.wu_unhide_button.configure(state=new_state)
def get_selected_updates(self) -> list[str]:
"""Returns a list of KB IDs for the selected updates."""
selected_kbs: list[str] = []
for row in self.wu_rows:
if row['checkbox'].get():
# Ensure 'KB' key exists and is not None before appending
if 'KB' in row and row['KB']:
selected_kbs.append(row['KB'])
return selected_kbs
def update_wu_table(self, update_list: list[dict[str, Any]]):
"""Efficiently updates the Windows Update table view by reusing or creating widgets as needed."""
# 1. Destroy extra rows if the new list is shorter
for row in self.wu_rows:
for widget_key in ['checkbox', 'title_label', 'kb_label', 'size_label']:
if widget_key in row and row[widget_key]:
row[widget_key].destroy()
self.wu_rows = self.wu_rows[:len(update_list)]
# 2. Update existing rows or create new ones
for i, update_data in enumerate(update_list):
grid_row_index = i + 1 # Account for header row
if i < len(self.wu_rows):
# --- UPDATE EXISTING ROW ---
row_info = self.wu_rows[i]
# Update data payload first
row_info.update(update_data)
# Update widget properties
row_info['title_label'].configure(text=update_data.get('Title', 'No Title'))
row_info['kb_label'].configure(text=update_data.get('KB', 'N/A'))
row_info['size_label'].configure(text=str(update_data.get('Size', 0)))
# Re-bind context menu with updated data
for widget in [row_info['title_label'], row_info['kb_label'], row_info['size_label'], row_info['checkbox']]:
widget.bind("<Button-3>", lambda event, info=row_info: self._show_wu_context_menu(event, info))
else:
# --- CREATE NEW ROW ---
checkbox = ctk.CTkCheckBox(self.wu_table_frame, text="", command=self._on_wu_checkbox_toggle)
checkbox.grid(row=grid_row_index, column=0, padx=10, sticky="w")
title_label = ctk.CTkLabel(self.wu_table_frame, text=update_data.get('Title', 'No Title'), anchor="w", wraplength=500, font=self.MONO_FONT)
title_label.grid(row=grid_row_index, column=1, padx=10, pady=5, sticky="ew")
kb_label = ctk.CTkLabel(self.wu_table_frame, text=update_data.get('KB', 'N/A'), anchor="w", font=self.MONO_FONT)
kb_label.grid(row=grid_row_index, column=2, padx=10, pady=5, sticky="ew")
size_label = ctk.CTkLabel(self.wu_table_frame, text=str(update_data.get('Size', 0)), anchor="e", font=self.MONO_FONT)
size_label.grid(row=grid_row_index, column=3, padx=10, pady=5, sticky="ew")
row_info = {'checkbox': checkbox, 'title_label': title_label, 'kb_label': kb_label, 'size_label': size_label, **update_data}
self.wu_rows.append(row_info)
# Bind right-click to all widgets in the new row
for widget in [title_label, kb_label, size_label, checkbox]:
widget.bind("<Button-3>", lambda event, info=row_info: self._show_wu_context_menu(event, info))
self._on_wu_checkbox_toggle()
def update_container_table(self, container_list: list[tuple[str, str, str, str]]):
"""
Efficiently updates the container table view by reusing existing widgets,
creating new ones, or deleting surplus ones.
Args:
container_list (list): A list of tuples containing
(name, status, uptime, image)
"""
# 1. Destroy extra rows if the new list is shorter
for i in range(len(container_list), len(self.container_rows)):
for widget in self.container_rows[i]:
if widget:
widget.destroy()
self.container_rows = self.container_rows[:len(container_list)]
# 2. Update existing rows or create new ones
for row_idx, row_data in enumerate(container_list):
grid_row_index = row_idx + 1 # Account for header row
if row_idx < len(self.container_rows):
# --- UPDATE EXISTING ROW ---
widgets = self.container_rows[row_idx]
name_label, status_label, uptime_label, image_label, actions_frame = widgets
# Update text labels
name_label.configure(text=row_data[0])
status_label.configure(text=row_data[1])
uptime_label.configure(text=row_data[2])
image_label.configure(text=row_data[3])
# Update status color
is_running = str(row_data[1]).strip().lower() == "running"
color = "#2ECC71" if is_running else "#E74C3C"
status_label.configure(text_color=color)
# Update action buttons state
if self.on_container_action and actions_frame:
start_button, stop_button, restart_button = actions_frame.winfo_children()
status = str(row_data[1]).lower()
is_running_or_restarting = 'running' in status or 'restarting' in status
start_button.configure(state="disabled" if is_running_or_restarting else "normal")
stop_button.configure(state="normal" if is_running_or_restarting else "disabled")
restart_button.configure(state="normal" if is_running_or_restarting else "disabled")
else:
# --- CREATE NEW ROW ---
new_widgets = []
# Create text labels
for col_idx, cell_data in enumerate(row_data):
if col_idx == 1: # Status column
is_running = str(cell_data).strip().lower() == "running"
color = "#2ECC71" if is_running else "#E74C3C"
cell_label = ctk.CTkLabel(self.docker_table_frame, text=cell_data, text_color=color, font=self.MONO_FONT_BOLD, anchor="w")
else:
cell_label = ctk.CTkLabel(self.docker_table_frame, text=cell_data, anchor="w", font=self.MONO_FONT)
cell_label.grid(row=grid_row_index, column=col_idx, padx=15, pady=4, sticky="ew")
new_widgets.append(cell_label)
# Create action buttons
actions_frame = None
if self.on_container_action:
container_name = row_data[0]
status = str(row_data[1]).lower()
actions_frame = ctk.CTkFrame(self.docker_table_frame, fg_color="transparent")
actions_frame.grid(row=grid_row_index, column=4, padx=5, pady=0, sticky="e")
actions_frame.grid_columnconfigure((0, 1, 2), weight=0)
start_button = ctk.CTkButton(actions_frame, text="▶", width=30, command=lambda c=container_name: self.on_container_action(c, 'start'), font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
start_button.grid(row=0, column=0, padx=(0, 2))
stop_button = ctk.CTkButton(actions_frame, text="■", width=30, command=lambda c=container_name: self.on_container_action(c, 'stop'), font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
stop_button.grid(row=0, column=1, padx=2)
restart_button = ctk.CTkButton(actions_frame, text="⟳", width=30, command=lambda c=container_name: self.on_container_action(c, 'restart'), font=self.MONO_FONT, fg_color=theme.NEON_MAGENTA, hover_color=theme.NEON_MAGENTA_HOVER)
restart_button.grid(row=0, column=2, padx=(2, 0))
is_running_or_restarting = 'running' in status or 'restarting' in status
start_button.configure(state="disabled" if is_running_or_restarting else "normal")
stop_button.configure(state="normal" if is_running_or_restarting else "disabled")
restart_button.configure(state="normal" if is_running_or_restarting else "disabled")
new_widgets.append(actions_frame)
self.container_rows.append(new_widgets)
def create_footer_frame(self):
"""Creates the fixed-height footer for status and user controls."""
self.footer_frame = ctk.CTkFrame(self, corner_radius=0, fg_color=theme.FRAME_BLUE, height=40)
self.footer_frame.grid(row=3, column=0, sticky="ew", padx=10, pady=(5, 10))
self.footer_frame.grid_columnconfigure(0, weight=1)
self.status_label = ctk.CTkLabel(self.footer_frame, text="Status: NOMINAL", font=self.MONO_FONT, anchor="w")
self.status_label.grid(row=0, column=0, padx=20, pady=5, sticky="ew")
# Add progress bar, initially hidden
self.progress_bar = ctk.CTkProgressBar(self.footer_frame)
# --- INTERACTION METHODS ---
def append_log(self, text: str):
"""Thread-safely appends logs to the console window."""
self.after(0, self._unsafe_append_log, text)
def _unsafe_append_log(self, text: str):
self.console_log.configure(state="normal")
self.console_log.insert("end", text + "\n")
self.console_log.see("end")
self.console_log.configure(state="disabled")
def append_wu_log(self, text: str):
"""Thread-safely appends logs to the Windows Update console window."""
self.after(0, self._unsafe_append_wu_log, text)
def _unsafe_append_wu_log(self, text: str):
self.wu_console.configure(state="normal")
self.wu_console.insert("end", text + "\n")
self.wu_console.see("end")
self.wu_console.configure(state="disabled")
def append_indexer_log(self, text: str):
"""Thread-safely appends logs to the indexer console window."""
self.after(0, self._unsafe_append_indexer_log, text)
def _unsafe_append_indexer_log(self, text: str):
if hasattr(self, 'indexer_console') and self.indexer_console.winfo_exists():
self.indexer_console.configure(state="normal")
self.indexer_console.insert("end", text + "\n")
self.indexer_console.see("end")
self.indexer_console.configure(state="disabled")
def append_pdf_ingest_log(self, text: str):
"""Thread-safely appends logs to the PDF ingest console window."""
self.after(0, self._unsafe_append_pdf_ingest_log, text)
def _unsafe_append_pdf_ingest_log(self, text: str):
if hasattr(self, 'pdf_ingest_console') and self.pdf_ingest_console.winfo_exists():
self.pdf_ingest_console.configure(state="normal")
self.pdf_ingest_console.insert("end", text + "\n")
self.pdf_ingest_console.see("end")
self.pdf_ingest_console.configure(state="disabled")
def clear_indexer_log(self):
"""Clears the indexer console."""
if hasattr(self, 'indexer_console') and self.indexer_console.winfo_exists():
self.indexer_console.configure(state="normal")
self.indexer_console.delete("1.0", "end")
self.indexer_console.configure(state="disabled")
def set_export_button_state(self, state: str):
"""Enables or disables the search result export button."""
if hasattr(self, 'export_search_button') and self.export_search_button.winfo_exists():
self.export_search_button.configure(state=state)
def clear_search_log(self):
"""Clears the search results console."""
if hasattr(self, 'search_results_console') and self.search_results_console.winfo_exists():
self.search_results_console.configure(state="normal")
self.search_results_console.delete("1.0", "end")
self.search_results_console.configure(state="disabled")
def display_search_results(self, results: list[dict[str, Any]]):
"""Renders search results as interactive elements in the textbox."""
self.clear_search_log()
self.search_results_console.configure(state="normal")
if not results:
self.search_results_console.insert("end", "\n[INFO] No relevant documents found in the vector matrix.\n")
self.search_results_console.configure(state="disabled")
return
self.search_results_console.insert("end", f"\n--- Top {len(results)} Search Results ---\n")
for i, result_data in enumerate(results):
file_path = result_data.get('file_path', 'N/A')
raw_content = result_data.get('raw_content', '')
similarity = result_data.get('similarity', 0.0)
# Create a unique tag for each clickable link
tag_name = f"res_{i}"
# Insert the clickable path
self.search_results_console.insert("end", f"\n{i+1}. Path: ")
path_start_index = self.search_results_console.index("end-1c")
self.search_results_console.insert("end", file_path)
path_end_index = self.search_results_console.index("end-1c")
self.search_results_console.tag_add(tag_name, path_start_index, path_end_index)
self.search_results_console.tag_add("hyperlink", path_start_index, path_end_index)
# Bind the click event
if self.on_search_result_clicked:
self.search_results_console.tag_bind(tag_name, "<Button-1>", lambda e, p=file_path, c=raw_content: self.on_search_result_clicked(p, c))
# Insert the rest of the info
content_snippet = ' '.join(raw_content.splitlines()[:5])
self.search_results_console.insert("end", f" (Similarity: {similarity:.2%})\n")
self.search_results_console.insert("end", f" Content: {content_snippet[:250]}...\n")
self.search_results_console.insert("end", "\n--- Search Complete ---\n")
self.search_results_console.configure(state="disabled")
def append_search_log(self, text: str):
"""Thread-safely appends logs to the search results console window."""
self.after(0, self._unsafe_append_search_log, text)
def _unsafe_append_search_log(self, text: str):
if hasattr(self, 'search_results_console') and self.search_results_console.winfo_exists():
self.search_results_console.configure(state="normal")
self.search_results_console.insert("end", text + "\n")
self.search_results_console.see("end")
self.search_results_console.configure(state="disabled")
def clear_url_ingest_log(self):
"""Clears the URL ingest console."""
if hasattr(self, 'url_ingest_console') and self.url_ingest_console.winfo_exists():
self.url_ingest_console.configure(state="normal")
self.url_ingest_console.delete("1.0", "end")
self.url_ingest_console.configure(state="disabled")
def append_url_ingest_log(self, text: str):
"""Thread-safely appends logs to the URL ingest console window."""
self.after(0, self._unsafe_append_url_ingest_log, text)
def _unsafe_append_url_ingest_log(self, text: str):
if hasattr(self, 'url_ingest_console') and self.url_ingest_console.winfo_exists():
self.url_ingest_console.configure(state="normal")
self.url_ingest_console.insert("end", text + "\n")
self.url_ingest_console.see("end")
self.url_ingest_console.configure(state="disabled")
def clear_pdf_ingest_log(self):
"""Clears the PDF ingest console."""
if hasattr(self, 'pdf_ingest_console') and self.pdf_ingest_console.winfo_exists():
self.pdf_ingest_console.configure(state="normal")
self.pdf_ingest_console.delete("1.0", "end")
self.pdf_ingest_console.configure(state="disabled")
def update_status(self, status_text: str):
"""Allows external controllers to safely update the footer status."""
self.status_label.configure(text=status_text)
def show_progress_bar(self):
"""Makes the progress bar visible and starts its indeterminate animation."""
self.progress_bar.grid(row=1, column=0, columnspan=3, padx=20, pady=(0, 10), sticky="ew")
self.progress_bar.configure(mode="indeterminate")
self.progress_bar.start()
def hide_progress_bar(self):
"""Hides the progress bar from the layout and stops its animation."""
self.progress_bar.stop()
self.progress_bar.grid_remove()
def register_callbacks(self,
on_exit_clicked: Callable[[], None],
on_menu_action: Callable[[str, str], None],
on_about_clicked: Callable[[], None] | None = None,
on_check_for_updates_clicked: Callable[[], None] | None = None,
on_container_action: Callable[[str, str], None] | None = None,
on_wu_check_clicked: Callable[[], None] | None = None,
on_wu_install_clicked: Callable[[], None] | None = None,
on_wu_context_action: Callable[[str, str, dict[str, Any]], None] | None = None,
on_wu_hide_clicked: Callable[[], None] | None = None,
on_wu_unhide_clicked: Callable[[], None] | None = None,
on_pdf_browse_source_clicked: Callable[[], None] | None = None,
on_pdf_browse_dest_clicked: Callable[[], None] | None = None,
on_pdf_clear_paths_clicked: Callable[[], None] | None = None,
on_save_settings_clicked: Callable[[], None] | None = None,
on_load_settings_clicked: Callable[[], None] | None = None,
on_export_search_clicked: Callable[[], None] | None = None,
on_search_result_clicked: Callable[[str, str], None] | None = None):
"""Connects UI interactives directly to controller event hooks."""
self.on_exit_callback = on_exit_clicked
# --- File Menu ---
file_frame = ctk.CTkFrame(self.menu_scroll_frame, fg_color="transparent")
file_frame.pack(fill="x", pady=5, padx=5)
ctk.CTkLabel(file_frame, text="File", font=self.MONO_FONT_BOLD).pack(anchor="w")
if on_save_settings_clicked:
ctk.CTkButton(file_frame, text="Save Settings", command=on_save_settings_clicked, anchor="w").pack(fill="x", pady=2)
if on_load_settings_clicked:
ctk.CTkButton(file_frame, text="Load Settings", command=on_load_settings_clicked, anchor="w").pack(fill="x", pady=2)
# --- Tools Menu ---
tools_frame = ctk.CTkFrame(self.menu_scroll_frame, fg_color="transparent")
tools_frame.pack(fill="x", pady=5, padx=5)
ctk.CTkLabel(tools_frame, text="Tools", font=self.MONO_FONT_BOLD).pack(anchor="w")
ctk.CTkButton(tools_frame, text="Run Mass Ingest Indexer", command=lambda: on_menu_action("Run Mass Ingest Indexer", "CRAWL & INGEST"), anchor="w").pack(fill="x", pady=2)
ctk.CTkButton(tools_frame, text="Start Cyber Deck Engine", command=lambda: on_menu_action("Start Cyber Deck Engine", "DB & SERVICES"), anchor="w").pack(fill="x", pady=2)
ctk.CTkButton(tools_frame, text="Run Autonomous Task Test", command=lambda: on_menu_action("Run Autonomous Task Test", "DOCKER"), anchor="w").pack(fill="x", pady=2)
ctk.CTkButton(tools_frame, text="Run Vector DB Diagnostic", command=lambda: on_menu_action("Run Vector DB Diagnostic", "DB & SERVICES"), anchor="w").pack(fill="x", pady=2)
ctk.CTkButton(tools_frame, text="Run PDF Ingestion", command=lambda: on_menu_action("Run PDF Ingestion", "CRAWL & INGEST"), anchor="w").pack(fill="x", pady=2)
ctk.CTkButton(tools_frame, text="Rebuild DB Schema", command=lambda: on_menu_action("Rebuild DB Schema", "DB & SERVICES"), anchor="w", fg_color="#E74C3C", hover_color="#C0392B").pack(fill="x", pady=2)
# --- Chains Sub-Menu ---
chains_frame = ctk.CTkFrame(self.menu_scroll_frame, fg_color="transparent")
chains_frame.pack(fill="x", pady=5, padx=5)
ctk.CTkLabel(chains_frame, text="Chains", font=self.MONO_FONT_BOLD).pack(anchor="w")
ctk.CTkButton(chains_frame, text="Sterilize & Audit", command=lambda: on_menu_action("Chain Sterilize Audit", "DOCKER"), anchor="w").pack(fill="x", pady=2)
ctk.CTkButton(chains_frame, text="Archive & Preserve", command=lambda: on_menu_action("Chain Archive Preserve", "DOCKER"), anchor="w").pack(fill="x", pady=2)
ctk.CTkButton(chains_frame, text="Scorched Earth Reset", command=lambda: on_menu_action("Chain: Scorched Earth Reset", "DOCKER"), anchor="w", fg_color="#D32F2F", hover_color="#E53935").pack(fill="x", pady=2)
ctk.CTkButton(chains_frame, text="System Bloat Purge", command=lambda: on_menu_action("Chain: System Bloat Purge", "DOCKER"), anchor="w", fg_color="#B71C1C", hover_color="#C62828").pack(fill="x", pady=2)
# --- Updates Menu ---
updates_frame = ctk.CTkFrame(self.menu_scroll_frame, fg_color="transparent")
updates_frame.pack(fill="x", pady=5, padx=5)
ctk.CTkLabel(updates_frame, text="Updates", font=self.MONO_FONT_BOLD).pack(anchor="w")
self.wu_check_button = ctk.CTkButton(updates_frame, text="Scan for Updates", anchor="w")
if on_wu_check_clicked:
self.wu_check_button.configure(command=on_wu_check_clicked)
self.wu_check_button.pack(fill="x", pady=2)
self.wu_install_button = ctk.CTkButton(updates_frame, text="Install Selected", state="disabled", anchor="w")
if on_wu_install_clicked:
self.wu_install_button.configure(command=on_wu_install_clicked)
self.wu_install_button.pack(fill="x", pady=2)
self.wu_hide_button = ctk.CTkButton(updates_frame, text="Hide Selected", state="disabled", anchor="w")
if on_wu_hide_clicked:
self.wu_hide_button.configure(command=on_wu_hide_clicked)
self.wu_hide_button.pack(fill="x", pady=2)
self.wu_unhide_button = ctk.CTkButton(updates_frame, text="Unhide Selected", state="disabled", anchor="w")
if on_wu_unhide_clicked:
self.wu_unhide_button.configure(command=on_wu_unhide_clicked)
self.wu_unhide_button.pack(fill="x", pady=2)
# --- Help Menu ---
help_frame = ctk.CTkFrame(self.menu_scroll_frame, fg_color="transparent")
help_frame.pack(fill="x", pady=5, padx=5)
ctk.CTkLabel(help_frame, text="Help", font=self.MONO_FONT_BOLD).pack(anchor="w")
if on_check_for_updates_clicked:
ctk.CTkButton(help_frame, text="Check for Updates...", command=on_check_for_updates_clicked, anchor="w").pack(fill="x", pady=2)
if on_about_clicked:
ctk.CTkButton(help_frame, text="About", command=on_about_clicked, anchor="w").pack(fill="x", pady=2)
# --- Premium Tools (Initially hidden) ---
self.premium_tools_frame = ctk.CTkFrame(self.menu_scroll_frame, fg_color="transparent")
# Don't pack it yet. The controller will decide.
ctk.CTkLabel(self.premium_tools_frame, text="Premium Tools", font=self.MONO_FONT_BOLD).pack(anchor="w")
ctk.CTkButton(self.premium_tools_frame, text="Run Advanced Diagnostics", command=lambda: on_menu_action("Run Advanced Diagnostics", "DB & SERVICES"), anchor="w").pack(fill="x", pady=2)
self.premium_tools_frame.pack_forget() # Hide by default
# General tools that don't have a dedicated tab yet
# --- Other Callbacks ---
if on_container_action:
self.on_container_action = on_container_action
if on_wu_check_clicked:
# This is now in the menu, but we keep the original handler for the menu command
pass
if on_wu_context_action:
self.on_wu_context_action = on_wu_context_action
if on_pdf_browse_source_clicked:
self.pdf_browse_source_button.configure(command=on_pdf_browse_source_clicked)
if on_pdf_browse_dest_clicked:
self.pdf_browse_dest_button.configure(command=on_pdf_browse_dest_clicked)
if on_pdf_clear_paths_clicked:
self.pdf_clear_paths_button.configure(command=on_pdf_clear_paths_clicked)
if on_menu_action and self.run_indexer_button:
self.run_indexer_button.configure(command=lambda: on_menu_action("Run Mass Ingest Indexer", "CRAWL & INGEST"))
if on_menu_action and self.run_pdf_ingest_button:
self.run_pdf_ingest_button.configure(command=lambda: on_menu_action("Run PDF Ingestion", "CRAWL & INGEST"))
if on_menu_action and self.run_url_ingest_button:
self.run_url_ingest_button.configure(command=lambda: on_menu_action("Run URL Ingestion", "CRAWL & INGEST"))
if on_menu_action and self.run_search_button:
self.run_search_button.configure(command=lambda: on_menu_action("Run Vector Search", "CRAWL & INGEST"))
if on_export_search_clicked and self.export_search_button:
self.export_search_button.configure(command=on_export_search_clicked)
if on_search_result_clicked:
self.on_search_result_clicked = on_search_result_clicked
def show_full_content_viewer(self, title: str, content: str):
"""Creates a new top-level window to display the full content of a search result."""
viewer = ctk.CTkToplevel(self)
viewer.title(f"Content Viewer - {title}")
viewer.geometry("800x600")
viewer.transient(self) # Keep it on top of the main window
viewer.grab_set() # Modal behavior
viewer.grid_columnconfigure(0, weight=1)
viewer.grid_rowconfigure(0, weight=1) # Textbox row
viewer.grid_rowconfigure(1, weight=0) # Button row
textbox = ctk.CTkTextbox(viewer, wrap="word", font=self.MONO_FONT)
textbox.grid(row=0, column=0, sticky="nsew", padx=10, pady=(10, 5))
textbox.insert("1.0", content)
textbox.configure(state="disabled")
# --- Button Frame ---
button_frame = ctk.CTkFrame(viewer, fg_color="transparent")
button_frame.grid(row=1, column=0, padx=10, pady=(0, 10), sticky="e")
def _copy_and_confirm():
"""Copies content to clipboard and provides visual feedback."""
self.clipboard_clear()
self.clipboard_append(content)
copy_button.configure(text="Copied!", state="disabled")
# Revert the button text after 2 seconds
self.after(2000, lambda: copy_button.configure(text="Copy to Clipboard", state="normal"))
copy_button = ctk.CTkButton(
button_frame,
text="Copy to Clipboard",
command=_copy_and_confirm,
font=self.MONO_FONT_BOLD,
fg_color=theme.NEON_MAGENTA,
hover_color=theme.NEON_MAGENTA_HOVER
)
copy_button.pack()
if __name__ == "__main__":
import multiprocessing
# 1. This must be the absolute first line inside the main block
# to prevent PyInstaller forks from restarting the GUI.
multiprocessing.freeze_support()
# 2. Import controller and instantiate the main application
from gui_controller import DashboardController
app = DevOpsDashboard()
controller = DashboardController(view=app)
# 3. Define a clean shutdown process to prevent _MEI temp folder errors
def on_closing():
"""Calls the controller's shutdown routine and then destroys the window."""
controller.shutdown()
app.destroy()
sys.exit(0)
app.protocol("WM_DELETE_WINDOW", on_closing)
app.mainloop()