merge Dependencies + Deploy into single 'Setup & Deploy' page; add console preview, Next binding, and stable footer"

Live-updating Dependencies (background checks via installer.config.get_dep_info)\n- Final steps retained: pre-completed items + Start Prole Workstation + Install Prole.app\n- Console shows docker run; Enter/Next runs detatched and verifies\n- Drag-to-install popup marks completion on close\n- Fixed-height footer to prevent background geometry shifts\n- Back/Next footer; Next triggers same action as Enter\n\nNote: docker run preview didn’t appear for the user; follow-up fix planned.
This commit is contained in:
chrisfu 2025-12-04 21:03:06 -08:00
parent 120c9c2567
commit bbeef67a0f
2 changed files with 279 additions and 37 deletions

View File

@ -3,14 +3,15 @@ Deploy/build helpers for the Prole installer (root-level package).
Encapsulates Xcode tools check and building the native Prole app.
Also defines the Deploy page (new logic):
1) Start Docker (ensure it's running)
2) Start Ollama serve
3) Start the prole-workstation container with required ports
Also defines the Deploy page (final milestone):
- Mark completed: Build Prole macOS App, Build Prole Workstation Docker Image, Verify Dependencies
- New: Start Prole Workstation (show docker run command in the black console, wait for Enter, run, verify)
- New: Install Prole.app (drag-to-install pop-up; mark complete when closed)
"""
from __future__ import annotations
from pathlib import Path
import os
import threading
import platform
import subprocess
@ -94,27 +95,118 @@ def _create_deploy_row(app, parent, step: dict):
status = ttk.Label(row, text='Pending', style='Dim.TLabel')
status.pack(side='right', padx=8)
app.deploy_widgets[step['name']] = {'canvas': canvas, 'label': status, 'step': step}
_draw_status(canvas, 'pending')
# Initialize status icon/text
init = step.get('status', 'pending')
_draw_status(canvas, init)
try:
if init in ('success', 'completed'):
status.configure(text='Done')
elif init == 'running':
status.configure(text='Running…')
else:
status.configure(text=init.title())
except Exception:
pass
def create_deploy_page(app):
"""Create the Deploy page UI and register it via installer.main.
New simplified 3-step flow:
1) Ensure Docker is running
2) Start Ollama serve
3) Run prole-workstation container (5901, 6667, 6697)
Final milestone flow for Deploy page:
- Show 3 completed steps from earlier phases
- Start Prole Workstation (docker run)
- Install Prole.app (drag-to-install pop-up)
"""
f = ttk.Frame(app.page_area)
f.place(x=0, y=0, relwidth=1, relheight=1)
ttk.Label(f, text='Deploy', style='Title.TLabel').pack(anchor='w', padx=24, pady=(24, 6))
ttk.Label(f, text='We will bring services online in order: Docker, Ollama, and the prole-workstation container.', style='Body.TLabel', wraplength=800).pack(anchor='w', padx=24)
ttk.Label(f, text='Setup & Deploy', style='Title.TLabel').pack(anchor='w', padx=24, pady=(24, 6))
ttk.Label(
f,
text='We will verify dependencies and then complete final deployment steps.',
style='Body.TLabel',
wraplength=800
).pack(anchor='w', padx=24)
# ----- Dependencies section (merged onto the first page) -----
deps_box = ttk.LabelFrame(f, text='Dependencies')
deps_box.pack(fill='x', padx=16, pady=(12, 8))
deps_wrap = ttk.Frame(deps_box)
deps_wrap.pack(fill='x', padx=8, pady=8)
app._deploy_dep_widgets = {}
def _dep_icon(canvas: tk.Canvas, status: str):
canvas.delete('all')
if status == 'ok':
canvas.create_oval(2, 2, 18, 18, fill='#34c759', outline='')
canvas.create_line(5, 10, 9, 14, fill='white', width=2)
canvas.create_line(9, 14, 16, 6, fill='white', width=2)
elif status == 'checking':
canvas.create_oval(2, 2, 18, 18, fill='#ffd60a', outline='')
elif status == 'missing':
canvas.create_oval(2, 2, 18, 18, fill='#ff3b30', outline='')
else:
canvas.create_oval(2, 2, 18, 18, outline='#b0b0b0')
def _create_dep_row(parent, dep: dict):
row = ttk.Frame(parent)
row.pack(fill='x', pady=4)
cnv = tk.Canvas(row, width=20, height=20, highlightthickness=0)
cnv.pack(side='left', padx=(4, 8))
name = ttk.Label(row, text=dep['name'], style='Body.TLabel')
name.pack(side='left')
status = ttk.Label(row, text='Checking…', style='Dim.TLabel')
status.pack(side='right', padx=8)
app._deploy_dep_widgets[dep['id']] = {'canvas': cnv, 'label': status, 'dep': dep}
_dep_icon(cnv, 'checking')
try:
from . import config as _cfg
deps = list(_cfg.DEPENDENCIES)
except Exception:
deps = []
for d in deps:
_create_dep_row(deps_wrap, d)
# Background thread to check dependencies live
def _check_deps_bg():
try:
from . import config as _cfg2
for d in deps:
try:
installed, location, version = _cfg2.get_dep_info(d)
except Exception:
installed, location, version = False, None, None
w = app._deploy_dep_widgets.get(d['id'])
if not w:
continue
lbl = w['label']
cnv = w['canvas']
try:
if installed:
_dep_icon(cnv, 'ok')
txt = version or 'Installed'
lbl.configure(text=txt)
else:
_dep_icon(cnv, 'missing')
lbl.configure(text='Missing')
except Exception:
pass
except Exception:
pass
try:
threading.Thread(target=_check_deps_bg, daemon=True).start()
except Exception:
_check_deps_bg()
# ----- Final steps (on the same merged page) -----
app.deploy_steps = [
{'name': 'Start Docker (ensure it is running)', 'status': 'pending'},
{'name': 'Start Ollama serve', 'status': 'pending'},
{'name': 'Start prole-workstation container (5901, 6667, 6697)', 'status': 'pending'},
{'name': 'Build Prole macOS App', 'status': 'completed'},
{'name': 'Build Prole Workstation Docker Image', 'status': 'completed'},
{'name': 'Verify Dependencies', 'status': 'completed'},
{'name': 'Start Prole Workstation', 'status': 'pending'},
{'name': 'Install Prole.app', 'status': 'pending'},
]
app.deploy_widgets = {}
container = ttk.Frame(f)
@ -124,32 +216,176 @@ def create_deploy_page(app):
inst_main.register_page(app, 'deploy', f)
# Kick off the steps asynchronously so UI stays responsive
def _run_steps():
_update_step(app, app.deploy_steps[0]['name'], 'running')
if _ensure_docker_running():
_update_step(app, app.deploy_steps[0]['name'], 'success')
else:
_update_step(app, app.deploy_steps[0]['name'], 'error')
return
_update_step(app, app.deploy_steps[1]['name'], 'running')
if _ensure_ollama_running():
_update_step(app, app.deploy_steps[1]['name'], 'success')
else:
_update_step(app, app.deploy_steps[1]['name'], 'error')
return
_update_step(app, app.deploy_steps[2]['name'], 'running')
if _ensure_workstation_container():
_update_step(app, app.deploy_steps[2]['name'], 'success')
else:
_update_step(app, app.deploy_steps[2]['name'], 'error')
# Prepare console overlay to display the docker run command
try:
threading.Thread(target=_run_steps, daemon=True).start()
# Place the console below the step list
app._ensure_console_overlay(radio_bottom_y=160)
except Exception:
pass
# Compose docker run command for workstation
project_root = cfg.PROJECT_ROOT
version = ws.get_workstation_version(project_root)
# Ports and volume mapping per spec
port_flags = (
"-p 127.0.0.1:5901:5901 "
"-p 127.0.0.1:6667:6667 "
"-p 127.0.0.1:6697:6697"
)
host_prole = os.path.expanduser('~/dev/prole')
docker_run_cmd = (
f"docker rm -f prole-workstation >/dev/null 2>&1 || true; "
f"docker run -d --name prole-workstation --restart unless-stopped "
f"{port_flags} -v \"{host_prole}\":/prole prole-workstation:{version}"
)
# Show the command preview and wait for Enter
try:
user = os.environ.get('USER') or 'user'
host = (platform.node() or 'host').split('.')[0]
preview_line = f"[{user}@{host}]# {docker_run_cmd}"
if hasattr(app, '_console_set_preview'):
app._console_set_preview(preview_line)
except Exception:
pass
# Handlers for steps 4 and 5
state = {'started': False}
def _open_drag_install():
"""Open a simple drag-to-install window; mark complete when closed."""
try:
win = tk.Toplevel(app.root)
win.title('Install Prole.app')
win.geometry('520x320')
ttk.Label(win, text='Drag Prole.app to Applications', style='Title.TLabel').pack(pady=(16, 8))
body = ttk.Frame(win)
body.pack(expand=True, fill='both', padx=20, pady=10)
# Left: Prole.app icon (from configured icon path)
left = ttk.Frame(body)
left.pack(side='left', expand=True, fill='both')
right = ttk.Frame(body)
right.pack(side='right', expand=True, fill='both')
# Try to load images if available
icon_path = cfg.get_ui_icon_image_path()
app._drag_img_prole = None
app._drag_img_apps = None
try:
from PIL import Image, ImageTk # type: ignore
if icon_path.exists():
img = Image.open(str(icon_path)).resize((128, 128))
app._drag_img_prole = ImageTk.PhotoImage(img)
# A simple generic Applications icon (fallback: text only)
except Exception:
pass
cnv_l = tk.Canvas(left, width=200, height=200, highlightthickness=0)
cnv_l.pack(expand=True)
if app._drag_img_prole is not None:
cnv_l.create_image(100, 100, image=app._drag_img_prole)
else:
cnv_l.create_text(100, 100, text='Prole.app', font=('Helvetica', 14))
cnv_r = tk.Canvas(right, width=200, height=200, highlightthickness=0)
cnv_r.pack(expand=True)
cnv_r.create_text(100, 80, text='Applications', font=('Helvetica', 14))
# Action buttons
btns = ttk.Frame(win)
btns.pack(side='bottom', pady=12)
def open_apps():
try:
subprocess.Popen(["open", "/Applications"]) # type: ignore
except Exception:
pass
ttk.Button(btns, text='Open Applications Folder', command=open_apps).pack()
def on_close():
try:
_update_step(app, 'Install Prole.app', 'success')
except Exception:
pass
try:
win.destroy()
except Exception:
pass
win.protocol('WM_DELETE_WINDOW', on_close)
except Exception:
# Even if popup fails, don't crash the deploy page
pass
def _start_workstation_and_verify():
# Kick off docker run and then verify container is running
try:
_update_step(app, 'Start Prole Workstation', 'running')
except Exception:
pass
try:
app._append_console("\nStarting Prole Workstation container...\n")
except Exception:
pass
try:
subprocess.run(["bash", "-lc", docker_run_cmd])
except Exception:
pass
# Verify container is running
import time as _t
ok = False
for _ in range(30): # ~30 * 0.5s = 15s
try:
r = subprocess.run(["bash", "-lc", "docker inspect -f '{{.State.Running}}' prole-workstation || echo false"], capture_output=True, text=True, timeout=4)
if r.returncode == 0 and 'true' in (r.stdout or '').lower():
ok = True
break
except Exception:
pass
_t.sleep(0.5)
try:
if ok:
_update_step(app, 'Start Prole Workstation', 'success')
app._append_console("Prole Workstation is running.\n")
else:
_update_step(app, 'Start Prole Workstation', 'error')
app._append_console("Failed to start Prole Workstation. See Docker for details.\n")
except Exception:
pass
def _on_enter(_evt=None):
# Only handle once
if state['started']:
return
state['started'] = True
try:
app._console_press_enter()
except Exception:
pass
# While docker deploy is running, pop up the drag-to-install window
try:
threading.Thread(target=_open_drag_install, daemon=True).start()
except Exception:
_open_drag_install()
# Start docker run + verification in background
try:
threading.Thread(target=_start_workstation_and_verify, daemon=True).start()
except Exception:
_start_workstation_and_verify()
# Bind Enter to trigger the start when the user is ready
try:
app.root.bind('<Return>', _on_enter)
app.root.bind('<KP_Enter>', _on_enter)
except Exception:
pass
# Navigation footer with Next button to proceed/trigger actions
def _noop():
return None
try:
btns = ui.create_nav_footer(f, [(1, 'Back'), (2, 'Next')], {1: _noop, 2: _on_enter})
# Optionally expose next_button like install.py does
try:
app.next_button = btns.get(2)
except Exception:
pass
except Exception:
pass
return f

View File

@ -158,6 +158,12 @@ def create_nav_footer(parent, buttons: list[tuple[int, str]], commands: dict[int
footer = ttk.Frame(parent)
footer.pack(fill='x', side='bottom')
# Ensure stable geometry even when there are no buttons
try:
footer.configure(height=60)
footer.pack_propagate(False)
except Exception:
pass
# Flexible spacer to push buttons to the right
spacer = ttk.Frame(footer)