mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
- Fix kube context switching for k3s single-context kubeconfigs and k3d shorthand prefixes - Update common init/status scripts (registry, kerberos, cnpg backup, service layer, common services) - Add Gitea init script and installer ArgoCD screen - Add Supabase realtime probe patching plus regression tests - Extend installer core/UI test coverage
761 lines
28 KiB
Python
761 lines
28 KiB
Python
"""Sidebar navigation, page transitions, footer and app identity."""
|
|
|
|
import os
|
|
import platform
|
|
from pathlib import Path
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
from installer import config as inst_config
|
|
from installer.core.env import get_resource_path
|
|
|
|
|
|
class NavigationMixin:
|
|
"""Sidebar navigation, page transitions, footer and app identity."""
|
|
|
|
def _set_app_identity(self):
|
|
"""Set the installer identity: process/menu name and Dock icon on macOS.
|
|
|
|
Notes:
|
|
- We set Tk's appname for consistency.
|
|
- On macOS, attempt to set the process/menu name to 'Prole Database Installer'
|
|
via NSProcessInfo if PyObjC is available.
|
|
- Prefer the Prole Tools.app .icns from the built app; fallback to local PNG/GIF.
|
|
"""
|
|
# Set Tk application name
|
|
try:
|
|
self.root.tk.call("tk", "appname", "Prole Database Installer")
|
|
except Exception:
|
|
pass
|
|
|
|
# macOS: set process/menu name and Dock icon via AppKit/Foundation
|
|
if platform.system() == "Darwin":
|
|
# Try to set the visible process name for the menu bar
|
|
try:
|
|
from Foundation import NSProcessInfo
|
|
|
|
NSProcessInfo.processInfo().setProcessName_("Prole Database Installer")
|
|
except Exception:
|
|
pass
|
|
|
|
# Also try to retitle the first main menu item so the menu next to the Apple logo reads 'Prole Database Installer'
|
|
try:
|
|
from AppKit import NSApplication
|
|
|
|
app = NSApplication.sharedApplication()
|
|
main_menu = app.mainMenu()
|
|
if main_menu is not None and main_menu.numberOfItems() > 0:
|
|
first_item = main_menu.itemAtIndex_(0)
|
|
if first_item is not None:
|
|
first_item.setTitle_("Prole Database Installer")
|
|
except Exception:
|
|
pass
|
|
|
|
icns_candidates = [
|
|
get_resource_path(
|
|
"prole-app/dist/Prole Tools.app/Contents/Resources/Prole Tools.icns"
|
|
),
|
|
get_resource_path(
|
|
"prole-app/dist/Prole Tools.app/Contents/Resources/Prole.icns"
|
|
),
|
|
]
|
|
icns_path = next((p for p in icns_candidates if p.exists()), None)
|
|
if icns_path is not None:
|
|
try:
|
|
# Use PyObjC if available
|
|
from AppKit import NSApplication, NSImage
|
|
|
|
img = NSImage.alloc().initWithContentsOfFile_(str(icns_path))
|
|
if img is not None:
|
|
NSApplication.sharedApplication().setApplicationIconImage_(img)
|
|
return
|
|
except Exception:
|
|
pass
|
|
|
|
# If no .icns was found, try setting Dock icon from configured PNG
|
|
try:
|
|
from AppKit import NSApplication, NSImage
|
|
|
|
cfg_icon = inst_config.get_ui_icon_image_path()
|
|
if cfg_icon.exists():
|
|
png_img = NSImage.alloc().initWithContentsOfFile_(str(cfg_icon))
|
|
if png_img is not None:
|
|
NSApplication.sharedApplication().setApplicationIconImage_(
|
|
png_img
|
|
)
|
|
# do not return; still set Tk icon below for consistency
|
|
except Exception:
|
|
pass
|
|
|
|
# Fallback: Tk icon from image assets (PNG/GIF)
|
|
# Prefer config-defined icon image
|
|
try:
|
|
cfg_icon = inst_config.get_ui_icon_image_path()
|
|
except Exception:
|
|
cfg_icon = get_resource_path("img/proleIcon.png")
|
|
img_candidates = [
|
|
cfg_icon,
|
|
get_resource_path("img/prole-type.png"),
|
|
get_resource_path("img/prole-type.gif"),
|
|
get_resource_path("img/Prole.png"),
|
|
get_resource_path("img/proleLogoSepia.png"),
|
|
]
|
|
for p in img_candidates:
|
|
try:
|
|
if p.exists():
|
|
self._app_iconphoto = tk.PhotoImage(file=str(p))
|
|
try:
|
|
self.root.iconphoto(True, self._app_iconphoto)
|
|
except Exception:
|
|
pass
|
|
break
|
|
except Exception:
|
|
continue
|
|
|
|
def show_config_dialog(self):
|
|
"""Show configuration dialog for Docker image import directory."""
|
|
dialog = tk.Toplevel(self.root)
|
|
dialog.title("Configuration")
|
|
dialog.geometry("500x200")
|
|
dialog.resizable(False, False)
|
|
dialog.transient(self.root)
|
|
dialog.grab_set()
|
|
|
|
# Center on parent
|
|
x = self.root.winfo_x() + (self.root.winfo_width() // 2) - 250
|
|
y = self.root.winfo_y() + (self.root.winfo_height() // 2) - 100
|
|
dialog.geometry(f"+{x}+{y}")
|
|
|
|
container = tk.Frame(dialog, padx=20, pady=20)
|
|
container.pack(fill="both", expand=True)
|
|
|
|
tk.Label(
|
|
container,
|
|
text="Docker Image Import Directory:",
|
|
font=("SF Pro Text", 12, "bold"),
|
|
).pack(anchor="w")
|
|
tk.Label(
|
|
container,
|
|
text="Images found here will be loaded instead of pulled from Docker Hub.",
|
|
font=("SF Pro Text", 10),
|
|
fg="#666666",
|
|
).pack(anchor="w", pady=(0, 10))
|
|
|
|
row = tk.Frame(container)
|
|
row.pack(fill="x")
|
|
|
|
entry = tk.Entry(
|
|
row,
|
|
textvariable=self.docker_import_dir,
|
|
font=("SF Pro Text", 11),
|
|
bg="white",
|
|
fg="black",
|
|
insertbackground="black",
|
|
)
|
|
entry.pack(side="left", fill="x", expand=True, padx=(0, 5))
|
|
|
|
def browse():
|
|
path = filedialog.askdirectory(
|
|
initialdir=self.docker_import_dir.get() or Path.home()
|
|
)
|
|
if path:
|
|
self.docker_import_dir.set(path)
|
|
|
|
tk.Button(row, text="Browse...", command=browse).pack(side="right")
|
|
|
|
def save():
|
|
path = self.docker_import_dir.get().strip()
|
|
self.prole_cfg_data["Global"]["DOCKER_IMPORT_DIR"] = path
|
|
self._save_prole_cfg()
|
|
dialog.destroy()
|
|
|
|
btns = tk.Frame(container)
|
|
btns.pack(fill="x", pady=(20, 0))
|
|
|
|
tk.Button(btns, text="Save", command=save, width=10).pack(side="right")
|
|
tk.Button(btns, text="Cancel", command=dialog.destroy, width=10).pack(
|
|
side="right", padx=10
|
|
)
|
|
|
|
def configure_styles(self):
|
|
"""Configure ttk styles"""
|
|
base_bg = "white"
|
|
self.style.configure("TFrame", background="white")
|
|
self.style.configure(
|
|
"TLabel", background="white", foreground="black", font=("SF Pro Text", 11)
|
|
)
|
|
self.style.configure("Header.TLabel", font=("SF Pro Text", 16, "bold"))
|
|
self.style.configure("Small.TLabel", font=("SF Pro Text", 10))
|
|
|
|
# Consistent button styling
|
|
self.style.configure(
|
|
"Nav.TButton",
|
|
font=("SF Pro Text", 11),
|
|
padding=(12, 8),
|
|
background="#F5F5DC",
|
|
bordercolor="#F5F5DC",
|
|
lightcolor="#F5F5DC",
|
|
darkcolor="#F5F5DC",
|
|
relief="flat",
|
|
)
|
|
|
|
# Map state colors to avoid black boxes on hover/active
|
|
self.style.map(
|
|
"Nav.TButton",
|
|
background=[("active", "#E5E5D5"), ("pressed", "#D5D5C5")],
|
|
bordercolor=[("active", "#E5E5D5"), ("pressed", "#D5D5C5")],
|
|
lightcolor=[("active", "#E5E5D5"), ("pressed", "#D5D5C5")],
|
|
darkcolor=[("active", "#E5E5D5"), ("pressed", "#D5D5C5")],
|
|
)
|
|
|
|
# Notebook styling to match light theme and avoid dark mode shifts on macOS
|
|
self.style.theme_use("default")
|
|
self.style.configure(
|
|
"TNotebook", background="white", borderwidth=0, highlightthickness=0
|
|
)
|
|
self.style.configure(
|
|
"TNotebook.Tab",
|
|
background="#F5F5DC",
|
|
foreground="black",
|
|
lightcolor="#F5F5DC",
|
|
bordercolor="#CCCCCC",
|
|
darkcolor="#F5F5DC",
|
|
borderwidth=1,
|
|
padding=[10, 5],
|
|
)
|
|
self.style.map(
|
|
"TNotebook.Tab",
|
|
background=[("selected", "white")],
|
|
bordercolor=[("selected", "#CCCCCC")],
|
|
lightcolor=[("selected", "white")],
|
|
focuscolor=[("selected", "white")],
|
|
)
|
|
|
|
# Ensure common controls inherit white background
|
|
try:
|
|
self.style.configure("TCheckbutton", background=base_bg, foreground="black")
|
|
self.style.configure(
|
|
"TCombobox", fieldbackground="white", background=base_bg
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
def _create_sidebar_nav(self):
|
|
"""Create the left-hand navigation menu."""
|
|
tk.Label(
|
|
self.sidebar,
|
|
text="INSTALLER",
|
|
bg="#F5F5DC",
|
|
fg="#8B8B7A",
|
|
font=("SF Pro Text", 10, "bold"),
|
|
anchor="w",
|
|
).pack(fill="x", padx=20, pady=(20, 10))
|
|
|
|
for text, page_id in self.nav_items:
|
|
lbl = tk.Label(
|
|
self.sidebar,
|
|
text=text,
|
|
bg="#F5F5DC",
|
|
fg="black",
|
|
font=("SF Pro Text", 11),
|
|
anchor="w",
|
|
padx=20,
|
|
pady=5,
|
|
cursor="hand2",
|
|
)
|
|
lbl.pack(fill="x")
|
|
lbl.bind("<Button-1>", lambda e, p=page_id: self.show_page(p))
|
|
self.nav_widgets[page_id] = lbl
|
|
|
|
def _update_nav_highlight(self, active_id):
|
|
"""Highlight the current active page in the sidebar."""
|
|
# Handle dep_id mapping to 'Dependencies'
|
|
if active_id.startswith("dep_"):
|
|
active_id = "deps_summary"
|
|
|
|
for page_id, widget in self.nav_widgets.items():
|
|
if page_id == active_id:
|
|
widget.configure(bg="#E5E5D5", font=("SF Pro Text", 11, "bold"))
|
|
else:
|
|
widget.configure(bg="#F5F5DC", font=("SF Pro Text", 11))
|
|
|
|
def create_navigation(self):
|
|
pass
|
|
|
|
def on_prev(self):
|
|
# Custom prev navigation for dependency pages when filtering
|
|
current_id = self.pages[self.page_index][0]
|
|
# Close Terminal if leaving build summary via Prev
|
|
if current_id == "build_summary":
|
|
try:
|
|
self.close_build_terminal()
|
|
except Exception:
|
|
pass
|
|
if current_id.startswith("dep_"):
|
|
seq = self._dep_navigation_sequence()
|
|
try:
|
|
i = seq.index(current_id)
|
|
except ValueError:
|
|
i = -1
|
|
if i > 0:
|
|
self.show_page(seq[i - 1])
|
|
return
|
|
else:
|
|
# Go back to summary if there is no previous in sequence
|
|
self.show_page("deps_summary")
|
|
return
|
|
if current_id == "deps_summary":
|
|
self.show_page("welcome")
|
|
return
|
|
if current_id == "network_scan":
|
|
# Go back to last relevant dep or deps_summary
|
|
if self.all_dependencies_installed():
|
|
self.show_page("deps_summary")
|
|
else:
|
|
seq = self._dep_navigation_sequence()
|
|
self.show_page(seq[-1] if seq else "deps_summary")
|
|
return
|
|
if current_id == "env_setup":
|
|
self.show_page("network_scan")
|
|
return
|
|
if current_id == "init_cluster":
|
|
if getattr(self, "_showing_service_overlay", False):
|
|
self._showing_service_overlay = False
|
|
self.show_page("init_cluster")
|
|
return
|
|
self.show_page("env_setup")
|
|
return
|
|
if current_id == "database_options":
|
|
self.show_page("init_cluster")
|
|
return
|
|
if current_id == "init_password":
|
|
self.show_page("init_db_build")
|
|
return
|
|
if current_id == "init_db_build":
|
|
self.show_page("database_options")
|
|
return
|
|
if current_id == "init_scripts":
|
|
self.show_page("init_password")
|
|
return
|
|
if current_id == "kerberos_config":
|
|
self.show_page("init_scripts")
|
|
return
|
|
if current_id == "argocd_config":
|
|
self.show_page("kerberos_config")
|
|
return
|
|
if current_id == "gitops_config":
|
|
self.show_page("argocd_config")
|
|
return
|
|
if current_id == "supabase_config":
|
|
self.show_page("gitops_config")
|
|
return
|
|
if current_id == "ollama_config":
|
|
self.show_page("supabase_config")
|
|
return
|
|
if current_id == "init_cnpg_deploy":
|
|
self.show_page("ollama_config")
|
|
return
|
|
if current_id == "create_installer":
|
|
self.show_page("init_cnpg_deploy")
|
|
return
|
|
# Default prev
|
|
if self.page_index > 0:
|
|
self.show_page(self.page_index - 1)
|
|
return
|
|
|
|
def on_next(self):
|
|
# Special handling for dynamic labels
|
|
current_id = self.pages[self.page_index][0]
|
|
print(
|
|
f"[DEBUG] on_next: current_id='{current_id}', page_index={self.page_index}"
|
|
)
|
|
|
|
if current_id == "welcome":
|
|
self.show_page("deps_summary")
|
|
return
|
|
|
|
if current_id == "deps_summary":
|
|
# Determine where to go from summary
|
|
# Always go to the next dependency or Network Scan
|
|
if self.all_dependencies_installed():
|
|
print("[DEBUG] on_next: all deps installed, going to network_scan")
|
|
self.show_page("network_scan")
|
|
return
|
|
|
|
# Go to first missing dependency page
|
|
seq = self._dep_navigation_sequence()
|
|
print(f"[DEBUG] on_next: missing deps sequence: {seq}")
|
|
if seq:
|
|
self.show_page(seq[0])
|
|
else:
|
|
print(
|
|
"[DEBUG] on_next: all deps seem OK in sequence, going to network_scan"
|
|
)
|
|
self.show_page("network_scan")
|
|
return
|
|
|
|
if current_id.startswith("dep_"):
|
|
# Navigate within dependency sequence
|
|
seq = self._dep_navigation_sequence()
|
|
print(f"[DEBUG] on_next: dep sequence: {seq}")
|
|
try:
|
|
i = seq.index(current_id)
|
|
except ValueError:
|
|
i = -1
|
|
|
|
if i >= 0 and i < len(seq) - 1:
|
|
self.show_page(seq[i + 1])
|
|
return
|
|
else:
|
|
# After last relevant dep page, go back to summary if anything is still missing
|
|
if not self.all_dependencies_installed():
|
|
print(
|
|
"[DEBUG] on_next: some deps still missing, returning to deps_summary"
|
|
)
|
|
self.show_page("deps_summary")
|
|
else:
|
|
print(
|
|
"[DEBUG] on_next: all deps now installed, going to network_scan"
|
|
)
|
|
self.show_page("network_scan")
|
|
return
|
|
|
|
if current_id == "network_scan":
|
|
# Capture network scan info
|
|
try:
|
|
self.prole_cfg_data["Network"][
|
|
"KDC_AUTO_DETECTED"
|
|
] = self.kerberos_kdc.get()
|
|
self.prole_cfg_data["Network"]["KERBEROS_AUTO_ENABLED"] = str(
|
|
self.kerberos_enabled.get()
|
|
)
|
|
self._save_prole_cfg()
|
|
except Exception:
|
|
pass
|
|
self.show_page("env_setup")
|
|
return
|
|
|
|
if current_id == "env_setup":
|
|
# Collect values, validate, write env.sh, then go to Database Creation
|
|
vals = {}
|
|
try:
|
|
for k in (
|
|
"PROLE_HOME",
|
|
"PROLE_CONF",
|
|
"PROLE_DATA",
|
|
"PROLE_LOGS",
|
|
"PROLE_SERVICE",
|
|
):
|
|
vals[k] = self._env_entries[k].get().strip()
|
|
self.prole_cfg_data["System Environment"][k] = vals[k]
|
|
vals["NAMESPACE"] = (self.db_namespace.get() or "").strip()
|
|
except Exception:
|
|
vals = self._env_defaults()
|
|
# Basic validation: require non-empty PROLE_HOME
|
|
if not vals.get("PROLE_HOME"):
|
|
try:
|
|
messagebox.showerror("Environment", "Please specify PROLE_HOME")
|
|
except Exception:
|
|
pass
|
|
return
|
|
try:
|
|
self._save_env_to_file(vals)
|
|
# Reload our environment so subsequent steps (Build) inherit it
|
|
self._after_env_saved()
|
|
except Exception as e:
|
|
try:
|
|
messagebox.showwarning("Environment", f"Could not save env.sh: {e}")
|
|
except Exception:
|
|
pass
|
|
return
|
|
self.show_page("init_cluster")
|
|
return
|
|
|
|
if current_id == "init_password":
|
|
p1 = self.db_password.get()
|
|
p2 = self.db_password_confirm.get()
|
|
if not p1:
|
|
try:
|
|
messagebox.showerror("Password", "Password cannot be empty.")
|
|
except Exception:
|
|
pass
|
|
return
|
|
if p1 != p2:
|
|
try:
|
|
messagebox.showerror("Password", "Passwords do not match.")
|
|
except Exception:
|
|
pass
|
|
return
|
|
if not self._validate_port_forward_overlaps():
|
|
return
|
|
self._run_preparation_overlay()
|
|
return
|
|
|
|
if current_id == "init_db_build":
|
|
self.show_page("init_password")
|
|
return
|
|
|
|
if current_id == "build":
|
|
if getattr(self, "_built_success", False):
|
|
self.show_page("create_installer")
|
|
else:
|
|
self.perform_build()
|
|
return
|
|
|
|
if current_id == "init_cluster":
|
|
if not self._validate_and_save_cluster_config():
|
|
return
|
|
if not self._cluster_ready_for_navigation():
|
|
return
|
|
if getattr(self, "_all_services_green", False):
|
|
# All common services healthy; skip repair/redeploy and proceed
|
|
self._common_services_success = True
|
|
self.show_page("database_options")
|
|
return
|
|
if not getattr(self, "_common_services_success", False):
|
|
self._run_service_layer_overlay(next_page="database_options")
|
|
return
|
|
self.show_page("database_options")
|
|
return
|
|
|
|
if current_id == "database_options":
|
|
if self._generate_prole_db_dockerfile():
|
|
self.show_page("init_db_build")
|
|
return
|
|
|
|
if current_id == "init_scripts":
|
|
self.prole_cfg_data["Initialization Scripts"]["STATUS"] = (
|
|
"Completed" if getattr(self, "_scripts_success", False) else "Attempted"
|
|
)
|
|
self._save_prole_cfg()
|
|
if self.kerberos_enabled.get():
|
|
self.show_page("kerberos_config")
|
|
else:
|
|
self.show_page("argocd_config")
|
|
return
|
|
|
|
if current_id == "kerberos_config":
|
|
# Capture kerberos config
|
|
self.prole_cfg_data["Kerberos Authentication"]["ENABLED"] = str(
|
|
self.kerberos_enabled.get()
|
|
)
|
|
self.prole_cfg_data["Kerberos Authentication"][
|
|
"REALM"
|
|
] = self.kerberos_realm.get()
|
|
self.prole_cfg_data["Kerberos Authentication"][
|
|
"KDC"
|
|
] = self.kerberos_kdc.get()
|
|
self.prole_cfg_data["Kerberos Authentication"][
|
|
"SERVER"
|
|
] = self.kerberos_kdc.get()
|
|
self.prole_cfg_data["Kerberos Authentication"][
|
|
"USER"
|
|
] = self.kerberos_user.get()
|
|
self.prole_cfg_data["Kerberos Authentication"][
|
|
"PASSWORD"
|
|
] = self.kerberos_password.get()
|
|
self.prole_cfg_data["Kerberos Authentication"]["AD_PORT_FORWARD"] = (
|
|
os.environ.get("KRB5_AD_PORT_FORWARD", "1")
|
|
)
|
|
self.prole_cfg_data["Kerberos Authentication"]["AD_TCP_PORTS"] = (
|
|
os.environ.get("KRB5_AD_TCP_PORTS", "88 389 445 464 636")
|
|
)
|
|
self.prole_cfg_data["Kerberos Authentication"]["AD_UDP_PORTS"] = (
|
|
os.environ.get("KRB5_AD_UDP_PORTS", "88 464")
|
|
)
|
|
self.prole_cfg_data["Kerberos Authentication"]["AD_PROXY_HOST_NETWORK"] = (
|
|
os.environ.get("KRB5_AD_PROXY_HOST_NETWORK", "1")
|
|
)
|
|
self.prole_cfg_data["Kerberos Authentication"]["AD_PROXY_IMAGE"] = (
|
|
os.environ.get("KRB5_AD_PROXY_IMAGE", "alpine/socat")
|
|
)
|
|
self.prole_cfg_data["Kerberos Authentication"]["AD_PROXY_SERVICE"] = (
|
|
os.environ.get("KRB5_AD_SERVICE_NAME", "prole-kerberos-ad-dc")
|
|
)
|
|
self._save_prole_cfg()
|
|
self.show_page("argocd_config")
|
|
return
|
|
|
|
if current_id == "argocd_config":
|
|
self.prole_cfg_data["Optional Features"]["ARGOCD_ENABLED"] = str(
|
|
self.argocd_enabled.get()
|
|
)
|
|
argocd_status = (
|
|
"Skipped"
|
|
if not self.argocd_enabled.get()
|
|
else (
|
|
"Deployed"
|
|
if getattr(self, "_argocd_success", False)
|
|
else "Attempted"
|
|
)
|
|
)
|
|
argocd_ns = (self.argocd_namespace.get() or "argocd").strip() or "argocd"
|
|
self.prole_cfg_data.setdefault("Global", {})["ARGOCD_NAMESPACE"] = argocd_ns
|
|
self.prole_cfg_data["ArgoCD"] = {
|
|
"STATUS": argocd_status,
|
|
"ARGOCD_NAMESPACE": argocd_ns,
|
|
}
|
|
self._save_prole_cfg()
|
|
self.show_page("gitops_config")
|
|
return
|
|
|
|
if current_id == "gitops_config":
|
|
self.prole_cfg_data["Optional Features"]["GITOPS_ENABLED"] = str(
|
|
self.gitops_enabled.get()
|
|
)
|
|
gitops_status = (
|
|
"Skipped"
|
|
if not self.gitops_enabled.get()
|
|
else (
|
|
"Deployed"
|
|
if getattr(self, "_gitops_success", False)
|
|
else "Attempted"
|
|
)
|
|
)
|
|
self.prole_cfg_data["GitOps"] = {
|
|
"STATUS": gitops_status,
|
|
"GITOPS_NAMESPACE": (self.gitops_namespace.get() or "gitea").strip()
|
|
or "gitea",
|
|
}
|
|
self._save_prole_cfg()
|
|
self.show_page("supabase_config")
|
|
return
|
|
|
|
if current_id == "supabase_config":
|
|
self.prole_cfg_data["Optional Features"]["SUPABASE_ENABLED"] = str(
|
|
self.supabase_enabled.get()
|
|
)
|
|
supabase_status = (
|
|
"Skipped"
|
|
if not self.supabase_enabled.get()
|
|
else (
|
|
"Deployed"
|
|
if getattr(self, "_supabase_success", False)
|
|
else "Attempted"
|
|
)
|
|
)
|
|
self.prole_cfg_data["Supabase"] = {
|
|
"STATUS": supabase_status,
|
|
}
|
|
self._save_prole_cfg()
|
|
self.show_page("ollama_config")
|
|
return
|
|
|
|
if current_id == "ollama_config":
|
|
self._save_ollama_config()
|
|
self.show_page("init_cnpg_deploy")
|
|
return
|
|
|
|
if current_id == "init_cnpg_deploy":
|
|
self.prole_cfg_data["Deployment"]["STATUS"] = (
|
|
"Deployed" if getattr(self, "_cnpg_success", False) else "Attempted"
|
|
)
|
|
self._save_prole_cfg()
|
|
self.show_page("create_installer")
|
|
return
|
|
|
|
if current_id == "create_installer":
|
|
print("[DEBUG] on_next: at create_installer, Finish clicked. Closing.")
|
|
if "Install" in self.prole_cfg_data:
|
|
self.prole_cfg_data["Install"]["STATUS"] = "Finished"
|
|
self._save_prole_cfg()
|
|
self.root.destroy()
|
|
return
|
|
|
|
# Default next
|
|
if self.page_index < len(self.pages) - 1:
|
|
print(f"[DEBUG] on_next: default next to index {self.page_index + 1}")
|
|
self.show_page(self.page_index + 1)
|
|
else:
|
|
print("[DEBUG] on_next: already at last page")
|
|
return
|
|
|
|
def update_footer(self):
|
|
# Default states for tk.Button
|
|
self.prev_button.configure(state="normal")
|
|
self.next_button.configure(state="normal")
|
|
first = self.page_index == 0
|
|
# Base label
|
|
self.next_button.configure(text="Next")
|
|
|
|
if getattr(self, "_showing_service_overlay", False):
|
|
if not getattr(self, "_common_services_success", False):
|
|
self.next_button.configure(state="disabled")
|
|
else:
|
|
self.next_button.configure(state="normal")
|
|
|
|
# Page-specific adjustments
|
|
pid = self.pages[self.page_index][0]
|
|
if pid == "build":
|
|
# Build page: show Build or Next depending on state
|
|
if getattr(self, "_built_success", False):
|
|
self.next_button.configure(text="Next")
|
|
else:
|
|
self.next_button.configure(text="Build")
|
|
elif pid == "supabase_config":
|
|
# Supabase screen: allow Next when disabled or after success
|
|
if self.supabase_enabled.get():
|
|
if getattr(self, "_supabase_success", False):
|
|
self.next_button.configure(state="normal")
|
|
else:
|
|
self.next_button.configure(state="disabled")
|
|
else:
|
|
self.next_button.configure(state="normal")
|
|
elif pid == "gitops_config":
|
|
if self.gitops_enabled.get():
|
|
if getattr(self, "_gitops_success", False):
|
|
self.next_button.configure(state="normal")
|
|
else:
|
|
self.next_button.configure(state="disabled")
|
|
else:
|
|
self.next_button.configure(state="normal")
|
|
elif pid == "argocd_config":
|
|
if self.argocd_enabled.get():
|
|
if getattr(self, "_argocd_success", False):
|
|
self.next_button.configure(state="normal")
|
|
else:
|
|
self.next_button.configure(state="disabled")
|
|
else:
|
|
self.next_button.configure(state="normal")
|
|
elif pid == "init_scripts":
|
|
# Initialization Scripts: Next is disabled until success
|
|
if getattr(self, "_scripts_success", False):
|
|
self.next_button.configure(state="normal")
|
|
else:
|
|
self.next_button.configure(state="disabled")
|
|
elif pid == "kerberos_config":
|
|
if self.kerberos_enabled.get():
|
|
if getattr(self, "_kerberos_success", False):
|
|
self.next_button.configure(state="normal")
|
|
else:
|
|
self.next_button.configure(state="disabled")
|
|
else:
|
|
self.next_button.configure(state="normal")
|
|
# Visibility rules
|
|
self.prev_button.pack_forget()
|
|
self.next_button.pack_forget()
|
|
if self.deploy_button:
|
|
self.deploy_button.pack_forget()
|
|
if self.launch_button:
|
|
self.launch_button.pack_forget()
|
|
if self.gear_button:
|
|
self.gear_button.pack_forget()
|
|
|
|
if pid == "init_password" and self.gear_button:
|
|
self.gear_button.pack(side="left", padx=(20, 0), pady=12)
|
|
|
|
if pid == "create_installer":
|
|
self.next_button.configure(text="Finish")
|
|
self.next_button.pack(side="right", padx=(0, 20), pady=12)
|
|
return
|
|
|
|
if first:
|
|
self.next_button.pack(side="right", padx=(0, 20), pady=12)
|
|
else:
|
|
# [Prev] [Next] clustered right
|
|
self.next_button.pack(side="right", padx=(0, 20), pady=12)
|
|
self.prev_button.pack(side="right", padx=(0, 8), pady=12)
|
|
|
|
def _splash_should_hide_nav(self) -> bool:
|
|
"""Deprecated: Welcome page no longer performs dependency checks or gates navigation."""
|
|
return False
|