mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
268 lines
9.9 KiB
Python
268 lines
9.9 KiB
Python
"""Welcome / splash screen."""
|
|
|
|
import os
|
|
import threading
|
|
import time
|
|
import tkinter as tk
|
|
from tkinter import ttk, messagebox, filedialog
|
|
from knoe import screen as ui
|
|
|
|
_WELCOME_CARDS = [
|
|
{
|
|
"mode": "min",
|
|
"title": "Just a Database",
|
|
"body": "One knoe-db container via containerd.\nNo Kubernetes needed. Perfect for\ndeveloping a Spring app locally.",
|
|
"badge": "Homebrew + 1Password",
|
|
"color": "#8E44AD",
|
|
},
|
|
{
|
|
"mode": "k3d",
|
|
"title": "Local Cluster",
|
|
"body": "k3s-in-Docker cluster on your Mac.\nFull CNPG database — add Supabase,\nArgoCD, Gitea or GitLab.",
|
|
"badge": "Docker + Homebrew + 1Password",
|
|
"color": "#4A90D9",
|
|
},
|
|
{
|
|
"mode": "k3s",
|
|
"title": "Homelab",
|
|
"body": "Multi-node k3s on real hardware.\nFull Kerberos auth stack, Garage S3\nand monitoring.",
|
|
"badge": "k3sup + Homebrew + 1Password",
|
|
"color": "#27AE60",
|
|
},
|
|
{
|
|
"mode": "gke",
|
|
"title": "Production",
|
|
"body": "Dual GKE clusters on Google Cloud.\nCNPG + GCS backups and Workload\nIdentity.",
|
|
"badge": "gcloud + 1Password",
|
|
"color": "#E67E22",
|
|
},
|
|
]
|
|
|
|
_MODE_TO_CLUSTER_ENV = {"min": "min", "k3d": "dev", "k3s": "service", "gke": "prod"}
|
|
|
|
_MODE_NEEDS = {
|
|
"min": "You'll need: Homebrew + 1Password",
|
|
"k3d": "You'll need: Docker, Homebrew + 1Password",
|
|
"k3s": "You'll need: k3sup, Homebrew + 1Password",
|
|
"gke": "You'll need: gcloud CLI + 1Password",
|
|
}
|
|
|
|
|
|
class WelcomeScreenMixin:
|
|
"""Welcome / splash screen."""
|
|
|
|
def _render_welcome_page(self):
|
|
content_width = self.bg_canvas.winfo_width() or 975
|
|
right_margin = content_width - 48
|
|
|
|
# Letterhead
|
|
ui.canvas_text(self, right_margin, 40, "knoe.dev",
|
|
fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne")
|
|
ui.canvas_text(self, right_margin, 85, "infrastructure.auto()",
|
|
fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne")
|
|
|
|
self._render_title("Welcome to Knoe.DB", y=145)
|
|
|
|
intro = (
|
|
"Knoe.DB Installer sets up a production-grade PostgreSQL cluster with Kerberos "
|
|
"authentication.\nPick the mode that matches your hardware — we'll walk you through every step."
|
|
)
|
|
ui.canvas_text(self, 48, 200, intro,
|
|
fill="#1d1d1f", font=("SF Pro Text", 13), width=800)
|
|
|
|
# Mode selector cards
|
|
card_w = 185
|
|
card_h = 200
|
|
card_gap = 18
|
|
card_x0 = 48
|
|
card_y0 = 265
|
|
|
|
self._welcome_card_rects = {}
|
|
|
|
for i, card in enumerate(_WELCOME_CARDS):
|
|
x0 = card_x0 + i * (card_w + card_gap)
|
|
x1 = x0 + card_w
|
|
y0 = card_y0
|
|
y1 = card_y0 + card_h
|
|
|
|
bg = self.bg_canvas.create_rectangle(
|
|
x0, y0, x1, y1, fill="white", outline="#CCCCCC", width=2,
|
|
)
|
|
self._canvas_items.append(bg)
|
|
|
|
bar = self.bg_canvas.create_rectangle(
|
|
x0 + 1, y0 + 1, x1 - 1, y0 + 6, fill=card["color"], outline="",
|
|
)
|
|
self._canvas_items.append(bar)
|
|
|
|
mode_lbl = self.bg_canvas.create_text(
|
|
x0 + 14, y0 + 18, text=card["mode"].upper(),
|
|
fill=card["color"], font=("SF Pro Text", 9, "bold"), anchor="nw",
|
|
)
|
|
self._canvas_items.append(mode_lbl)
|
|
|
|
title_lbl = self.bg_canvas.create_text(
|
|
x0 + 14, y0 + 36, text=card["title"],
|
|
fill="#1d1d1f", font=("SF Pro Text", 13, "bold"), anchor="nw",
|
|
)
|
|
self._canvas_items.append(title_lbl)
|
|
|
|
body_lbl = self.bg_canvas.create_text(
|
|
x0 + 14, y0 + 62, text=card["body"],
|
|
fill="#555555", font=("SF Pro Text", 11), anchor="nw",
|
|
width=card_w - 28,
|
|
)
|
|
self._canvas_items.append(body_lbl)
|
|
|
|
badge_lbl = self.bg_canvas.create_text(
|
|
x0 + 14, y1 - 26, text=card["badge"],
|
|
fill=card["color"], font=("SF Pro Text", 9), anchor="nw",
|
|
width=card_w - 28,
|
|
)
|
|
self._canvas_items.append(badge_lbl)
|
|
|
|
self._welcome_card_rects[card["mode"]] = bg
|
|
|
|
mode = card["mode"]
|
|
for item_id in (bg, bar, mode_lbl, title_lbl, body_lbl, badge_lbl):
|
|
self.bg_canvas.tag_bind(
|
|
item_id, "<Button-1>",
|
|
lambda e, m=mode: self._on_welcome_card_click(m),
|
|
)
|
|
self.bg_canvas.tag_bind(
|
|
item_id, "<Enter>",
|
|
lambda e, r=bg, m=mode: self.bg_canvas.itemconfig(
|
|
r, outline=self._MODE_COLORS.get(m, "#4A90D9")
|
|
if self.deployment_mode.get() != m else None
|
|
),
|
|
)
|
|
self.bg_canvas.tag_bind(
|
|
item_id, "<Leave>",
|
|
lambda e, r=bg, m=mode: self.bg_canvas.itemconfig(
|
|
r, outline=self._MODE_COLORS.get(m, "#4A90D9")
|
|
if self.deployment_mode.get() == m else "#CCCCCC"
|
|
),
|
|
)
|
|
|
|
# "You'll need" status line
|
|
status_y = card_y0 + card_h + 20
|
|
self._splash_status_item = self.bg_canvas.create_text(
|
|
48, status_y,
|
|
text="← Select a mode above to continue",
|
|
fill="#8B8B7A", font=("SF Pro Text", 12, "italic"), anchor="nw",
|
|
)
|
|
self._canvas_items.append(self._splash_status_item)
|
|
|
|
# Restore highlight if a mode was already chosen (e.g. loaded from config)
|
|
current = self.deployment_mode.get()
|
|
if current and current in self._welcome_card_rects:
|
|
self._highlight_welcome_card(current)
|
|
try:
|
|
self.bg_canvas.itemconfig(
|
|
self._splash_status_item,
|
|
text=_MODE_NEEDS.get(current, ""),
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
self.update_footer()
|
|
|
|
def _on_welcome_card_click(self, mode: str) -> None:
|
|
self.deployment_mode.set(mode)
|
|
os.environ["KNOE_MODE"] = mode
|
|
try:
|
|
self.cluster_env.set(_MODE_TO_CLUSTER_ENV.get(mode, "dev"))
|
|
except Exception:
|
|
pass
|
|
self._highlight_welcome_card(mode)
|
|
try:
|
|
if self._splash_status_item is not None:
|
|
self.bg_canvas.itemconfig(
|
|
self._splash_status_item,
|
|
text=_MODE_NEEDS.get(mode, ""),
|
|
)
|
|
except Exception:
|
|
pass
|
|
self.update_footer()
|
|
|
|
def _highlight_welcome_card(self, selected_mode: str) -> None:
|
|
for m, rect_id in getattr(self, "_welcome_card_rects", {}).items():
|
|
if m == selected_mode:
|
|
color = self._MODE_COLORS.get(m, "#4A90D9")
|
|
self.bg_canvas.itemconfig(rect_id, outline=color, width=3)
|
|
else:
|
|
self.bg_canvas.itemconfig(rect_id, outline="#CCCCCC", width=2)
|
|
|
|
def _start_welcome_dependency_scan(self):
|
|
self.splash_scan_running = True
|
|
self.splash_scan_done_at = None
|
|
self.splash_scan_started_at = time.time()
|
|
|
|
deps = list(self.dependencies)
|
|
total = len(deps)
|
|
results = {}
|
|
|
|
def set_status(text: str):
|
|
try:
|
|
if self._splash_status_item is not None:
|
|
# Replace existing text to avoid creating many items
|
|
self.bg_canvas.itemconfig(self._splash_status_item, text=text)
|
|
except Exception:
|
|
pass
|
|
|
|
def worker():
|
|
missing_names = []
|
|
for idx, dep in enumerate(deps, start=1):
|
|
try:
|
|
ok, location, version = self.get_dep_info(dep)
|
|
except Exception:
|
|
ok, location, version = False, None, None
|
|
results[dep["id"]] = (ok, location, version)
|
|
# Update status text progressively
|
|
if ok:
|
|
txt = f"[{idx}/{total}] {dep['name']}: Installed"
|
|
else:
|
|
txt = f"[{idx}/{total}] {dep['name']}: Not installed"
|
|
missing_names.append(dep["name"])
|
|
self.safe_after(lambda s=txt: set_status(s))
|
|
# tiny sleep to keep UI responsive without being too fast
|
|
try:
|
|
time.sleep(0.02)
|
|
except Exception:
|
|
pass
|
|
|
|
# Final message
|
|
final_msg = (
|
|
"All dependencies installed."
|
|
if not missing_names
|
|
else ("Preparing to install ... " + ", ".join(missing_names))
|
|
)
|
|
|
|
def on_done():
|
|
set_status(final_msg)
|
|
self.splash_scan_running = False
|
|
self.splash_scan_done_at = time.time()
|
|
# Re-evaluate footer to potentially show Next
|
|
self.update_footer()
|
|
# Important: schedule a follow-up refresh slightly after the debounce window
|
|
# so the Next button becomes visible without requiring further UI events.
|
|
self.safe_after(self.update_footer, delay=600)
|
|
|
|
self.safe_after(on_done)
|
|
|
|
t = threading.Thread(target=worker, daemon=True)
|
|
t.start()
|
|
# Failsafe refresh slightly after the 8s gating window in case no UI events fire.
|
|
self.safe_after(self.update_footer, delay=9000)
|
|
|
|
def _create_page_welcome(self):
|
|
f = self._page_container()
|
|
ttk.Label(f, text="Welcome to Knoe.DB", style="Title.TLabel").pack(
|
|
anchor="w", padx=24, pady=(24, 6)
|
|
)
|
|
msg = "Thanks for joining Knoe. We will prepare your system and install the software needed to build and run Knoe."
|
|
ttk.Label(
|
|
f, text=msg, style="Body.TLabel", wraplength=800, justify="left"
|
|
).pack(anchor="w", padx=24)
|
|
# self._register_page('welcome', f)
|