481 lines
25 KiB
Python
481 lines
25 KiB
Python
"""Blueforce GUI surfaces.
|
||
|
||
The installer and the remote portal deliberately have separate view models.
|
||
They share only the backend contract; no selection, navigation, or execution
|
||
state crosses the boundary.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import queue
|
||
import subprocess
|
||
import threading
|
||
import tkinter as tk
|
||
from tkinter import messagebox, ttk
|
||
from typing import Any
|
||
|
||
from bfos import ui_theme as theme
|
||
from bfos.ui_gui import (
|
||
CommandError,
|
||
GuiContext,
|
||
Operation,
|
||
Target,
|
||
dealer_error,
|
||
operation_by_id,
|
||
parse_inventory,
|
||
)
|
||
from bfos.preferences import load_preferences, save_dealer
|
||
|
||
|
||
class SurfaceFrame(ttk.Frame):
|
||
"""Common chrome only; each product surface owns its own state below."""
|
||
|
||
def __init__(self, root: tk.Tk, backend: Any, ctx: GuiContext) -> None:
|
||
self.root = root
|
||
self.backend = backend
|
||
self.ctx = ctx
|
||
self.styles = theme.apply_theme(root)
|
||
super().__init__(root, style=self.styles["frame"])
|
||
self.pack(fill="both", expand=True)
|
||
|
||
def clear(self) -> None:
|
||
for child in self.winfo_children():
|
||
child.destroy()
|
||
|
||
def title_row(self, title: str, subtitle: str) -> ttk.Frame:
|
||
row = ttk.Frame(self, style=self.styles["frame"])
|
||
row.pack(fill="x", padx=theme.SP_6, pady=(theme.SP_5, theme.SP_3))
|
||
ttk.Label(row, text=title, style=self.styles["section"]).pack(anchor="w")
|
||
ttk.Label(row, text=subtitle, style=self.styles["panel_muted"]).pack(anchor="w")
|
||
return row
|
||
|
||
|
||
class ModePicker(SurfaceFrame):
|
||
"""Neutral launcher: choosing a mode is the only shared interaction."""
|
||
|
||
def __init__(self, root: tk.Tk, backend: Any, ctx: GuiContext) -> None:
|
||
super().__init__(root, backend, ctx)
|
||
root.title("Blueforce Field OS — çalışma alanı")
|
||
root.geometry("980x620")
|
||
root.minsize(760, 520)
|
||
self._render()
|
||
|
||
def _active_identity(self) -> str:
|
||
return f"Aktif kullanıcı: {self.ctx.active_user}" if self.ctx.active_user else "Aktif kullanıcı ayarlanmadı"
|
||
|
||
def _render(self) -> None:
|
||
self.title_row(
|
||
"Blueforce Field OS",
|
||
self._active_identity() + " · Çalışacağınız yüzeyi seçin.",
|
||
)
|
||
body = ttk.Frame(self, style=self.styles["frame"])
|
||
body.pack(fill="both", expand=True, padx=theme.SP_6, pady=theme.SP_3)
|
||
body.columnconfigure(0, weight=1)
|
||
body.columnconfigure(1, weight=1)
|
||
|
||
self._mode_card(
|
||
body, 0, "Kurulum konsolu",
|
||
"Yeni veya yeniden kurulacak tek cihaz için kontrollü, adım adım akış.",
|
||
("Bayi numarası", "Ön kontrol", "Kurulum ve kayıt", "Canlı çıktı"),
|
||
"Kurulum alanına gir",
|
||
lambda: self._open(InstallerSurface),
|
||
)
|
||
self._mode_card(
|
||
body, 1, "Uzaktan yönetim portalı",
|
||
"Envanterdeki bilgisayarları izleyin, seçin ve yetkili operasyonları çalıştırın.",
|
||
("Filo görünümü", "Cihaz durumu", "Operasyon", "Denetim izi"),
|
||
"Portala gir",
|
||
lambda: self._open(RemotePortalSurface),
|
||
)
|
||
ttk.Label(
|
||
self, text="Modu sonradan üst çubuktan değiştirerek yeni bir çalışma alanı açabilirsiniz.",
|
||
style=self.styles["faint"],
|
||
).pack(anchor="w", padx=theme.SP_6, pady=(0, theme.SP_5))
|
||
|
||
def _mode_card(self, parent: ttk.Frame, column: int, title: str,
|
||
description: str, steps: tuple[str, ...], action: str,
|
||
command) -> None:
|
||
card = ttk.Frame(parent, style=self.styles["panel"])
|
||
card.grid(row=0, column=column, sticky="nsew",
|
||
padx=(0, theme.SP_3) if column == 0 else (theme.SP_3, 0))
|
||
inner = ttk.Frame(card, style=self.styles["panel"])
|
||
inner.pack(fill="both", expand=True, padx=theme.SP_5, pady=theme.SP_5)
|
||
ttk.Label(inner, text=title, style=self.styles["panel_title"]).pack(anchor="w")
|
||
ttk.Label(inner, text=description, style=self.styles["panel_label"],
|
||
wraplength=330, justify="left").pack(anchor="w", pady=(theme.SP_2, theme.SP_5))
|
||
for step in steps:
|
||
ttk.Label(inner, text="— " + step, style=self.styles["panel_muted"]).pack(anchor="w", pady=1)
|
||
ttk.Button(inner, text=action, style=self.styles["accent_button"],
|
||
command=command).pack(anchor="w", pady=(theme.SP_6, 0))
|
||
|
||
def _open(self, surface_cls) -> None:
|
||
self.destroy()
|
||
surface_cls(self.root, self.backend, self.ctx)
|
||
|
||
|
||
class InstallerSurface(SurfaceFrame):
|
||
"""Single-device installation surface; no fleet navigation exists here."""
|
||
|
||
def __init__(self, root: tk.Tk, backend: Any, ctx: GuiContext) -> None:
|
||
super().__init__(root, backend, ctx)
|
||
root.title("Blueforce Field OS — Kurulum")
|
||
root.geometry("980x680")
|
||
root.minsize(760, 560)
|
||
self.dealer_var = tk.StringVar()
|
||
self.preferences = load_preferences()
|
||
self.dealer_var.set(self.preferences.get("dealer_id", ""))
|
||
self.dealer_locked = bool(self.preferences.get("dealer_id"))
|
||
self.has_root = getattr(os, "geteuid", lambda: 0)() == 0
|
||
self.identity_var = tk.StringVar(value=self._identity_text())
|
||
self.dealer_var.trace_add("write", lambda *_: self._on_dealer_changed())
|
||
initial_status = (
|
||
"Hazır — bayi numarası bekleniyor"
|
||
if self.has_root
|
||
else "Yönetici yetkisi yok — kurulum için uygulamayı 'sudo bf' ile açın"
|
||
)
|
||
self.status_var = tk.StringVar(value=initial_status)
|
||
self.output_queue: queue.Queue[str] = queue.Queue()
|
||
self._render()
|
||
self._drain_output()
|
||
|
||
def _render(self) -> None:
|
||
self.title_row("Kurulum konsolu", "Tek cihazı kurun, kaydedin ve doğrulayın.")
|
||
content = ttk.Frame(self, style=self.styles["frame"])
|
||
content.pack(fill="both", expand=True, padx=theme.SP_6)
|
||
content.columnconfigure(1, weight=1)
|
||
steps = ttk.Frame(content, style=self.styles["panel"])
|
||
steps.grid(row=0, column=0, sticky="ns", padx=(0, theme.SP_4))
|
||
ttk.Label(steps, text="KURULUM AKIŞI", style=self.styles["panel_title"]).pack(
|
||
anchor="w", padx=theme.SP_4, pady=theme.SP_4)
|
||
for i, label in enumerate(("Cihazı tanımla", "Ön kontrol", "Kur ve kaydet", "Doğrula"), 1):
|
||
ttk.Label(steps, text=f"{i:02d} {label}", style=self.styles["panel_label"] if i == 1 else self.styles["panel_muted"]).pack(
|
||
anchor="w", padx=theme.SP_4, pady=(0, theme.SP_3))
|
||
|
||
main = ttk.Frame(content, style=self.styles["panel"])
|
||
main.grid(row=0, column=1, sticky="nsew")
|
||
main.columnconfigure(0, weight=1)
|
||
ttk.Label(main, text="Cihaz kimliği", style=self.styles["panel_title"]).grid(
|
||
row=0, column=0, sticky="w", padx=theme.SP_5, pady=(theme.SP_5, theme.SP_2))
|
||
ttk.Label(main, text="8 haneli bayi numarasını girin. Bu akış yalnızca bu cihazı etkiler.",
|
||
style=self.styles["panel_muted"]).grid(row=1, column=0, sticky="w", padx=theme.SP_5)
|
||
ttk.Label(main, textvariable=self.identity_var, style=self.styles["panel_label"]).grid(
|
||
row=1, column=1, sticky="e", padx=theme.SP_5)
|
||
entry = ttk.Entry(main, textvariable=self.dealer_var, style=self.styles["entry"], width=22,
|
||
state="readonly" if self.dealer_locked else "normal")
|
||
self.dealer_entry = entry
|
||
entry.grid(row=2, column=0, sticky="w", padx=theme.SP_5, pady=(theme.SP_4, theme.SP_2))
|
||
entry.focus_set()
|
||
self.change_dealer_button = ttk.Button(
|
||
main, text="Bayi no değiştir", style=self.styles["quiet_button"],
|
||
command=self._unlock_dealer,
|
||
state="normal" if self.dealer_locked else "disabled")
|
||
self.change_dealer_button.grid(row=2, column=1, sticky="w", padx=theme.SP_3,
|
||
pady=(theme.SP_4, theme.SP_2))
|
||
self.install_button = ttk.Button(
|
||
main, text="Kurulumu başlat", style=self.styles["accent_button"],
|
||
command=self._run_install,
|
||
state="normal" if self.has_root else "disabled",
|
||
)
|
||
self.install_button.grid(row=3, column=0, sticky="w", padx=theme.SP_5, pady=theme.SP_4)
|
||
ttk.Label(main, textvariable=self.status_var, style=self.styles["panel_muted"]).grid(
|
||
row=4, column=0, sticky="w", padx=theme.SP_5, pady=(0, theme.SP_4))
|
||
|
||
output = ttk.Frame(self, style=self.styles["panel"])
|
||
output.pack(fill="both", expand=True, padx=theme.SP_6, pady=theme.SP_4)
|
||
ttk.Label(output, text="Kurulum çıktısı", style=self.styles["panel_title"]).pack(
|
||
anchor="w", padx=theme.SP_4, pady=(theme.SP_3, theme.SP_2))
|
||
self.output = tk.Text(output, height=8, wrap="none", state="disabled",
|
||
font=("TkFixedFont", 10), relief="flat", padx=8, pady=8)
|
||
self.output.pack(fill="both", expand=True, padx=theme.SP_3, pady=(0, theme.SP_3))
|
||
ttk.Button(self, text="Çalışma alanı seçimine dön", style=self.styles["quiet_button"],
|
||
command=self._back).pack(anchor="w", padx=theme.SP_6, pady=(0, theme.SP_5))
|
||
|
||
def _run_install(self) -> None:
|
||
if not self.has_root:
|
||
self.status_var.set("Kurulum engellendi — yönetici yetkisi için: sudo bf")
|
||
self._append(
|
||
"[ENGELLENDİ] Kurulum yönetici yetkisi ister.\n"
|
||
"Bu uygulamayı terminalden 'sudo bf' ile yeniden başlatın.\n"
|
||
)
|
||
return
|
||
dealer = self.dealer_var.get().strip()
|
||
problem = dealer_error(dealer)
|
||
if problem:
|
||
self.status_var.set(problem)
|
||
return
|
||
if self.preferences.get("dealer_id") and self.preferences["dealer_id"] != dealer:
|
||
if not messagebox.askyesno(
|
||
"Kimlik geçişini onaylayın",
|
||
"Mevcut Linux hesabı, SSH anahtarı, RustDesk ve sudo sahiplikleri yeni kimliğe taşınacak. Devam edilsin mi?",
|
||
default="no",
|
||
):
|
||
self.status_var.set("Kimlik geçişi iptal edildi; mevcut kimlik korundu.")
|
||
return
|
||
script = self.ctx.repo_root / "scripts" / "identity" / "bf-reidentity"
|
||
if not script.is_file():
|
||
self.status_var.set("Kimlik geçiş komutu bulunamadı.")
|
||
return
|
||
self.status_var.set("Kimlik geçişi çalışıyor…")
|
||
result = subprocess.run([str(script), dealer], capture_output=True, text=True, check=False)
|
||
self._append(result.stdout + result.stderr)
|
||
if result.returncode != 0:
|
||
self.status_var.set("Kimlik geçişi başarısız; eski kimlik korundu.")
|
||
return
|
||
saved = load_preferences()
|
||
else:
|
||
try:
|
||
saved = save_dealer(dealer)
|
||
except (OSError, PermissionError, ValueError) as exc:
|
||
self.status_var.set(f"Ayar kaydedilemedi: {exc}")
|
||
return
|
||
self.ctx.active_dealer = saved["dealer_id"]
|
||
self.ctx.active_user = saved["active_user"]
|
||
self.dealer_locked = True
|
||
self.dealer_entry.configure(state="readonly")
|
||
self.change_dealer_button.configure(state="normal")
|
||
operation = operation_by_id("install-local")
|
||
if operation is None:
|
||
self.status_var.set("Kurulum operasyonu katalogda bulunamadı.")
|
||
return
|
||
target = Target("dealer", dealer)
|
||
try:
|
||
plan = self.backend.build_command(operation, target, {}, check_mode=False)
|
||
except Exception as exc:
|
||
self.status_var.set(f"Plan üretilemedi: {exc}")
|
||
return
|
||
if not messagebox.askyesno("Kurulumu onaylayın", "Bu cihaz için kurulum başlatılsın mı?\n\n" + "\n".join(plan.lines), default="no"):
|
||
self.status_var.set("İptal edildi; hiçbir komut çalıştırılmadı.")
|
||
return
|
||
self.status_var.set("Kurulum çalışıyor…")
|
||
self._append("\n" + "\n".join(plan.lines) + "\n")
|
||
|
||
def worker() -> None:
|
||
try:
|
||
result = self.backend.run(plan, on_line=self.output_queue.put)
|
||
self.output_queue.put(f"\n{'[OK]' if result.ok else '[HATA]'} Çıkış kodu: {result.exit_code}\n")
|
||
except Exception as exc:
|
||
self.output_queue.put(f"[HATA] {exc}\n")
|
||
threading.Thread(target=worker, daemon=True).start()
|
||
|
||
def _identity_text(self) -> str:
|
||
dealer = self.dealer_var.get().strip()
|
||
return f"Aktif kullanıcı: bf-{dealer}" if dealer else "Aktif kullanıcı ayarlanmadı"
|
||
|
||
def _on_dealer_changed(self) -> None:
|
||
self.identity_var.set(self._identity_text())
|
||
|
||
def _unlock_dealer(self) -> None:
|
||
if not messagebox.askyesno(
|
||
"Bayi numarasını değiştir",
|
||
"Mevcut bayi kimliği kilidini açıp yeni bir numara girmek istiyor musunuz?",
|
||
default="no",
|
||
):
|
||
return
|
||
self.dealer_locked = False
|
||
self.dealer_entry.configure(state="normal")
|
||
self.dealer_var.set("")
|
||
self.change_dealer_button.configure(state="disabled")
|
||
self.status_var.set("Yeni bayi numarası girin; kurulum onayında kaydedilecek.")
|
||
self.dealer_entry.focus_set()
|
||
|
||
def _append(self, text: str) -> None:
|
||
self.output.configure(state="normal")
|
||
self.output.insert("end", text)
|
||
self.output.see("end")
|
||
self.output.configure(state="disabled")
|
||
|
||
def _drain_output(self) -> None:
|
||
try:
|
||
while True:
|
||
self._append(self.output_queue.get_nowait())
|
||
self.status_var.set("Kurulum çıktısı güncelleniyor…")
|
||
except queue.Empty:
|
||
pass
|
||
self.after(100, self._drain_output)
|
||
|
||
def _back(self) -> None:
|
||
self.destroy()
|
||
ModePicker(self.root, self.backend, self.ctx)
|
||
|
||
|
||
class RemotePortalSurface(SurfaceFrame):
|
||
"""Fleet portal surface; its state never reuses installer controls."""
|
||
|
||
def __init__(self, root: tk.Tk, backend: Any, ctx: GuiContext) -> None:
|
||
super().__init__(root, backend, ctx)
|
||
root.title("Blueforce Field OS — Uzaktan Yönetim")
|
||
root.geometry("1180x760")
|
||
root.minsize(860, 620)
|
||
self.search_var = tk.StringVar()
|
||
self.selected_host: str | None = None
|
||
self.preferences = load_preferences()
|
||
self.status_var = tk.StringVar(value="Bir cihaz seçin")
|
||
self.output_queue: queue.Queue[str] = queue.Queue()
|
||
self._render()
|
||
root.bind("<Configure>", self._on_resize)
|
||
self.search_var.trace_add("write", lambda *_: self._render_devices())
|
||
self._render_devices()
|
||
|
||
def _render(self) -> None:
|
||
self.title_row("Uzaktan yönetim portalı", "Envanter, sağlık ve kontrollü uzaktan operasyonlar · " +
|
||
(f"Aktif kullanıcı: {self.ctx.active_user}" if self.ctx.active_user else "Aktif kullanıcı ayarlanmadı"))
|
||
body = ttk.Frame(self, style=self.styles["frame"])
|
||
body.pack(fill="both", expand=True, padx=theme.SP_6)
|
||
body.columnconfigure(1, weight=1)
|
||
body.rowconfigure(1, weight=1)
|
||
nav = ttk.Frame(body, style=self.styles["panel"])
|
||
nav.grid(row=0, column=0, rowspan=2, sticky="nsew", padx=(0, theme.SP_4))
|
||
ttk.Label(nav, text="PORTAL", style=self.styles["panel_title"]).pack(anchor="w", padx=theme.SP_4, pady=theme.SP_4)
|
||
for label in ("Genel bakış", "Cihazlar", "Operasyonlar", "Denetim kaydı"):
|
||
ttk.Button(nav, text=label, style=self.styles["quiet_button"], state="disabled" if label != "Cihazlar" else "normal").pack(fill="x", padx=theme.SP_3, pady=2)
|
||
ttk.Button(nav, text="Kurulum alanına geç", style=self.styles["quiet_button"], command=self._back).pack(side="bottom", fill="x", padx=theme.SP_3, pady=theme.SP_3)
|
||
ttk.Button(nav, text="Ayarlar · bayi no değiştir", style=self.styles["quiet_button"], command=self._change_dealer).pack(side="bottom", fill="x", padx=theme.SP_3, pady=(0, theme.SP_3))
|
||
|
||
metrics = ttk.Frame(body, style=self.styles["frame"])
|
||
metrics.grid(row=0, column=1, sticky="ew", pady=(0, theme.SP_4))
|
||
for col, (label, value) in enumerate(self._metrics()):
|
||
metrics.columnconfigure(col, weight=1)
|
||
card = ttk.Frame(metrics, style=self.styles["panel"])
|
||
card.grid(row=0, column=col, sticky="ew", padx=(0, theme.SP_3) if col < 3 else (0, 0))
|
||
ttk.Label(card, text=label, style=self.styles["panel_title"]).pack(anchor="w", padx=theme.SP_3, pady=(theme.SP_3, 0))
|
||
ttk.Label(card, text=value, style=self.styles["panel_label"]).pack(anchor="w", padx=theme.SP_3, pady=(theme.SP_1, theme.SP_3))
|
||
|
||
devices = ttk.Frame(body, style=self.styles["panel"])
|
||
devices.grid(row=1, column=1, sticky="nsew")
|
||
devices.columnconfigure(0, weight=1)
|
||
devices.columnconfigure(1, weight=1)
|
||
devices.rowconfigure(2, weight=1)
|
||
ttk.Label(devices, text="Cihaz envanteri", style=self.styles["panel_title"]).grid(row=0, column=0, sticky="w", padx=theme.SP_4, pady=(theme.SP_4, theme.SP_2))
|
||
ttk.Entry(devices, textvariable=self.search_var, style=self.styles["entry"]).grid(row=1, column=0, sticky="ew", padx=theme.SP_4, pady=(0, theme.SP_3))
|
||
columns = ("host", "wave", "address")
|
||
self.tree = ttk.Treeview(devices, columns=columns, show="headings", selectmode="browse")
|
||
for column, heading in zip(columns, ("CİHAZ", "DALGA", "ADRES")):
|
||
self.tree.heading(column, text=heading)
|
||
self.tree.column(column, width=180, anchor="w")
|
||
self.tree.grid(row=2, column=0, sticky="nsew", padx=(theme.SP_4, theme.SP_2), pady=(0, theme.SP_3))
|
||
self.tree.bind("<<TreeviewSelect>>", self._select_device)
|
||
|
||
self.detail = ttk.Frame(devices, style=self.styles["panel"])
|
||
self.detail.grid(row=2, column=1, sticky="nsew", padx=(theme.SP_2, theme.SP_4), pady=(0, theme.SP_3))
|
||
ttk.Label(self.detail, text="Cihaz detayları", style=self.styles["panel_title"]).pack(
|
||
anchor="w", padx=theme.SP_4, pady=(theme.SP_4, theme.SP_2))
|
||
self.detail_value = ttk.Label(self.detail, text="Bir cihaz seçin", style=self.styles["panel_label"], justify="left")
|
||
self.detail_value.pack(anchor="w", padx=theme.SP_4, pady=(0, theme.SP_3))
|
||
self.detail_hint = ttk.Label(self.detail, text="Envanterden bir satır seçtiğinizde kimlik ve hedef bilgisi burada görünür.",
|
||
style=self.styles["panel_muted"], wraplength=260, justify="left")
|
||
self.detail_hint.pack(anchor="w", padx=theme.SP_4)
|
||
|
||
action = ttk.Frame(self, style=self.styles["panel"])
|
||
action.pack(fill="x", padx=theme.SP_6, pady=(theme.SP_4, theme.SP_3))
|
||
ttk.Label(action, textvariable=self.status_var, style=self.styles["panel_muted"]).pack(side="left", padx=theme.SP_4, pady=theme.SP_3)
|
||
self.operation = ttk.Combobox(action, state="readonly", width=32, values=tuple(op.label for op in self.backend.list_operations() if not op.destructive))
|
||
self.operation.pack(side="right", padx=(theme.SP_2, theme.SP_4), pady=theme.SP_3)
|
||
ttk.Button(action, text="Salt-okunur çalıştır", style=self.styles["accent_button"], command=self._run_read_only).pack(side="right", pady=theme.SP_3)
|
||
|
||
def _metrics(self) -> tuple[tuple[str, str], ...]:
|
||
groups = parse_inventory(self.ctx.inventory) if self.ctx.inventory else {}
|
||
hosts = sorted({host for values in groups.values() for host in values})
|
||
return (("TOPLAM CİHAZ", str(len(hosts))), ("ÇEVRİMİÇİ", "—"),
|
||
("ÇALIŞAN İŞLEM", "yok"), ("UYARI", "yok"))
|
||
|
||
def _on_resize(self, event) -> None:
|
||
if event.widget is not self.root or not hasattr(self, "detail"):
|
||
return
|
||
if event.width < 1050:
|
||
self.detail.grid_remove()
|
||
else:
|
||
self.detail.grid()
|
||
|
||
def _render_devices(self) -> None:
|
||
if not hasattr(self, "tree"):
|
||
return
|
||
for item in self.tree.get_children():
|
||
self.tree.delete(item)
|
||
groups = parse_inventory(self.ctx.inventory) if self.ctx.inventory else {}
|
||
query = self.search_var.get().strip().lower()
|
||
hosts = sorted({host for values in groups.values() for host in values})
|
||
for host in hosts:
|
||
if query and query not in host.lower():
|
||
continue
|
||
wave = next((name for name, values in groups.items() if host in values), "—")
|
||
self.tree.insert("", "end", iid=host, values=(host, wave, "envanterden okunur"))
|
||
|
||
def _select_device(self, _event=None) -> None:
|
||
selected = self.tree.selection()
|
||
self.selected_host = selected[0] if selected else None
|
||
self.status_var.set(f"Seçili cihaz: {self.selected_host}" if self.selected_host else "Bir cihaz seçin")
|
||
if self.selected_host:
|
||
item = self.tree.item(self.selected_host)
|
||
values = item.get("values", ())
|
||
self.detail_value.configure(text=(
|
||
f"{self.selected_host}\n\n"
|
||
f"Durum: bağlantı bekleniyor\n"
|
||
f"Dalga: {values[1] if len(values) > 1 else '—'}\n"
|
||
f"Adres: {values[2] if len(values) > 2 else '—'}"
|
||
))
|
||
self.detail_hint.configure(text="Salt-okunur operasyonlar seçili cihaza uygulanır. Yıkıcı işlemler ayrıca onay ve yetki kapısından geçer.")
|
||
|
||
def _run_read_only(self) -> None:
|
||
if not self.selected_host:
|
||
messagebox.showinfo("Cihaz seçin", "Önce envanterden bir cihaz seçin.")
|
||
return
|
||
label = self.operation.get()
|
||
operation = next((op for op in self.backend.list_operations() if op.label == label), None)
|
||
if operation is None:
|
||
messagebox.showinfo("Operasyon seçin", "Salt-okunur bir operasyon seçin.")
|
||
return
|
||
self.status_var.set(f"Çalıştırılıyor: {operation.label} — {self.selected_host}")
|
||
# The existing backend remains the only command authority.
|
||
try:
|
||
plan = self.backend.build_command(operation, Target("dealer", self.selected_host.replace("bf-", "")), {}, check_mode=True)
|
||
if not messagebox.askyesno("Salt-okunur operasyon", "Komut çalıştırılsın mı?\n\n" + "\n".join(plan.lines), default="no"):
|
||
self.status_var.set("İptal edildi")
|
||
return
|
||
def worker() -> None:
|
||
try:
|
||
result = self.backend.run(plan)
|
||
self.root.after(0, lambda: self.status_var.set(
|
||
f"Tamamlandı: çıkış kodu {result.exit_code}"))
|
||
except Exception as exc:
|
||
message = str(exc)
|
||
self.root.after(0, lambda: self.status_var.set(f"Hata: {message}"))
|
||
threading.Thread(target=worker, daemon=True).start()
|
||
except Exception as exc:
|
||
self.status_var.set(f"Hata: {exc}")
|
||
|
||
def _back(self) -> None:
|
||
self.destroy()
|
||
ModePicker(self.root, self.backend, self.ctx)
|
||
|
||
def _change_dealer(self) -> None:
|
||
dialog = tk.Toplevel(self.root)
|
||
dialog.title("Bayi numarasını değiştir")
|
||
dialog.transient(self.root)
|
||
dialog.grab_set()
|
||
value = tk.StringVar(value=self.preferences.get("dealer_id", ""))
|
||
ttk.Label(dialog, text="Yeni bayi no (8 hane)", style=self.styles["panel_title"]).pack(
|
||
padx=theme.SP_5, pady=(theme.SP_5, theme.SP_2), anchor="w")
|
||
entry = ttk.Entry(dialog, textvariable=value, width=20, style=self.styles["entry"])
|
||
entry.pack(padx=theme.SP_5, pady=(0, theme.SP_4), anchor="w")
|
||
def apply() -> None:
|
||
problem = dealer_error(value.get().strip())
|
||
if problem:
|
||
messagebox.showerror("Geçersiz bayi numarası", problem, parent=dialog)
|
||
return
|
||
if not messagebox.askyesno("Kimliği değiştir", "Aktif kullanıcı bf-" + value.get().strip() + " olarak değiştirilsin mi?", parent=dialog, default="no"):
|
||
return
|
||
saved = save_dealer(value.get().strip())
|
||
self.ctx.active_dealer = saved["dealer_id"]
|
||
self.ctx.active_user = saved["active_user"]
|
||
dialog.destroy()
|
||
self._back()
|
||
ttk.Button(dialog, text="Kaydet ve portalı yenile", style=self.styles["accent_button"], command=apply).pack(
|
||
padx=theme.SP_5, pady=(0, theme.SP_5), anchor="w")
|
||
entry.focus_set()
|
||
|
||
|
||
def launch_surface(root: tk.Tk, backend: Any, ctx: GuiContext, mode: str | None = None) -> SurfaceFrame:
|
||
"""Launch one independent surface, or the neutral mode picker."""
|
||
surface = {"install": InstallerSurface, "portal": RemotePortalSurface}.get(mode, ModePicker)
|
||
return surface(root, backend, ctx)
|