From 58d693de054e9e1939643cc59e3cface18ddcf1e Mon Sep 17 00:00:00 2001 From: chrisfu Date: Tue, 27 Jan 2026 22:32:27 -0800 Subject: [PATCH] UI: Standardized console appearance and fixed cosmetic inconsistencies. This commit establishes a stable, consistent UI across the entire installer: Unified 'Light Background, Dark Text' theme for all console components. Standardized console creation routine to eliminate thick black borders on macOS. Refactored SSH key generation to use the standard canvas-based layout. Switched to tk.Frame and tk.Scrollbar for TerminalConsole to resist macOS dark mode shifts. Fixed shutil.SameFileError when PROLE_HOME matches the source directory. Improved notebook styling for inactive tabs and execution outputs. All screens now share the same professional look and feel. --- etc/init_cloudnative_pg.sh | 6 +- etc/init_openbao.sh | 25 ++- install.py | 359 ++++++++++++++++++++----------------- installer/screen.py | 21 ++- 4 files changed, 233 insertions(+), 178 deletions(-) diff --git a/etc/init_cloudnative_pg.sh b/etc/init_cloudnative_pg.sh index 3adb1ac..e7c27d5 100755 --- a/etc/init_cloudnative_pg.sh +++ b/etc/init_cloudnative_pg.sh @@ -221,7 +221,8 @@ initialize() { # Ensure port-forward is running for OpenBao (dependency) echo "Ensuring port-forward for OpenBao is active ..." - "$SCRIPT_DIR/init_port_forwards.sh" restart openbao + "$SCRIPT_DIR/init_port_forwards.sh" restart openbao & + sleep 2 fetch_admin_keys_and_db_pass_from_bao_or_local apply_cnpg_admin_secret @@ -231,7 +232,8 @@ initialize() { # Ensure port-forward is running for Postgres (local access) echo "Ensuring port-forward for Postgres is active ..." - "$SCRIPT_DIR/init_port_forwards.sh" restart postgres + "$SCRIPT_DIR/init_port_forwards.sh" restart postgres & + sleep 2 echo "Initialization complete for CNPG + Kerberos + cert artifacts." } diff --git a/etc/init_openbao.sh b/etc/init_openbao.sh index 8ab464d..a458e39 100755 --- a/etc/init_openbao.sh +++ b/etc/init_openbao.sh @@ -459,13 +459,14 @@ case "$ACTION" in if [[ -n "$db_pass" ]]; then username=${PROLE_DB_USER:-"prole"} - echo "Creating database user secret 'prole-db-user' for '$username' ..." + echo "Checking/creating database user secret 'prole-db-user' for '$username' ..." + # Use apply to be idempotent kubectl create secret generic prole-db-user -n "$NAMESPACE" \ --from-literal=username="$username" \ --from-literal=password="$db_pass" \ --dry-run=client -o yaml | kubectl apply -n "$NAMESPACE" -f - - echo "Creating database superuser secret 'prole-db-superuser' ..." + echo "Checking/creating database superuser secret 'prole-db-superuser' ..." kubectl create secret generic prole-db-superuser -n "$NAMESPACE" \ --from-literal=username=postgres \ --from-literal=password="$db_pass" \ @@ -476,13 +477,21 @@ case "$ACTION" in # Ensure port-forward is running for OpenBao echo "Ensuring port-forward for OpenBao is active ..." - "$SCRIPT_DIR/init_port_forwards.sh" restart openbao + "$SCRIPT_DIR/init_port_forwards.sh" restart openbao & + sleep 2 - if ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; then - echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200." >&2 - echo "Please start port-forwards: $SCRIPT_DIR/init_port_forwards.sh start openbao" >&2 - exit 1 - fi + local max_retries=15 + local count=0 + while ! curl -sS http://127.0.0.1:18200/v1/sys/health >/dev/null 2>&1; do + if [[ $count -ge $max_retries ]]; then + echo "ERROR: OpenBao not reachable at http://127.0.0.1:18200 after waiting." >&2 + echo "Please check logs: $SCRIPT_DIR/init_port_forwards.sh status openbao" >&2 + exit 1 + fi + echo "Waiting for OpenBao port-forward to be ready... ($count/$max_retries)" + sleep 2 + count=$((count + 1)) + done init_openbao_kv_and_store_admin_key echo "Initialization complete. Manifests in: $OPENBAO_MANIFEST_DIR" ;; diff --git a/install.py b/install.py index d453ac6..630dc6d 100755 --- a/install.py +++ b/install.py @@ -56,6 +56,12 @@ def get_resource_path(relative_path): return base_path / relative_path +# Standard Console Theme (Light background, Dark text) +CONSOLE_BG = '#F5F5DC' # Cream/Light background similar to our images +CONSOLE_FG = '#1d1d1f' # Dark text +CONSOLE_INSERT = '#1d1d1f' +CONSOLE_FONT = ('Menlo', 10) + class ProleController: """Business logic for Prole installer, separated from UI.""" def __init__(self, project_root): @@ -99,25 +105,28 @@ class ProleController: target_k8s = prole_home / "k8s" source_k8s = self.project_root / "k8s" if source_k8s.exists(): - if not target_k8s.exists() or (source_k8s.stat().st_mtime > target_k8s.stat().st_mtime): - if target_k8s.exists(): - shutil.rmtree(target_k8s) - shutil.copytree(source_k8s, target_k8s) + if source_k8s.resolve() != target_k8s.resolve(): + if not target_k8s.exists() or (source_k8s.stat().st_mtime > target_k8s.stat().st_mtime): + if target_k8s.exists(): + shutil.rmtree(target_k8s) + shutil.copytree(source_k8s, target_k8s) # Ensure conf/postgresql is copied for version detection target_conf = prole_home / "conf" source_conf = self.project_root / "conf" if source_conf.exists(): - if not target_conf.exists() or (source_conf.stat().st_mtime > target_conf.stat().st_mtime): - # Only copy what we need or the whole thing? etc/ already exists in PROLE_HOME. - # Let's copy the whole conf dir if it doesn't exist or is older. - if target_conf.exists(): - shutil.rmtree(target_conf) - shutil.copytree(source_conf, target_conf) + if source_conf.resolve() != target_conf.resolve(): + if not target_conf.exists() or (source_conf.stat().st_mtime > target_conf.stat().st_mtime): + # Only copy what we need or the whole thing? etc/ already exists in PROLE_HOME. + # Let's copy the whole conf dir if it doesn't exist or is older. + if target_conf.exists(): + shutil.rmtree(target_conf) + shutil.copytree(source_conf, target_conf) # Copy script and set permissions if source_script.exists(): - shutil.copy2(source_script, target_script) + if source_script.resolve() != target_script.resolve(): + shutil.copy2(source_script, target_script) os.chmod(target_script, 0o755) # Run from the installed location @@ -129,6 +138,7 @@ class ProleController: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + bufsize=0, env=env ) @@ -197,7 +207,7 @@ class ProleInstaller: self.sidebar.pack_propagate(False) # Vertical Divider Line - self.divider = tk.Frame(self.main_container, bg='#CCCCCC', width=1) + self.divider = tk.Frame(self.main_container, bg='#CCCCCC', width=1, highlightthickness=0, bd=0) self.divider.pack(side='left', fill='y') # Right Content Area (approx 66%) @@ -509,6 +519,23 @@ class ProleInstaller: 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: @@ -1068,8 +1095,25 @@ class ProleInstaller: def _render_paragraph(self, text, y, wrap=600): ui.render_paragraph(self, text, y, wrap) + def _create_console_output(self, y=280, title="Scan Output", width=880, height=450): + """Create a standardized console output area with a label and scrollable text.""" + # Section title/label for the console + ui.canvas_text(self, 48, y, title, fill='#1d1d1f', font=('SF Pro Text', 12, 'bold')) + + y_console = y + 30 + # Use a background frame to ensure NO borders are visible around the console + console_bg = tk.Frame(self.bg_canvas, bg='white', highlightthickness=0, bd=0) + console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) + console.pack(fill='both', expand=True, padx=1, pady=1) + + console_window = self.bg_canvas.create_window(48, y_console, window=console_bg, + anchor='nw', width=width, height=height) + self._canvas_items.append(console_window) + self._overlay_widgets.append(console_bg) + self._overlay_widgets.append(console) + return console + def _render_welcome_page(self): - # Opening page should NOT perform any dependency checks. # Letterhead at top right content_width = self.bg_canvas.winfo_width() or 975 # 1300 * 0.75 approx right_margin = content_width - 48 @@ -1094,7 +1138,7 @@ class ProleInstaller: "Welcome to the neighborhood! Let's get started by preparing your system for the Prole experience." ) - ui.canvas_text(self, 48, 210, welcome_text, fill='black', font=('SF Pro Text', 13), width=750) + ui.canvas_text(self, 48, 220, welcome_text, fill='black', font=('SF Pro Text', 13), width=750) # Ensure footer is updated (Next button visible) self.update_footer() @@ -1124,34 +1168,11 @@ class ProleInstaller: pass self.scan_status_var.trace_add('write', update_status_text) - y += 40 - # Tabs for Scan Output and LLM Analysis - self.scan_notebook = ttk.Notebook(self.bg_canvas) - self.scan_tab = tk.Frame(self.scan_notebook, bg='white') - self.analysis_tab = tk.Frame(self.scan_notebook, bg='white') + # Standardized Console Output + self.scan_results_console = self._create_console_output(y=320, title="Scan Output", width=880, height=380) + self.scan_results_text = self.scan_results_console.text - self.scan_notebook.add(self.scan_tab, text="Scan Output") - self.scan_notebook.add(self.analysis_tab, text="AI Analysis", state='disabled') - - # Output Box (placed inside scan_tab) - self.scan_results_text = scrolledtext.ScrolledText(self.scan_tab, width=110, height=22, - font=('Menlo', 10), bg='white', fg='black', - insertbackground='black', highlightthickness=1, - highlightbackground='#CCCCCC') - self.scan_results_text.pack(fill='both', expand=True, padx=5, pady=5) - - # Analysis Box (placed inside analysis_tab) - self.analysis_results_text = scrolledtext.ScrolledText(self.analysis_tab, width=110, height=22, - font=('SF Pro Text', 11), bg='#F9F9F9', fg='black', - insertbackground='black', highlightthickness=1, - highlightbackground='#CCCCCC') - self.analysis_results_text.pack(fill='both', expand=True, padx=5, pady=5) - - notebook_window = self.bg_canvas.create_window(48, y, window=self.scan_notebook, anchor='nw', width=880, height=450) - self._overlay_widgets.append(self.scan_notebook) - self._canvas_items.append(notebook_window) - - y += 480 + y = 740 # Start Network Scan Button (placed on canvas) self.scan_btn = tk.Button(self.bg_canvas, text="Start Network Scan", command=self._run_network_scan, @@ -1166,7 +1187,7 @@ class ProleInstaller: if not GLOBAL_SCAN_FRAMES: try: from PIL import Image, ImageTk - gif_path = PROJECT_ROOT / 'img' / 'prole-type.gif' + gif_path = get_resource_path('img/prole-type.gif') if gif_path.exists(): gif = Image.open(str(gif_path)) try: @@ -1202,12 +1223,8 @@ class ProleInstaller: return self._scan_running = True - # Reset tabs - self.scan_notebook.tab(1, state='disabled') - self.scan_notebook.select(0) - - self.scan_results_text.delete('1.0', tk.END) - self.scan_results_text.insert(tk.END, "Initializing network scan using prole-net/prole-scan ...\n") + self.scan_results_console.clear() + self.scan_results_console.write("Initializing network scan using prole-net/prole-scan ...\n") self.scan_status_var.set("Scanning...") # Start animation @@ -1235,7 +1252,7 @@ class ProleInstaller: stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, - bufsize=1, + bufsize=0, cwd=str(scan_dir)) # Capture output in real-time @@ -1244,8 +1261,11 @@ class ProleInstaller: if not line and process.poll() is not None: break if line: - self.root.after(0, lambda l=line: self.scan_results_text.insert(tk.END, l)) - self.root.after(0, lambda: self.scan_results_text.see(tk.END)) + # Use a method that forces text onto the console + def _append(l): + self.scan_results_console.write(l) + self.scan_results_text.see(tk.END) + self.root.after(0, lambda l=line: _append(l)) # Heuristic parsing for Kerberos/AD to auto-fill if "Active Directory" in line or "88" in line: @@ -1273,7 +1293,7 @@ class ProleInstaller: self._scan_running = False except Exception as e: - self.root.after(0, lambda: self.scan_results_text.insert(tk.END, f"Scan error: {str(e)}\n")) + self.root.after(0, lambda: self.scan_results_console.write(f"Scan error: {str(e)}\n")) self.root.after(0, lambda: self.scan_status_var.set("Scan failed")) self._scan_running = False @@ -1446,18 +1466,32 @@ class ProleInstaller: ["kinit", principal], stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True + stderr=subprocess.STDOUT, + text=True, + bufsize=0 ) - stdout, stderr = process.communicate(input=password + "\n") + # Send password if needed + if password: + process.stdin.write(password + "\n") + process.stdin.close() + + # Real-time output capture for Kerberos too + while True: + line = process.stdout.readline() + if not line and process.poll() is not None: + break + if line: + self.root.after(0, lambda l=line: self.kerberos_status_text.insert(tk.END, l)) + self.root.after(0, lambda: self.kerberos_status_text.see(tk.END)) + if process.returncode == 0: self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, "Success! Authenticated successfully.\n")) # Get ticket details klist_res = subprocess.run(["klist"], capture_output=True, text=True) self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"\nTicket details:\n{klist_res.stdout}")) else: - self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"Failed to authenticate.\nExit code: {process.returncode}\nError: {stderr}\n")) + self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"Failed to authenticate.\nExit code: {process.returncode}\n")) except Exception as e: self.root.after(0, lambda: self.kerberos_status_text.insert(tk.END, f"Error: {str(e)}\n")) @@ -1703,27 +1737,26 @@ class ProleInstaller: # ---------------- Initialize Screen Handlers ---------------- def _generate_ssh_key_with_overlay(self): - """Generate ed25519 SSH key pair with a status overlay.""" - # Create a semi-transparent overlay - overlay_bg = ui.canvas_rectangle(self, 0, 0, 2000, 2000, fill='black', state='normal') - self.bg_canvas.itemconfig(overlay_bg, stipple='gray50') # Approximation of transparency - self._canvas_items.append(overlay_bg) - - # Center console - cw = self.bg_canvas.winfo_width() or 975 - ch = self.bg_canvas.winfo_height() or 800 + """Generate ed25519 SSH key pair using a standard screen layout.""" + self._clear_canvas_page() - console_w, console_h = 700, 400 - x = (cw - console_w) // 2 - y = (ch - console_h) // 2 + # Letterhead at top right + content_width = self.bg_canvas.winfo_width() or 975 + right_margin = content_width - 48 - console = ui.TerminalConsole(self.bg_canvas) - console_window = self.bg_canvas.create_window(x, y, window=console, anchor='nw', width=console_w, height=console_h) - self._canvas_items.append(console_window) - self._overlay_widgets.append(console) + 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') - status_text = ui.canvas_text(self, x, y - 30, "Generating SSH Key Pair...", fill='white', font=('SF Pro Text', 14, 'bold')) - self._canvas_items.append(status_text) + self._render_title('Generate SSH Key', y=150) + self._render_paragraph('Generating ed25519 SSH key pair for secure database access.', y=200) + + # Output Console - standardized to match Docker Build screen + console = self._create_console_output(y=260, title="SSH Output", width=900, height=520) + + # Status Label + status_label = ui.canvas_text(self, 48, 812, "Initializing...", fill='black', font=('SF Pro Text', 12)) + self._canvas_items.append(status_label) def worker(): key_path = Path.home() / ".ssh" / "id_prole_ed25519" @@ -1731,8 +1764,10 @@ class ProleInstaller: if key_path.exists(): console.write(f"Key already exists at {key_path}. Skipping generation.\n") - time.sleep(1) + self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="SSH key already exists. Proceeding...", fill='#34c759') if self.bg_canvas.winfo_exists() else None) + time.sleep(1.5) else: + self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="Generating key...", fill='blue') if self.bg_canvas.winfo_exists() else None) cmd = ["ssh-keygen", "-t", "ed25519", "-N", "", "-f", str(key_path), "-C", self.db_username.get()] console.write(f"Running: {' '.join(cmd)}\n\n") @@ -1746,8 +1781,10 @@ class ProleInstaller: if proc.returncode == 0: console.write("\nSSH key generated successfully.\n") + self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text="SSH key generated successfully.", fill='#34c759') if self.bg_canvas.winfo_exists() else None) else: console.write(f"\nError generating SSH key (code {proc.returncode})\n") + self.safe_after(lambda: self.bg_canvas.itemconfig(status_label, text=f"Error generating key (code {proc.returncode})", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None) # In case of error, we might want to let the user see it before continuing or stopping time.sleep(2) @@ -1907,10 +1944,7 @@ class ProleInstaller: self._render_paragraph('Building the prole-db Postgres image. This may take a few minutes.', y=200) # Output Console - self._db_build_console = ui.TerminalConsole(self.bg_canvas) - console_window = self.bg_canvas.create_window(48, 260, window=self._db_build_console, anchor='nw', width=900, height=520) - self._canvas_items.append(console_window) - self._overlay_widgets.append(self._db_build_console) + self._db_build_console = self._create_console_output(y=260, title="Build Output", width=900, height=520) # Use tk.Button self._db_build_button = tk.Button(self.bg_canvas, text='Start Build', command=self.run_db_build, @@ -1953,9 +1987,10 @@ class ProleInstaller: if source_dir.exists(): import shutil # Remove old build dir and copy fresh - if build_dir.exists(): - shutil.rmtree(build_dir) - shutil.copytree(source_dir, build_dir) + if source_dir.resolve() != build_dir.resolve(): + if build_dir.exists(): + shutil.rmtree(build_dir) + shutil.copytree(source_dir, build_dir) cwd = build_dir @@ -2079,10 +2114,7 @@ class ProleInstaller: self._render_paragraph('Deploying the CloudNative-PG operator and cluster manifests to Kubernetes.', y=200) # Output Console - self._cnpg_deploy_console = ui.TerminalConsole(self.bg_canvas) - console_window = self.bg_canvas.create_window(48, 260, window=self._cnpg_deploy_console, anchor='nw', width=900, height=520) - self._canvas_items.append(console_window) - self._overlay_widgets.append(self._cnpg_deploy_console) + self._cnpg_deploy_console = self._create_console_output(y=260, title="Deployment Output", width=900, height=520) # Use tk.Button self._cnpg_deploy_button = tk.Button(self.bg_canvas, text='Run Deployment', command=self.run_cnpg_deploy, @@ -2191,10 +2223,17 @@ class ProleInstaller: self._render_title('Initialization Scripts', y=150) self._render_paragraph('Running initialization scripts to set up OpenBao, CloudNative-PG, and Port Forwards.', y=200) - # Tabs for output - self.script_tabs = ttk.Notebook(self.bg_canvas) - tab_window = self.bg_canvas.create_window(48, 260, window=self.script_tabs, anchor='nw', width=900, height=480) + # Tabs for output - using standardized appearance + ui.canvas_text(self, 48, 260, "Execution Output", fill='#1d1d1f', font=('SF Pro Text', 12, 'bold')) + + # Use a background frame for the notebook to hide potential system borders + notebook_bg = tk.Frame(self.bg_canvas, bg='white', highlightthickness=0, bd=0) + self.script_tabs = ttk.Notebook(notebook_bg, style='TNotebook') + self.script_tabs.pack(fill='both', expand=True, padx=1, pady=1) + + tab_window = self.bg_canvas.create_window(48, 290, window=notebook_bg, anchor='nw', width=900, height=450) self._canvas_items.append(tab_window) + self._overlay_widgets.append(notebook_bg) self._overlay_widgets.append(self.script_tabs) self.script_consoles = {} @@ -2208,10 +2247,12 @@ class ProleInstaller: ] for title, fname in scripts: - frame = tk.Frame(self.script_tabs, bg='white') - self.script_tabs.add(frame, text=title) - console = ui.TerminalConsole(frame) - console.pack(fill='both', expand=True) + # Use a background frame to ensure NO borders are visible around the console + console_bg = tk.Frame(self.script_tabs, bg='white', highlightthickness=0, bd=0) + self.script_tabs.add(console_bg, text=title) + # Use TerminalConsole for consistent styling + console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) + console.pack(fill='both', expand=True, padx=1, pady=1) self.script_consoles[fname] = console # Use tk.Button @@ -2222,7 +2263,7 @@ class ProleInstaller: btn_window = self.bg_canvas.create_window(48, 760, window=self._init_scripts_button, anchor='nw', width=180) self._canvas_items.append(btn_window) self._overlay_widgets.append(self._init_scripts_button) - + # Status Label self._init_scripts_status_label = ui.canvas_text(self, 240, 772, "", fill='black', font=('SF Pro Text', 12)) self._canvas_items.append(self._init_scripts_status_label) @@ -2385,6 +2426,7 @@ class ProleInstaller: self.safe_after(lambda: self.bg_canvas.itemconfig(self._init_scripts_status_label, text="Initialization failed.", fill='#ff3b30') if self.bg_canvas.winfo_exists() else None) self._scripts_success = False + self.safe_after(lambda: self.update_footer()) self.safe_after(lambda: self._init_scripts_button.configure(state='normal') if self._init_scripts_button.winfo_exists() else None) threading.Thread(target=worker, daemon=True).start() @@ -2590,10 +2632,7 @@ class ProleInstaller: self.bg_canvas.bind('', _on_click) # Output Console (Consistent size and location: y=260, width=900, height=520) - self._build_console = ui.TerminalConsole(self.bg_canvas) - console_window = self.bg_canvas.create_window(48, 260, window=self._build_console, anchor='nw', width=900, height=520) - self._canvas_items.append(console_window) - self._overlay_widgets.append(self._build_console) + self._build_console = self._create_console_output(y=230, title="Build Output", width=900, height=550) self._console_text = self._build_console.text # For compatibility with _append_console and others # Show a command preview with PS1-style prompt and blinking cursor @@ -2884,6 +2923,12 @@ class ProleInstaller: self.next_button.configure(text='Next') else: self.next_button.configure(text='Build') + 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 == 'create_installer': self.next_button.configure(text='Finish') @@ -3140,8 +3185,12 @@ class ProleInstaller: ttk.Combobox(wrap, textvariable=self.deploy_environment, values=['Dev', 'Service', 'Prod'], state='readonly', width=18).pack(side='left', padx=10) # Build output console - self.build_output = scrolledtext.ScrolledText(f, height=12, bg='#fafafa', fg='#1d1d1f') - self.build_output.pack(fill='both', expand=True, padx=24, pady=12) + # Use a background frame to ensure NO borders are visible around the console + console_bg = tk.Frame(f, bg='white', highlightthickness=0, bd=0) + console_bg.pack(fill='both', expand=True, padx=24, pady=12) + self.build_output_console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) + self.build_output_console.pack(fill='both', expand=True, padx=1, pady=1) + self.build_output = self.build_output_console.text # self._register_page('build', f) @@ -3368,7 +3417,7 @@ echo "-------------------------------------------------------------------"; # ---------------- Embedded console helpers ---------------- def _ensure_console_overlay(self, radio_bottom_y: int = 160): - """Create semi-transparent black backdrop and a ScrolledText console overlay. + """Create semi-transparent backdrop and a ScrolledText console overlay. The overlay is placed within slide_area between given top and bottom margins. """ # Ensure slide_area is visible @@ -3378,30 +3427,23 @@ echo "-------------------------------------------------------------------"; # Compute geometry within slide area geom = self._compute_console_geometry(radio_bottom_y) left, top, width, height = geom - # Draw a stippled rectangle on the canvas to simulate ~60% opacity - rect = ui.canvas_rectangle(self, left, top, left + width, top + height, fill='#000000', outline='') - try: - # Apply stipple directly on the canvas item if supported - self.bg_canvas.itemconfig(rect, stipple='gray50') - except Exception: - pass - self._canvas_items.append(rect) - # Create ScrolledText overlay - txt = scrolledtext.ScrolledText(self.slide_area, wrap='word', - bg='#000000', fg='#ffffff', insertbackground='#ffffff', - font=('Menlo', 11), relief='flat', bd=0, highlightthickness=0) - txt.place(x=left + 8, y=top + 8, width=max(50, width - 16), height=max(50, height - 16)) - self._overlay_widgets.append(txt) - self._console_text = txt + + # Use a background frame to ensure NO borders are visible around the console + console_bg = tk.Frame(self.slide_area, bg='white', highlightthickness=0, bd=0) + console_bg.place(x=left, y=top, width=width, height=height) + + # Create TerminalConsole overlay + console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) + console.pack(fill='both', expand=True, padx=1, pady=1) + self._overlay_widgets.append(console_bg) + self._overlay_widgets.append(console) + self._console_text = console.text + # Keep overlay positioned on resize def _on_resize(_evt=None): l, t, w, h = self._compute_console_geometry(radio_bottom_y) try: - self.bg_canvas.coords(rect, l, t, l + w, t + h) - except Exception: - pass - try: - txt.place(x=l + 8, y=t + 8, width=max(50, w - 16), height=max(50, h - 16)) + console_bg.place(x=l, y=t, width=w, height=h) except Exception: pass # Bind to slide area; store bind id to unbind later @@ -3922,38 +3964,30 @@ echo "-------------------------------------------------------------------"; ) self._render_paragraph(tips, y=88) logp = getattr(self, 'last_build_log_path', None) - # Place a scrolled text overlay to show the log + # Place a scrollable console to show the log try: - # Use same geometry helper as console but with DARK theme (match Build console) - l, t, w, h = self._compute_console_geometry(160) - txt = scrolledtext.ScrolledText(self.slide_area, wrap='word', - bg='#000000', fg='#ffffff', insertbackground='#ffffff', - font=('Menlo', 11), relief='flat', bd=0, highlightthickness=0) - txt.place(x=l, y=t, width=max(200, w), height=max(140, h)) - self._overlay_widgets.append(txt) + # Standardized Console Output area for build summary + self.build_summary_console = self._create_console_output(y=160, title="Build Log Output", width=900, height=520) + txt = self.build_summary_console.text + if logp and os.path.exists(logp): try: with open(logp, 'r', encoding='utf-8', errors='ignore') as fp: content = fp.read() - txt.insert('1.0', content) + self.build_summary_console.write(content) except Exception as e: - txt.insert('1.0', f"Failed to read log: {e}\n") + self.build_summary_console.write(f"Failed to read log: {e}\n") else: - txt.insert('1.0', 'No build log available.') - txt.configure(state='disabled') - # Keep overlay positioned on resize - def _on_resize(_evt=None): - try: - l2, t2, w2, h2 = self._compute_console_geometry(160) - txt.place(x=l2, y=t2, width=max(200, w2), height=max(140, h2)) - except Exception: - pass - self._overlay_bind_id = self.slide_area.bind('', _on_resize) + self.build_summary_console.write('No build log available.') + + # Keep standard y for links + y_links = 760 except Exception: # Fallback: just show path self._render_paragraph('No build log could be displayed.', y=120) + y_links = 140 + # Add link to open the log file in Finder/TextEdit - y_links = 100 + 40 if logp: link = ui.render_link(self, 56, y_links, 'Open build log file') self._canvas_items.append(link) @@ -5157,7 +5191,7 @@ esac def create_validate_screen(self): """Create the Validate screen""" - frame = tk.Frame(self.content_area, bg='#1a1a1a') + frame = self._page_container() self.screens['validate'] = frame # Title @@ -5165,19 +5199,19 @@ esac title.pack(pady=(0, 20)) # Prometheus link - prometheus_frame = tk.Frame(frame, bg='#1a1a1a') + prometheus_frame = tk.Frame(frame, bg='white') prometheus_frame.pack(pady=(0, 20)) prometheus_label = tk.Label(prometheus_frame, text="Prometheus: ", - bg='#1a1a1a', - fg='#aaaaaa', + bg='white', + fg='#6e6e73', font=('Helvetica', 11)) prometheus_label.pack(side='left') prometheus_link = tk.Label(prometheus_frame, text="http://localhost:9090", - bg='#1a1a1a', + bg='white', fg='#4a9eff', font=('Helvetica', 11, 'underline'), cursor='hand2') @@ -5185,35 +5219,33 @@ esac prometheus_link.bind('', lambda e: webbrowser.open('http://localhost:9090')) # Status display - status_frame = tk.Frame(frame, bg='#1a1a1a') + status_frame = tk.Frame(frame, bg='white') status_frame.pack(fill='both', expand=True, pady=10) status_label = ttk.Label(status_frame, text="Cluster Status", style='Heading.TLabel') status_label.pack(anchor='w', pady=(0, 10)) - # Status text area - self.status_text = scrolledtext.ScrolledText(status_frame, - bg='#0a0a0a', - fg='#4a9eff', - font=('Courier', 10), - wrap='word', - relief='flat', - bd=1) - self.status_text.pack(fill='both', expand=True) + # Status text area - using TerminalConsole for consistency + # Use a background frame to ensure NO borders are visible around the console + console_bg = tk.Frame(status_frame, bg='white', highlightthickness=0, bd=0) + console_bg.pack(fill='both', expand=True, padx=1, pady=1) + self._validation_console = ui.TerminalConsole(console_bg, highlightthickness=0, bd=0) + self._validation_console.pack(fill='both', expand=True) + self.status_text = self._validation_console.text # Auto-refresh checkbox - refresh_frame = tk.Frame(frame, bg='#1a1a1a') + refresh_frame = tk.Frame(frame, bg='white') refresh_frame.pack(pady=10) self.auto_refresh_var = tk.BooleanVar(value=True) refresh_check = tk.Checkbutton(refresh_frame, text="Auto-refresh every 10 seconds", variable=self.auto_refresh_var, - bg='#1a1a1a', - fg='#aaaaaa', - selectcolor='#2a2a2a', - activebackground='#1a1a1a', - activeforeground='#aaaaaa', + bg='white', + fg='#1d1d1f', + selectcolor='#f0f0f0', + activebackground='white', + activeforeground='#1d1d1f', font=('Helvetica', 10), command=self.toggle_auto_refresh) refresh_check.pack(side='left', padx=10) @@ -5225,6 +5257,7 @@ esac bg='#4a9eff', fg='white', activebackground='#3a8eef', + highlightbackground='white', font=('Helvetica', 10), padx=15, pady=5, diff --git a/installer/screen.py b/installer/screen.py index e4ae3fb..8f95d2d 100644 --- a/installer/screen.py +++ b/installer/screen.py @@ -191,20 +191,31 @@ def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int return btn_map -class TerminalConsole(ttk.Frame): +class TerminalConsole(tk.Frame): """A scrollable text widget that mimics a terminal console.""" def __init__(self, parent, **kwargs): + bg = '#F5F5DC' # Cream/Light background + kwargs.setdefault('bg', bg) super().__init__(parent, **kwargs) - self.text = tk.Text(self, bg='#1e1e1e', fg='#f0f0f0', font=('Menlo', 11) if 'Darwin' in platform.system() else ('Consolas', 11), - padx=10, pady=10, insertbackground='white') - self.scroll = ttk.Scrollbar(self, orient='vertical', command=self.text.yview) + # Standard Console Theme (Light background, Dark text) + fg = '#1d1d1f' # Dark text + font = ('Menlo', 10) if 'Darwin' in platform.system() else ('Consolas', 10) + + self.text = tk.Text(self, bg=bg, fg=fg, font=font, + padx=10, pady=10, insertbackground=fg, + highlightthickness=0, bd=0, relief='flat') + + # Force a light scrollbar style on macOS to avoid dark mode shifts + self.scroll = tk.Scrollbar(self, orient='vertical', command=self.text.yview, + highlightthickness=0, bd=0, bg=bg, + activebackground=bg, troughcolor=bg) self.text.configure(yscrollcommand=self.scroll.set) self.scroll.pack(side='right', fill='y') self.text.pack(side='left', fill='both', expand=True) # Make read-only by default but allow selection and copying - self.text.configure(state='disabled') + self.text.configure(state='disabled', relief='flat') def write(self, content: str): """Append text to the console and scroll to the bottom."""