"""Supabase configuration and deployment screen.""" import os import subprocess import threading import tkinter as tk from knoe import prole_conf from knoe import screen as ui from knoe.core.env import PROJECT_ROOT, _normalize_cluster_env, _parse_bool class SupabaseScreenMixin: """Supabase configuration and deployment screen.""" def _render_supabase_config_page(self): # Letterhead at top right (matching welcome screen theme) content_width = self.bg_canvas.winfo_width() or 975 right_margin = content_width - 48 ui.canvas_text( self, right_margin, 40, "Prole", fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne", ) ui.canvas_text( self, right_margin, 85, "Infrastructure Automated.", fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne", ) self._render_title("Supabase", y=150) description = ( "Launches the Supabase open-source stack via supabase/deploy.sh.\n" "Default mode is local (Docker Compose). Set SUPABASE_DEPLOY_MODE=k3d\n" "to deploy into the 'supabase' Kubernetes namespace." ) self._render_paragraph(description, y=210) enable_cb = tk.Checkbutton( self.bg_canvas, text="Enable Supabase", variable=self.supabase_enabled, command=self._on_supabase_toggle, bg="white", fg="black", activebackground="white", selectcolor="white", font=("SF Pro Text", 11), ) enable_window = self.bg_canvas.create_window( 48, 250, window=enable_cb, anchor="nw" ) self._canvas_items.append(enable_window) self._overlay_widgets.append(enable_cb) y = 280 node_label = ui.canvas_text( self, 48, y, "Node selector:", fill="black", font=("SF Pro Text", 12), ) self._canvas_items.append(node_label) node_entry = tk.Entry( self.bg_canvas, textvariable=self.supabase_pv_node, font=("SF Pro Text", 11), width=38, relief="solid", bd=1, ) node_entry_window = self.bg_canvas.create_window( 190, y - 2, window=node_entry, anchor="nw" ) self._canvas_items.append(node_entry_window) self._overlay_widgets.append(node_entry) y += 36 storage_label = ui.canvas_text( self, 48, y, "Storage directory:", fill="black", font=("SF Pro Text", 12), ) self._canvas_items.append(storage_label) storage_entry = tk.Entry( self.bg_canvas, textvariable=self.supabase_pv_base_dir, font=("SF Pro Text", 11), width=38, relief="solid", bd=1, ) storage_entry_window = self.bg_canvas.create_window( 190, y - 2, window=storage_entry, anchor="nw" ) self._canvas_items.append(storage_entry_window) self._overlay_widgets.append(storage_entry) y += 42 initial_status = "Ready" if self.supabase_enabled.get() else "Disabled" self._supabase_status_var = tk.StringVar(value=initial_status) status_item = ui.canvas_text( self, 48, y, f"Status: {initial_status}", fill="black", font=("SF Pro Text", 12), ) def update_status_text(*args): try: self.bg_canvas.itemconfig( status_item, text=f"Status: {self._supabase_status_var.get()}" ) except Exception: pass self._supabase_status_var.trace_add("write", update_status_text) # Standardized Console Output self._supabase_console = self._create_console_output( y=390, title="Deployment Output", width=880, height=310 ) y = 720 # Deploy Supabase Button self._supabase_deploy_button = tk.Button( self.bg_canvas, text="Deploy Supabase", command=self.run_supabase_deploy, bg="#F5F5DC", fg="black", activebackground="#E5E5D5", activeforeground="black", highlightbackground="#F5F5DC", highlightcolor="#F5F5DC", highlightthickness=0, relief="flat", bd=0, cursor="hand2", disabledforeground="#8B8B7A", font=("SF Pro Text", 11), padx=20, pady=10, ) btn_window = self.bg_canvas.create_window( 48, y, window=self._supabase_deploy_button, anchor="nw" ) self._overlay_widgets.append(self._supabase_deploy_button) self._canvas_items.append(btn_window) def run_supabase_deploy(self): if getattr(self, "_supabase_deploying", False): return if not self.supabase_enabled.get(): self._supabase_status_var.set("Disabled") self.update_footer() return self._supabase_deploying = True self._supabase_deploy_button.configure(state="disabled") self._supabase_status_var.set("Deploying...") self._supabase_success = False self._supabase_console.clear() self.update_footer() def worker(): namespace = ( (self.db_namespace.get() or "").strip() or os.environ.get("NAMESPACE") or "default" ) env = os.environ.copy() env["PROLE_HOME"] = str(PROJECT_ROOT) env["PROLE_SERVICE"] = str(PROJECT_ROOT) env["NAMESPACE"] = namespace pv_node = "" try: pv_node = (self.supabase_pv_node.get() or "").strip() except Exception: pv_node = "" if pv_node: env["SUPABASE_PV_NODE"] = pv_node pv_base_dir = "" try: pv_base_dir = (self.supabase_pv_base_dir.get() or "").strip() except Exception: pv_base_dir = "" if pv_base_dir: env["SUPABASE_PV_BASE_DIR"] = pv_base_dir env["SUPABASE_PV_BASE"] = pv_base_dir self._supabase_console.write("Starting Supabase deployment...\n") self._supabase_console.write( f"Placement: node={pv_node or 'auto'} storage={pv_base_dir or 'auto'}\n" ) script_path = PROJECT_ROOT / "supabase" / "deploy.sh" if not script_path.exists(): self._supabase_console.write(f"Error: {script_path} not found.\n") self.safe_after( lambda: self._supabase_status_var.set("Failed (Script not found)") ) self.safe_after( lambda: self._supabase_deploy_button.configure(state="normal") ) self._supabase_deploying = False return mode = ( os.environ.get("SUPABASE_DEPLOY_MODE") or os.environ.get("SUPABASE_MODE") or "" ).strip() if not mode: # Match cluster environment to supabase deploy mode cluster_env = _normalize_cluster_env(self.cluster_env.get()) if cluster_env == "dev": mode = "k3d" elif cluster_env in ("service", "prod"): mode = "k8s" else: mode = "k3d" args = ["--mode", mode] cfg_path = None try: if self._cfg_path_override and self._cfg_path_override.exists(): cfg_path = self._cfg_path_override else: conf_dir = self._resolve_prole_conf_dir() candidate = conf_dir / "prole.cfg" if candidate.exists(): cfg_path = candidate if not cfg_path: candidate = prole_conf.entrypoint_path( prole_conf.resolve_prole_conf_dir(PROJECT_ROOT) ) if candidate.exists(): cfg_path = candidate except Exception: cfg_path = None if cfg_path: args.extend(["-c", str(cfg_path)]) try: self._save_prole_cfg() except Exception: pass if _parse_bool(os.environ.get("SUPABASE_USE_DEV_COMPOSE"), False): args.append("--with-dev-helpers") if _parse_bool(os.environ.get("SUPABASE_FOREGROUND"), False): args.append("--foreground") self._supabase_console.write(f"Mode: {mode}\n") cmd = ["bash", str(script_path)] + args try: proc = subprocess.Popen( cmd, cwd=str(PROJECT_ROOT), env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, ) except Exception as e: self._supabase_console.write(f"Failed to start deploy: {e}\n") self.safe_after( lambda: self._supabase_status_var.set("Failed (Launch error)") ) self.safe_after( lambda: self._supabase_deploy_button.configure(state="normal") ) self._supabase_deploying = False return if proc.stdout: for line in iter(proc.stdout.readline, ""): if line: self._supabase_console.write(line) proc.stdout.close() rc = proc.wait() if rc == 0: self._supabase_console.write( "\nSupabase deployment completed successfully.\n" ) ports_script = PROJECT_ROOT / "etc" / "init_supabase_ports.sh" self.safe_after( lambda: self._supabase_status_var.set("Configuring Ports...") ) if not ports_script.exists(): self._supabase_success = False self.safe_after( lambda: self._supabase_status_var.set( "Failed (Ports script not found)" ) ) self._supabase_console.write(f"Error: {ports_script} not found.\n") else: self._supabase_console.write( f"\nConfiguring Supabase ports for namespace '{namespace}'...\n" ) self._supabase_console.write( f"> bash etc/init_supabase_ports.sh -n {namespace}\n\n" ) try: ports_proc = subprocess.Popen( ["bash", str(ports_script), "-n", namespace], cwd=str(PROJECT_ROOT), env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, ) except Exception as e: self._supabase_success = False self.safe_after( lambda: self._supabase_status_var.set( "Failed (Ports launch error)" ) ) self._supabase_console.write( f"Failed to start port wiring: {e}\n" ) else: if ports_proc.stdout: for line in iter(ports_proc.stdout.readline, ""): if line: self._supabase_console.write(line) ports_proc.stdout.close() ports_rc = ports_proc.wait() if ports_rc == 0: self._supabase_success = True self.safe_after( lambda: self._supabase_status_var.set( "Deployed Successfully" ) ) self._supabase_console.write( "\nSupabase port wiring completed successfully.\n" ) else: self._supabase_success = False self.safe_after( lambda: self._supabase_status_var.set( f"Failed (Ports code {ports_rc})" ) ) self._supabase_console.write( f"\nSupabase port wiring failed with exit code {ports_rc}.\n" ) else: self._supabase_success = False self.safe_after( lambda: self._supabase_status_var.set(f"Failed (Code {rc})") ) self._supabase_console.write( f"\nSupabase deployment failed with exit code {rc}.\n" ) self._supabase_deploying = False self.safe_after( lambda: self._supabase_deploy_button.configure(state="normal") ) self.safe_after(self.update_footer) threading.Thread(target=worker, daemon=True).start() def _on_supabase_toggle(self): enabled = self.supabase_enabled.get() try: self.prole_cfg_data["Optional Features"]["SUPABASE_ENABLED"] = str(enabled) self._save_prole_cfg() except Exception: pass if not enabled: self._supabase_success = False if hasattr(self, "_supabase_status_var"): self._supabase_status_var.set("Disabled") try: self._supabase_console.write("Supabase disabled. Deployment skipped.\n") except Exception: pass self.update_footer() return if hasattr(self, "_supabase_status_var"): self._supabase_status_var.set("Enabled") try: self._supabase_console.clear() self._supabase_console.write("Supabase enabled. Starting deployment...\n") except Exception: pass self.update_footer() self.safe_after(self.run_supabase_deploy)