Update Healer Man server

This commit is contained in:
phenom committed 2026-09-26 17:51:56 -04:00
1 parent 9c5fb4c614
commit bd073fb00f
113 files changed
+4902 -273

No files matched your search

+245 -115
View File
@@ -555,13 +555,13 @@ def launch_gui(*, smoke_test: bool = False) -> int:
from tkinter import filedialog, messagebox, scrolledtext, ttk
class ReleaseManagerApp:
BG = "#0c1515"
PANEL = "#122222"
PANEL_2 = "#172b2a"
TEXT = "#e9f4ef"
MUTED = "#91aaa2"
ACCENT = "#63d9a4"
ACCENT_DARK = "#183b31"
BG = "#0b1220"
PANEL = "#172337"
PANEL_2 = "#22344d"
TEXT = "#f2f6fc"
MUTED = "#aabbd0"
ACCENT = "#63dfb5"
ACCENT_DARK = "#214a47"
WARNING = "#f2c66d"
ERROR = "#ff8b83"
@@ -580,63 +580,66 @@ def launch_gui(*, smoke_test: bool = False) -> int:
def _configure_window(self) -> None:
self.root.title("Healer Man Release Manager")
self.root.geometry("1120x820")
self.root.minsize(960, 700)
width = min(1100, max(700, self.root.winfo_screenwidth() - 60))
height = min(740, max(500, self.root.winfo_screenheight() - 100))
self.root.geometry(f"{width}x{height}")
self.root.minsize(700, 500)
self.root.configure(bg=self.BG)
style = ttk.Style(self.root)
try:
style.theme_use("clam")
except tk.TclError:
pass
style.configure(".", background=self.BG, foreground=self.TEXT)
style.configure(".", background=self.BG, foreground=self.TEXT, font=("Segoe UI", 10))
style.configure("TFrame", background=self.BG)
style.configure("Panel.TFrame", background=self.PANEL)
style.configure(
"Title.TLabel",
background=self.BG,
foreground=self.TEXT,
font=("Segoe UI Semibold", 20),
)
style.configure(
"Sub.TLabel",
background=self.BG,
foreground=self.MUTED,
font=("Segoe UI", 10),
)
style.configure(
"Panel.TLabel", background=self.PANEL, foreground=self.TEXT
)
style.configure(
"Muted.Panel.TLabel", background=self.PANEL, foreground=self.MUTED
)
style.configure(
"Section.Panel.TLabel",
background=self.PANEL,
foreground=self.TEXT,
font=("Segoe UI Semibold", 13),
)
style.configure(
"Accent.TButton",
background=self.ACCENT,
foreground="#06110d",
foreground="#082019",
font=("Segoe UI Semibold", 10),
padding=(13, 9),
padding=(16, 11),
)
style.map("Accent.TButton", background=[("active", "#84e7bb")])
style.configure("TButton", padding=(10, 7))
style.map("Accent.TButton", background=[("active", "#8af1cc"), ("disabled", self.PANEL_2)])
style.configure("TButton", background=self.PANEL_2, foreground=self.TEXT, padding=(12, 8))
style.map("TButton", background=[("active", "#314866")], foreground=[("disabled", self.MUTED)])
style.configure("Panel.TCheckbutton", background=self.PANEL, foreground=self.TEXT)
style.map("Panel.TCheckbutton", background=[("active", self.PANEL)])
style.configure("Panel.TRadiobutton", background=self.PANEL, foreground=self.TEXT)
style.map("Panel.TRadiobutton", background=[("active", self.PANEL)])
style.configure(
"TEntry",
fieldbackground="#0a1716",
fieldbackground="#101b2b",
foreground=self.TEXT,
insertcolor=self.TEXT,
bordercolor="#31504a",
bordercolor="#3b5068",
padding=6,
)
style.configure(
"TCombobox",
fieldbackground="#0a1716",
fieldbackground="#101b2b",
foreground=self.TEXT,
arrowcolor=self.TEXT,
)
style.configure("Horizontal.TProgressbar", background=self.ACCENT, troughcolor=self.PANEL_2)
style.configure("TNotebook", background=self.PANEL, borderwidth=0)
style.configure(
"TNotebook.Tab",
background=self.PANEL_2,
foreground=self.MUTED,
padding=(12, 7),
padding=(17, 9),
)
style.map(
"TNotebook.Tab",
@@ -645,33 +648,81 @@ def launch_gui(*, smoke_test: bool = False) -> int:
)
def _build_ui(self) -> None:
shell = ttk.Frame(self.root, padding=18)
shell = ttk.Frame(self.root, padding=10)
shell.pack(fill="both", expand=True)
shell.columnconfigure(0, weight=3)
shell.columnconfigure(1, weight=2)
shell.columnconfigure(0, weight=1)
shell.rowconfigure(2, weight=1)
title = ttk.Frame(shell)
title.grid(row=0, column=0, columnspan=2, sticky="ew", pady=(0, 14))
title.columnconfigure(0, weight=1)
ttk.Label(title, text="Healer Man Release Manager", style="Title.TLabel").grid(
row=0, column=0, sticky="w"
mode_panel = ttk.Frame(shell, style="Panel.TFrame", padding=(10, 6))
mode_panel.grid(row=0, column=0, sticky="ew", pady=(0, 8))
mode_panel.columnconfigure(3, weight=1)
ttk.Label(mode_panel, text="Mode", style="Muted.Panel.TLabel").grid(
row=0, column=0, sticky="w", padx=(0, 12)
)
ttk.Label(
title,
text="Validate → inspect → commit → push → deploy, with optional signed APK",
style="Sub.TLabel",
).grid(row=1, column=0, sticky="w", pady=(2, 0))
self.refresh_button = ttk.Button(title, text="Refresh", command=self.refresh)
self.refresh_button.grid(row=0, column=1, rowspan=2, sticky="e")
self.git_truenas_only_var = tk.BooleanVar(value=DEFAULT_GIT_TRUENAS_ONLY)
self.release_mode_toggle = ttk.Radiobutton(
mode_panel,
text="Git + TrueNAS",
variable=self.git_truenas_only_var,
value=True,
command=self._update_release_mode,
style="Panel.TRadiobutton",
)
self.release_mode_toggle.grid(row=0, column=1, sticky="w", padx=(0, 18))
self.apk_mode_toggle = ttk.Radiobutton(
mode_panel,
text="Include signed APK",
variable=self.git_truenas_only_var,
value=False,
command=self._update_release_mode,
style="Panel.TRadiobutton",
)
self.apk_mode_toggle.grid(row=0, column=2, sticky="w")
self.refresh_button = ttk.Button(mode_panel, text="Refresh", command=self.refresh)
self.refresh_button.grid(row=0, column=4, sticky="e")
self.git_summary_var = tk.StringVar(value="Loading Git status…")
release_panel = ttk.Frame(shell, style="Panel.TFrame", padding=15)
release_panel.grid(row=1, column=0, sticky="nsew", padx=(0, 8))
setup_host = ttk.Frame(shell)
setup_host.grid(row=1, column=0, sticky="nsew")
setup_host.columnconfigure(0, weight=1)
setup_host.rowconfigure(0, weight=1)
self.setup_canvas = tk.Canvas(
setup_host, bg=self.BG, highlightthickness=0, borderwidth=0, height=220
)
self.setup_canvas.grid(row=0, column=0, sticky="nsew")
setup_scrollbar = ttk.Scrollbar(
setup_host, orient="vertical", command=self.setup_canvas.yview
)
setup_scrollbar.grid(row=0, column=1, sticky="ns", padx=(4, 0))
self.setup_canvas.configure(yscrollcommand=setup_scrollbar.set)
self.setup_content = ttk.Frame(self.setup_canvas)
self.setup_content.columnconfigure(0, weight=3)
self.setup_content.columnconfigure(1, weight=2)
self.setup_window = self.setup_canvas.create_window(
(0, 0), window=self.setup_content, anchor="nw"
)
self.setup_canvas.bind(
"<Configure>",
lambda event: self.setup_canvas.itemconfigure(self.setup_window, width=event.width),
)
self.setup_content.bind(
"<Configure>",
lambda _event: self.setup_canvas.configure(
scrollregion=self.setup_canvas.bbox("all")
),
)
self.setup_canvas.bind(
"<MouseWheel>",
self._scroll_setup,
)
release_panel = ttk.Frame(self.setup_content, style="Panel.TFrame", padding=12)
release_panel.grid(row=0, column=0, sticky="nsew")
self.release_panel = release_panel
release_panel.columnconfigure(1, weight=1)
ttk.Label(
release_panel, text="Release details", style="Panel.TLabel",
font=("Segoe UI Semibold", 12),
).grid(row=0, column=0, columnspan=4, sticky="w", pady=(0, 11))
release_panel, text="Release details", style="Section.Panel.TLabel",
).grid(row=0, column=0, columnspan=4, sticky="w", pady=(0, 8))
self.current_version_var = tk.StringVar(value="—")
self.current_code_var = tk.StringVar(value="—")
@@ -679,11 +730,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.next_version_var = tk.StringVar()
self.next_code_var = tk.StringVar()
self.commit_message_var = tk.StringVar()
self.git_truenas_only_var = tk.BooleanVar(
value=DEFAULT_GIT_TRUENAS_ONLY
)
self.run_tests_var = tk.BooleanVar(value=DEFAULT_RUN_TESTS)
self.release_mode_hint_var = tk.StringVar()
self.test_mode_hint_var = tk.StringVar()
ttk.Label(release_panel, text="Current", style="Muted.Panel.TLabel").grid(
@@ -700,7 +747,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
)
ttk.Label(release_panel, text="Bump", style="Muted.Panel.TLabel").grid(
row=2, column=0, sticky="w", pady=(12, 0)
row=2, column=0, sticky="w", pady=(7, 0)
)
self.bump_box = ttk.Combobox(
release_panel,
@@ -709,7 +756,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
state="readonly",
width=10,
)
self.bump_box.grid(row=2, column=1, sticky="w", padx=(8, 20), pady=(12, 0))
self.bump_box.grid(row=2, column=1, sticky="w", padx=(8, 20), pady=(7, 0))
self.bump_box.bind("<<ComboboxSelected>>", lambda _event: self._suggest_versions())
ttk.Label(release_panel, text="Next version", style="Muted.Panel.TLabel").grid(
row=3, column=0, sticky="w", pady=(9, 0)
@@ -740,9 +787,9 @@ def launch_gui(*, smoke_test: bool = False) -> int:
)
self.release_notes = tk.Text(
release_panel,
height=4,
height=3,
wrap="word",
bg="#0a1716",
bg="#101b2b",
fg=self.TEXT,
insertbackground=self.TEXT,
relief="flat",
@@ -753,48 +800,40 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.release_notes.grid(
row=5, column=1, columnspan=3, sticky="nsew", padx=(8, 0), pady=(9, 0)
)
self.release_mode_toggle = ttk.Checkbutton(
release_panel,
text="Git + TrueNAS only (skip Android version bump and APK publication)",
variable=self.git_truenas_only_var,
command=self._update_release_mode,
)
self.release_mode_toggle.grid(
row=6, column=0, columnspan=4, sticky="w", pady=(12, 0)
)
ttk.Label(
release_panel,
textvariable=self.release_mode_hint_var,
style="Muted.Panel.TLabel",
wraplength=610,
).grid(row=7, column=0, columnspan=4, sticky="w", pady=(4, 0))
self.run_tests_toggle = ttk.Checkbutton(
release_panel,
text="Run test suites (recommended)",
text="Run all test suites before release",
variable=self.run_tests_var,
command=self._update_test_mode,
style="Panel.TCheckbutton",
)
self.run_tests_toggle.grid(
row=8, column=0, columnspan=4, sticky="w", pady=(10, 0)
row=6, column=0, columnspan=4, sticky="w", pady=(10, 0)
)
ttk.Label(
release_panel,
textvariable=self.test_mode_hint_var,
style="Muted.Panel.TLabel",
wraplength=610,
).grid(row=9, column=0, columnspan=4, sticky="w", pady=(4, 0))
wraplength=620,
).grid(row=7, column=0, columnspan=4, sticky="w", pady=(4, 0))
self.apk_detail_widgets = tuple(
widget
for row in (2, 3, 5)
for widget in release_panel.grid_slaves(row=row)
)
secrets_panel = ttk.Frame(shell, style="Panel.TFrame", padding=15)
secrets_panel.grid(row=1, column=1, sticky="nsew", padx=(8, 0))
secrets_panel = ttk.Frame(self.setup_content, style="Panel.TFrame", padding=12)
secrets_panel.grid(row=0, column=1, sticky="nsew", padx=(8, 0))
self.secrets_panel = secrets_panel
secrets_panel.columnconfigure(1, weight=1)
ttk.Label(
secrets_panel, text="Signing and Gitea (APK only)", style="Panel.TLabel",
font=("Segoe UI Semibold", 12),
secrets_panel, text="Signing and Gitea", style="Section.Panel.TLabel",
).grid(row=0, column=0, columnspan=3, sticky="w")
ttk.Label(
secrets_panel,
text="Signing password and saved token are Windows-encrypted outside Git.",
style="Muted.Panel.TLabel",
wraplength=260,
).grid(row=1, column=0, columnspan=3, sticky="w", pady=(2, 10))
self.keystore_var = tk.StringVar(value=str(DEFAULT_KEYSTORE_FILE))
@@ -829,12 +868,13 @@ def launch_gui(*, smoke_test: bool = False) -> int:
secrets_panel,
text="Remember Gitea token on this PC",
variable=self.remember_token_var,
style="Panel.TCheckbutton",
).grid(row=7, column=0, columnspan=2, sticky="w", pady=(7, 0))
ttk.Button(
secrets_panel,
text="Forget saved token",
command=self._forget_saved_token,
).grid(row=7, column=2, sticky="e", pady=(7, 0))
).grid(row=8, column=0, columnspan=3, sticky="w", pady=(5, 0))
ttk.Label(
secrets_panel,
text=(
@@ -842,16 +882,17 @@ def launch_gui(*, smoke_test: bool = False) -> int:
"enter the Gitea token once and it will load automatically."
),
style="Muted.Panel.TLabel",
wraplength=360,
).grid(row=8, column=0, columnspan=3, sticky="w", pady=(7, 0))
wraplength=260,
).grid(row=9, column=0, columnspan=3, sticky="w", pady=(7, 0))
lower = ttk.Panedwindow(shell, orient="horizontal")
lower.grid(row=2, column=0, columnspan=2, sticky="nsew", pady=(16, 0))
workspace = ttk.Notebook(shell)
workspace.grid(row=2, column=0, sticky="nsew", pady=(8, 0))
self.workspace_tabs = workspace
log_panel = ttk.Frame(lower, style="Panel.TFrame", padding=12)
commands_panel = ttk.Frame(lower, style="Panel.TFrame", padding=12)
lower.add(log_panel, weight=3)
lower.add(commands_panel, weight=2)
log_panel = ttk.Frame(workspace, style="Panel.TFrame", padding=14)
commands_panel = ttk.Frame(workspace, style="Panel.TFrame", padding=14)
workspace.add(log_panel, text="Activity")
workspace.add(commands_panel, text="TrueNAS commands")
log_panel.rowconfigure(2, weight=1)
log_panel.columnconfigure(0, weight=1)
commands_panel.rowconfigure(1, weight=1)
@@ -867,28 +908,27 @@ def launch_gui(*, smoke_test: bool = False) -> int:
).grid(row=0, column=0, sticky="w")
self.progress = ttk.Progressbar(status_row, mode="indeterminate", length=150)
self.progress.grid(row=0, column=1, sticky="e")
self.git_summary_var = tk.StringVar(value="Loading Git status…")
ttk.Label(
log_panel,
textvariable=self.git_summary_var,
style="Muted.Panel.TLabel",
wraplength=600,
wraplength=1060,
).grid(row=1, column=0, sticky="ew", pady=(4, 8))
self.log = scrolledtext.ScrolledText(
log_panel,
wrap="word",
bg="#071110",
fg="#cfe2dc",
bg="#101b2b",
fg=self.TEXT,
insertbackground=self.TEXT,
relief="flat",
font=("Cascadia Mono", 9),
font=("Cascadia Mono", 10),
state="disabled",
)
self.log.grid(row=2, column=0, sticky="nsew")
ttk.Label(
commands_panel,
text="TrueNAS shell commands",
text="Deploy from TrueNAS after the Git push",
style="Panel.TLabel",
font=("Segoe UI Semibold", 11),
).grid(row=0, column=0, sticky="w", pady=(0, 7))
@@ -908,7 +948,8 @@ def launch_gui(*, smoke_test: bool = False) -> int:
)
actions = ttk.Frame(shell)
actions.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(14, 0))
self.actions = actions
actions.grid(row=3, column=0, sticky="ew", pady=(8, 0))
actions.columnconfigure(0, weight=1)
self.release_button = ttk.Button(
actions,
@@ -927,25 +968,79 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.cancel_button.grid(row=0, column=2, padx=(7, 0))
self._update_release_mode()
self._update_test_mode()
self._bind_setup_scroll(self.setup_content)
self.root.bind("<Configure>", self._fit_setup_height)
self.root.after_idle(self._fit_setup_height)
def _bind_setup_scroll(self, widget: tk.Misc) -> None:
if not isinstance(widget, (tk.Text, ttk.Combobox)):
widget.bind("<MouseWheel>", self._scroll_setup, add="+")
for child in widget.winfo_children():
self._bind_setup_scroll(child)
def _scroll_setup(self, event: tk.Event[tk.Misc]) -> str:
if event.delta:
self.setup_canvas.yview_scroll(-1 if event.delta > 0 else 1, "units")
return "break"
def _fit_setup_height(self, event: tk.Event[tk.Misc] | None = None) -> None:
if event is not None and event.widget is not self.root:
return
self._layout_setup_panels()
available = max(140, int(self.root.winfo_height() * 0.43))
desired = min(self.setup_content.winfo_reqheight(), available)
if int(self.setup_canvas.cget("height")) != desired:
self.setup_canvas.configure(height=desired)
def _layout_setup_panels(self) -> None:
apk_mode = not self.git_truenas_only_var.get()
stacked = apk_mode and self.root.winfo_width() < 940
layout = (apk_mode, stacked)
if layout == getattr(self, "_setup_layout", None):
return
self._setup_layout = layout
if not apk_mode:
self.release_panel.grid_configure(
row=0, column=0, columnspan=2, padx=0, pady=0
)
self.secrets_panel.grid_remove()
elif stacked:
self.release_panel.grid_configure(
row=0, column=0, columnspan=2, padx=0, pady=0
)
self.secrets_panel.grid_configure(
row=1, column=0, columnspan=2, padx=0, pady=(8, 0)
)
else:
self.release_panel.grid_configure(
row=0, column=0, columnspan=1, padx=(0, 4), pady=0
)
self.secrets_panel.grid_configure(
row=0, column=1, columnspan=1, padx=(4, 0), pady=0
)
self.root.after_idle(self._fit_setup_height)
def _update_release_mode(self) -> None:
git_truenas_only = self.git_truenas_only_var.get()
if git_truenas_only:
self.retry_button.grid_remove()
for widget in self.apk_detail_widgets:
widget.grid_remove()
else:
self.retry_button.grid()
for widget in self.apk_detail_widgets:
widget.grid()
self._layout_setup_panels()
self.setup_canvas.yview_moveto(0)
self.root.after_idle(self._fit_setup_height)
self.bump_box.configure(state="disabled" if git_truenas_only else "readonly")
entry_state = "disabled" if git_truenas_only else "normal"
self.next_version_entry.configure(state=entry_state)
self.next_code_entry.configure(state=entry_state)
if git_truenas_only:
self.release_button.configure(text="Validate and Push Git + TrueNAS Update")
self.release_mode_hint_var.set(
"Default mode: keeps the current Android version, skips Android sync and APK "
"publication, and prepares the pinned TrueNAS deployment commands after push."
)
else:
self.release_button.configure(text="Validate, Push, and Publish APK")
self.release_mode_hint_var.set(
"APK mode: bumps the app and Android versions, runs Android sync, and publishes "
"the signed APK after the Git push."
)
self._suggest_versions()
current_notes = self.release_notes.get("1.0", "end").strip()
server_default = "Healer Man Git and TrueNAS server update."
@@ -980,15 +1075,18 @@ def launch_gui(*, smoke_test: bool = False) -> int:
frame.columnconfigure(0, weight=1)
widget = tk.Text(
frame,
wrap="none",
bg="#071110",
fg="#cfe2dc",
wrap="word",
bg="#101b2b",
fg=self.TEXT,
relief="flat",
padx=8,
pady=8,
font=("Cascadia Mono", 8),
padx=12,
pady=10,
font=("Cascadia Mono", 10),
)
widget.grid(row=0, column=0, sticky="nsew")
scrollbar = ttk.Scrollbar(frame, orient="vertical", command=widget.yview)
scrollbar.grid(row=0, column=1, sticky="ns")
widget.configure(yscrollcommand=scrollbar.set)
return widget
def _browse_keystore(self) -> None:
@@ -1265,11 +1363,13 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.busy = True
self.cancel_event.clear()
self.status_var.set(label)
self.workspace_tabs.select(0)
self.progress.start(12)
self.release_button.configure(state="disabled")
self.retry_button.configure(state="disabled")
self.refresh_button.configure(state="disabled")
self.release_mode_toggle.configure(state="disabled")
self.apk_mode_toggle.configure(state="disabled")
self.run_tests_toggle.configure(state="disabled")
self.cancel_button.configure(state="normal")
self._append_log(f"\n=== {label} ===")
@@ -1774,6 +1874,7 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.retry_button.configure(state="normal")
self.refresh_button.configure(state="normal")
self.release_mode_toggle.configure(state="normal")
self.apk_mode_toggle.configure(state="normal")
self.run_tests_toggle.configure(state="normal")
self.cancel_button.configure(state="disabled")
self.keystore_password_var.set("")
@@ -1781,21 +1882,24 @@ def launch_gui(*, smoke_test: bool = False) -> int:
if self.remember_token_var.get() and SAVED_GITEA_TOKEN_FILE.is_file():
self.token_var.set(read_saved_gitea_token())
if result == "success":
self.status_var.set("Workflow completed")
self._append_log("\n✓ Workflow completed successfully.")
if self.git_truenas_only_var.get():
self.workspace_tabs.select(1)
messagebox.showinfo(
"Workflow complete",
detail,
parent=self.root,
)
elif result == "cancelled":
self.status_var.set("Workflow cancelled")
self._append_log(f"\nCancelled: {detail}")
else:
self.status_var.set("Workflow failed")
self._append_log(f"\nERROR: {detail}")
messagebox.showerror("Workflow failed", detail, parent=self.root)
self.refresh()
self.status_var.set({
"success": "Workflow completed",
"cancelled": "Workflow cancelled",
}.get(result, "Workflow failed"))
def _close(self) -> None:
if self.busy:
@@ -1809,11 +1913,37 @@ def launch_gui(*, smoke_test: bool = False) -> int:
self.root.destroy()
root = tk.Tk()
ReleaseManagerApp(root)
app = ReleaseManagerApp(root)
if smoke_test:
root.withdraw()
root.update_idletasks()
root.update()
app.git_truenas_only_var.set(False)
app._update_release_mode()
root.update_idletasks()
assert app.secrets_panel.winfo_manager() == "grid"
assert app.release_panel.grid_info()["columnspan"] == 1
app.git_truenas_only_var.set(True)
app._update_release_mode()
root.update_idletasks()
assert app.release_panel.grid_info()["columnspan"] == 2
assert not app.secrets_panel.winfo_manager()
root.geometry("700x500")
root.deiconify()
root.update()
app.git_truenas_only_var.set(False)
app._update_release_mode()
root.update()
app._fit_setup_height()
assert app.secrets_panel.grid_info()["row"] == 1
assert app.release_panel.grid_info()["columnspan"] == 2
assert int(app.setup_canvas.cget("height")) <= 215
assert app.setup_canvas.yview()[1] < 1
app.setup_canvas.yview_moveto(1)
root.update_idletasks()
assert app.setup_canvas.yview()[0] > 0
assert app.release_button.winfo_manager() == "grid"
assert app.actions.winfo_y() + app.actions.winfo_height() <= root.winfo_height()
root.destroy()
return 0
root.mainloop()