mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
Phase 0: test pipeline foundation — pyproject.toml, IntelliJ run configs, coverage fix, welcome mode selector
Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
parent
22906ee656
commit
03d89eaa53
24
.idea/runConfigurations/pytest_all.xml
Normal file
24
.idea/runConfigurations/pytest_all.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="pytest — all (with coverage)" type="PythonConfigurationType" factoryName="Python">
|
||||||
|
<module name="knoe-db" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONPATH" value="$PROJECT_DIR$" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="SCRIPT_NAME" value="-m pytest" />
|
||||||
|
<option name="PARAMETERS" value="--cov=knoe --cov-report=html --cov-report=term-missing tests/" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="true" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
24
.idea/runConfigurations/pytest_gke.xml
Normal file
24
.idea/runConfigurations/pytest_gke.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="pytest — gke mode" type="PythonConfigurationType" factoryName="Python">
|
||||||
|
<module name="knoe-db" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONPATH" value="$PROJECT_DIR$" />
|
||||||
|
<env name="KNOE_MODE" value="gke" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<option name="SCRIPT_NAME" value="-m pytest" />
|
||||||
|
<option name="PARAMETERS" value="-m gke --cov=knoe --cov-report=term-missing -v tests/" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="true" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
24
.idea/runConfigurations/pytest_k3d.xml
Normal file
24
.idea/runConfigurations/pytest_k3d.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="pytest — k3d mode" type="PythonConfigurationType" factoryName="Python">
|
||||||
|
<module name="knoe-db" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONPATH" value="$PROJECT_DIR$" />
|
||||||
|
<env name="KNOE_MODE" value="k3d" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<option name="SCRIPT_NAME" value="-m pytest" />
|
||||||
|
<option name="PARAMETERS" value="-m k3d --cov=knoe --cov-report=term-missing -v tests/" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="true" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
24
.idea/runConfigurations/pytest_min.xml
Normal file
24
.idea/runConfigurations/pytest_min.xml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="pytest — min mode" type="PythonConfigurationType" factoryName="Python">
|
||||||
|
<module name="knoe-db" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONPATH" value="$PROJECT_DIR$" />
|
||||||
|
<env name="KNOE_MODE" value="min" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<option name="SCRIPT_NAME" value="-m pytest" />
|
||||||
|
<option name="PARAMETERS" value="-m min --cov=knoe --cov-report=term-missing -v tests/" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="true" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
23
.idea/runConfigurations/pytest_unit.xml
Normal file
23
.idea/runConfigurations/pytest_unit.xml
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="pytest — unit (fast, no deps)" type="PythonConfigurationType" factoryName="Python">
|
||||||
|
<module name="knoe-db" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONPATH" value="$PROJECT_DIR$" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||||
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<option name="SCRIPT_NAME" value="-m pytest" />
|
||||||
|
<option name="PARAMETERS" value="-m "unit and not integration" --cov=knoe --cov-report=term-missing -v tests/" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="true" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
@ -390,6 +390,8 @@ class KnoeInstaller(
|
|||||||
)
|
)
|
||||||
self.project_root = PROJECT_ROOT
|
self.project_root = PROJECT_ROOT
|
||||||
self.cfg_path = resolved_cfg
|
self.cfg_path = resolved_cfg
|
||||||
|
from knoe.core.env import DEFAULT_ACTION_FLAGS
|
||||||
|
self._action_flags: dict[str, bool] = DEFAULT_ACTION_FLAGS.copy()
|
||||||
self._init_shared_state()
|
self._init_shared_state()
|
||||||
self._init_database_options_state()
|
self._init_database_options_state()
|
||||||
self.screens = None
|
self.screens = None
|
||||||
@ -525,7 +527,7 @@ class KnoeInstaller(
|
|||||||
]
|
]
|
||||||
self.nav_widgets = {}
|
self.nav_widgets = {}
|
||||||
self._mode_tab_widgets: dict = {}
|
self._mode_tab_widgets: dict = {}
|
||||||
self.deployment_mode = tk.StringVar(value=__import__("os").environ.get("KNOE_MODE", "k3s"))
|
self.deployment_mode = tk.StringVar(value=__import__("os").environ.get("KNOE_MODE", ""))
|
||||||
self._create_sidebar_nav()
|
self._create_sidebar_nav()
|
||||||
|
|
||||||
# Validation attributes
|
# Validation attributes
|
||||||
|
|||||||
@ -262,44 +262,23 @@ class NavigationMixin:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
_MODE_COLORS = {"k3d": "#4A90D9", "k3s": "#27AE60", "k8s": "#E67E22"}
|
_MODE_COLORS = {"min": "#8E44AD", "k3d": "#4A90D9", "k3s": "#27AE60", "gke": "#E67E22"}
|
||||||
|
|
||||||
def _set_deployment_mode(self, mode: str) -> None:
|
def _set_deployment_mode(self, mode: str) -> None:
|
||||||
"""Switch the active deployment mode and persist it to the environment."""
|
"""Switch the active deployment mode and persist it to the environment."""
|
||||||
self.deployment_mode.set(mode)
|
self.deployment_mode.set(mode)
|
||||||
os.environ["KNOE_MODE"] = mode
|
os.environ["KNOE_MODE"] = mode
|
||||||
self._refresh_mode_tabs()
|
|
||||||
|
|
||||||
def _refresh_mode_tabs(self) -> None:
|
def _refresh_mode_tabs(self) -> None:
|
||||||
"""Update tab highlight colours to reflect the active deployment mode."""
|
"""No-op: mode tabs replaced by welcome screen card selector."""
|
||||||
active = self.deployment_mode.get()
|
pass
|
||||||
for value, widget in self._mode_tab_widgets.items():
|
|
||||||
if value == active:
|
def _is_min_mode(self) -> bool:
|
||||||
widget.configure(bg=self._MODE_COLORS.get(value, "#888"), fg="white")
|
"""Return True when the user has selected minimal (containerd-only) mode."""
|
||||||
else:
|
return self.deployment_mode.get() == "min"
|
||||||
widget.configure(bg="#D5D5C5", fg="#555")
|
|
||||||
|
|
||||||
def _create_sidebar_nav(self):
|
def _create_sidebar_nav(self):
|
||||||
"""Create the left-hand navigation menu."""
|
"""Create the left-hand navigation menu."""
|
||||||
# ── Deployment mode tab strip ────────────────────────────────────
|
|
||||||
mode_frame = tk.Frame(self.sidebar, bg="#F5F5DC")
|
|
||||||
mode_frame.pack(fill="x", padx=12, pady=(14, 4))
|
|
||||||
for label, value in [("k3d", "k3d"), ("k3s", "k3s"), ("k8s", "k8s")]:
|
|
||||||
btn = tk.Label(
|
|
||||||
mode_frame,
|
|
||||||
text=label,
|
|
||||||
bg="#D5D5C5",
|
|
||||||
fg="#555",
|
|
||||||
font=("SF Pro Text", 9, "bold"),
|
|
||||||
padx=8,
|
|
||||||
pady=3,
|
|
||||||
cursor="hand2",
|
|
||||||
)
|
|
||||||
btn.pack(side="left", padx=2)
|
|
||||||
btn.bind("<Button-1>", lambda e, v=value: self._set_deployment_mode(v))
|
|
||||||
self._mode_tab_widgets[value] = btn
|
|
||||||
self._refresh_mode_tabs()
|
|
||||||
|
|
||||||
# ── INSTALLER label ──────────────────────────────────────────────
|
# ── INSTALLER label ──────────────────────────────────────────────
|
||||||
tk.Label(
|
tk.Label(
|
||||||
self.sidebar,
|
self.sidebar,
|
||||||
@ -375,7 +354,10 @@ class NavigationMixin:
|
|||||||
self.show_page(seq[-1] if seq else "deps_summary")
|
self.show_page(seq[-1] if seq else "deps_summary")
|
||||||
return
|
return
|
||||||
if current_id == "env_setup":
|
if current_id == "env_setup":
|
||||||
self.show_page("network_scan")
|
if self._is_min_mode():
|
||||||
|
self.show_page("deps_summary")
|
||||||
|
else:
|
||||||
|
self.show_page("network_scan")
|
||||||
return
|
return
|
||||||
if current_id == "init_cluster":
|
if current_id == "init_cluster":
|
||||||
if getattr(self, "_showing_service_overlay", False):
|
if getattr(self, "_showing_service_overlay", False):
|
||||||
@ -397,7 +379,10 @@ class NavigationMixin:
|
|||||||
self.show_page("common_services")
|
self.show_page("common_services")
|
||||||
return
|
return
|
||||||
if current_id == "init_password":
|
if current_id == "init_password":
|
||||||
self.show_page("init_db_build")
|
if self._is_min_mode():
|
||||||
|
self.show_page("env_setup")
|
||||||
|
else:
|
||||||
|
self.show_page("init_db_build")
|
||||||
return
|
return
|
||||||
if current_id == "init_db_build":
|
if current_id == "init_db_build":
|
||||||
self.show_page("database_options")
|
self.show_page("database_options")
|
||||||
@ -426,6 +411,10 @@ class NavigationMixin:
|
|||||||
if current_id == "init_cnpg_deploy":
|
if current_id == "init_cnpg_deploy":
|
||||||
self.show_page("supabase_config")
|
self.show_page("supabase_config")
|
||||||
return
|
return
|
||||||
|
if current_id == "security":
|
||||||
|
if self._is_min_mode():
|
||||||
|
self.show_page("init_scripts")
|
||||||
|
return
|
||||||
if current_id == "create_installer":
|
if current_id == "create_installer":
|
||||||
self.show_page("init_cnpg_deploy")
|
self.show_page("init_cnpg_deploy")
|
||||||
return
|
return
|
||||||
@ -492,6 +481,9 @@ class NavigationMixin:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if current_id == "network_scan":
|
if current_id == "network_scan":
|
||||||
|
if self._is_min_mode():
|
||||||
|
self.show_page("env_setup")
|
||||||
|
return
|
||||||
# Capture network scan info
|
# Capture network scan info
|
||||||
try:
|
try:
|
||||||
self.knoe_cfg_data["Network"][
|
self.knoe_cfg_data["Network"][
|
||||||
@ -593,7 +585,10 @@ class NavigationMixin:
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return
|
return
|
||||||
self.show_page("init_cluster")
|
if self._is_min_mode():
|
||||||
|
self.show_page("init_password")
|
||||||
|
else:
|
||||||
|
self.show_page("init_cluster")
|
||||||
return
|
return
|
||||||
|
|
||||||
if current_id == "init_password":
|
if current_id == "init_password":
|
||||||
@ -678,7 +673,9 @@ class NavigationMixin:
|
|||||||
"Completed" if getattr(self, "_scripts_success", False) else "Attempted"
|
"Completed" if getattr(self, "_scripts_success", False) else "Attempted"
|
||||||
)
|
)
|
||||||
self._save_knoe_cfg()
|
self._save_knoe_cfg()
|
||||||
if self.kerberos_enabled.get():
|
if self._is_min_mode():
|
||||||
|
self.show_page("security")
|
||||||
|
elif self.kerberos_enabled.get():
|
||||||
self.show_page("kerberos_config")
|
self.show_page("kerberos_config")
|
||||||
else:
|
else:
|
||||||
self.show_page("argocd_config")
|
self.show_page("argocd_config")
|
||||||
@ -885,7 +882,12 @@ class NavigationMixin:
|
|||||||
|
|
||||||
# Page-specific adjustments
|
# Page-specific adjustments
|
||||||
pid = self.pages[self.page_index][0]
|
pid = self.pages[self.page_index][0]
|
||||||
if pid == "build":
|
if pid == "welcome":
|
||||||
|
if not self.deployment_mode.get():
|
||||||
|
self.next_button.configure(state="disabled")
|
||||||
|
else:
|
||||||
|
self.next_button.configure(state="normal")
|
||||||
|
elif pid == "build":
|
||||||
# Build page: show Build or Next depending on state
|
# Build page: show Build or Next depending on state
|
||||||
if getattr(self, "_built_success", False):
|
if getattr(self, "_built_success", False):
|
||||||
self.next_button.configure(text="Next")
|
self.next_button.configure(text="Next")
|
||||||
|
|||||||
@ -1,68 +1,198 @@
|
|||||||
"""Welcome / splash screen."""
|
"""Welcome / splash screen."""
|
||||||
|
|
||||||
|
import os
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, messagebox, filedialog
|
from tkinter import ttk, messagebox, filedialog
|
||||||
from knoe import screen as ui
|
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:
|
class WelcomeScreenMixin:
|
||||||
"""Welcome / splash screen."""
|
"""Welcome / splash screen."""
|
||||||
|
|
||||||
def _render_welcome_page(self):
|
def _render_welcome_page(self):
|
||||||
# Letterhead at top right
|
content_width = self.bg_canvas.winfo_width() or 975
|
||||||
content_width = self.bg_canvas.winfo_width() or 975 # 1300 * 0.75 approx
|
|
||||||
right_margin = content_width - 48
|
right_margin = content_width - 48
|
||||||
|
|
||||||
ui.canvas_text(
|
# Letterhead
|
||||||
self,
|
ui.canvas_text(self, right_margin, 40, "knoe.dev",
|
||||||
right_margin,
|
fill="#6e6e73", font=("SF Pro Text", 32, "bold"), anchor="ne")
|
||||||
40,
|
ui.canvas_text(self, right_margin, 85, "infrastructure.auto()",
|
||||||
"knoe.dev",
|
fill="#6e6e73", font=("SF Pro Text", 18), anchor="ne")
|
||||||
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",
|
|
||||||
)
|
|
||||||
|
|
||||||
# Welcome title
|
self._render_title("Welcome to Knoe.DB", y=145)
|
||||||
self._render_title("Welcome", y=150)
|
|
||||||
|
|
||||||
welcome_text = (
|
intro = (
|
||||||
"This installer will guide you through the process of setting up the Knoe Database and its supporting "
|
"Knoe.DB Installer sets up a production-grade PostgreSQL cluster with Kerberos "
|
||||||
"infrastructure. We have designed this process to be as automated as possible, ensuring that your "
|
"authentication.\nPick the mode that matches your hardware — we'll walk you through every step."
|
||||||
"deployment is secure, efficient, and tailored to your specific network environment.\n\n"
|
|
||||||
"What to expect:\n"
|
|
||||||
"• Network Environment Discovery: We'll scan for existing services like Active Directory and DNS.\n"
|
|
||||||
"• System Configuration: Setting up local paths and environment variables.\n"
|
|
||||||
"• Dependency Management: Ensuring all required tools (Docker, k3d, etc.) are ready.\n"
|
|
||||||
"• Database Initialization: Configuring passwords, Kerberos authentication, and deploying the database cluster.\n\n"
|
|
||||||
"We are excited to have you join our community and start building with us. "
|
|
||||||
"Welcome to the neighborhood! Let's get started by preparing your system for the Knoe experience."
|
|
||||||
)
|
)
|
||||||
|
ui.canvas_text(self, 48, 200, intro,
|
||||||
|
fill="#1d1d1f", font=("SF Pro Text", 13), width=800)
|
||||||
|
|
||||||
ui.canvas_text(
|
# Mode selector cards
|
||||||
self,
|
card_w = 185
|
||||||
48,
|
card_h = 200
|
||||||
220,
|
card_gap = 18
|
||||||
welcome_text,
|
card_x0 = 48
|
||||||
fill="black",
|
card_y0 = 265
|
||||||
font=("SF Pro Text", 13),
|
|
||||||
width=750,
|
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
|
||||||
|
|
||||||
# Ensure footer is updated (Next button visible)
|
|
||||||
self.update_footer()
|
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):
|
def _start_welcome_dependency_scan(self):
|
||||||
self.splash_scan_running = True
|
self.splash_scan_running = True
|
||||||
self.splash_scan_done_at = None
|
self.splash_scan_done_at = None
|
||||||
|
|||||||
36
pyproject.toml
Normal file
36
pyproject.toml
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
python_files = ["test_*.py"]
|
||||||
|
python_classes = ["Test*"]
|
||||||
|
python_functions = ["test_*"]
|
||||||
|
addopts = "-ra --tb=short"
|
||||||
|
|
||||||
|
markers = [
|
||||||
|
"unit: pure unit test — no external services, no cluster, runs anywhere",
|
||||||
|
"integration: requires a running service, cluster, or network dependency",
|
||||||
|
"min: tests the min (containerd) deployment path",
|
||||||
|
"k3d: tests the k3d (local Docker cluster) deployment path",
|
||||||
|
"k3s: tests the k3s (homelab) deployment path",
|
||||||
|
"gke: tests the GKE (production) deployment path",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.run]
|
||||||
|
source = ["knoe"]
|
||||||
|
branch = true
|
||||||
|
omit = [
|
||||||
|
"tests/*",
|
||||||
|
"*/__init__.py",
|
||||||
|
"knoe/ui/*", # UI layer tested via integration only
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.coverage.report]
|
||||||
|
show_missing = true
|
||||||
|
skip_covered = false
|
||||||
|
# Raised incrementally as each pipeline phase lands — see docs/pipeline-phases.md Appendix C
|
||||||
|
fail_under = 0
|
||||||
|
|
||||||
|
[tool.coverage.xml]
|
||||||
|
output = "coverage.xml"
|
||||||
|
|
||||||
|
[tool.coverage.html]
|
||||||
|
directory = "htmlcov"
|
||||||
8
requirements-test.txt
Normal file
8
requirements-test.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Test-only dependencies — install alongside requirements.txt in CI and local dev
|
||||||
|
# pip install -r requirements.txt -r requirements-test.txt
|
||||||
|
|
||||||
|
pytest>=7.4
|
||||||
|
pytest-cov>=4.1
|
||||||
|
pytest-mock>=3.11
|
||||||
|
coverage[toml]>=7.3
|
||||||
|
vulture>=2.10
|
||||||
@ -1,17 +1,25 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Run all unit tests and generate a coverage report
|
# Run all unit tests and generate a coverage report.
|
||||||
|
# Coverage config is in pyproject.toml [tool.coverage.*].
|
||||||
|
|
||||||
export PYTHONPATH=$PYTHONPATH:.
|
set -euo pipefail
|
||||||
|
|
||||||
# Check if pytest-cov is installed
|
export PYTHONPATH="${PYTHONPATH:+$PYTHONPATH:}."
|
||||||
if pytest --trace-config | grep -q "pytest_cov"; then
|
|
||||||
|
if python -c "import pytest_cov" 2>/dev/null; then
|
||||||
echo "Running tests with coverage..."
|
echo "Running tests with coverage..."
|
||||||
pytest --cov=knoe --cov=install --cov-report=term-missing --cov-report=html tests/
|
pytest \
|
||||||
|
--cov=knoe \
|
||||||
|
--cov-report=term-missing \
|
||||||
|
--cov-report=html \
|
||||||
|
--cov-report=xml \
|
||||||
|
tests/
|
||||||
RET=$?
|
RET=$?
|
||||||
echo "Coverage report (HTML) generated in htmlcov/index.html"
|
echo "HTML coverage report: htmlcov/index.html"
|
||||||
|
echo "XML coverage report: coverage.xml"
|
||||||
exit $RET
|
exit $RET
|
||||||
else
|
else
|
||||||
echo "Warning: pytest-cov not found. Running tests without coverage."
|
echo "Warning: pytest-cov not found. Running tests without coverage."
|
||||||
echo "To enable coverage, install it via: pip install pytest-cov"
|
echo "Install it with: pip install -r requirements-test.txt"
|
||||||
pytest tests/
|
pytest tests/
|
||||||
fi
|
fi
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user