feat(console): application shell with overview, browser, drawer and safety UX

The console was a flat form: every control on the root window, 48 operations
as undifferentiated rows, a giant output textarea, privilege as a buried
sentence. It now has a shell: a fixed header band (product, target, device
state, privilege) plus a page body, and in detailed mode a sidebar with five
operation categories and an Activity page.

What changed (presentation layer only; _execute, gates, run_plan, audit and
redaction untouched):
- simple mode is an Overview: four state cards (target/device/privilege/
  inventory) plus Install/Status/Exit, instead of the raw form;
- the target is one segmented control (Device/Wave/Entire fleet) with a single
  scope source of truth; the dealer text can no longer silently override the
  wave, and the scope stays visible across pages;
- operations are browsed, not dumped: icon + name + explanation + scope/risk
  badges, with a detail panel (meta grid, warnings, options form, backend-built
  command preview on demand);
- destructive confirmations are dedicated dialogs showing OPERATION/TARGET/
  SCOPE/EFFECT with a fleet/production strip and focus on Cancel; the token
  dialog is styled; secrets never enter either;
- the output textarea is an execution drawer (IDLE/RUNNING with mm:ss /
  DONE-FAILED with duration and exit code) streaming the same backend output;
- write operations stay visible but disabled with their reason when
  unprivileged, in both entry points;
- new module bfos/ui_widgets.py (~490 lines, stdlib + tkinter only, no colour
  literals): canvas line icons, segmented control, collapsible panel, meta
  grid, timer, the two safety dialogs.

Tests: test_bfos_gui_static.py 95 OK, test_bfos.py 155 OK, run-all.sh 12/12.
The colour-literal test now also scans ui_widgets.py (proven by mutation:
a #ff0000 probe goes red, green after removal). Live QA: zero geometry errors
over 712 widgets, all 48 operations listed, 13 write ops disabled with reasons
without root, TUI and --list smoke-tested.

Known limits: no dark theme (tokens ready); the detail page's target line does
not refresh when the target changes mid-form (the confirmation dialog always
shows the current one); visual QA was geometry + OCR, not a human eye.
This commit is contained in:
emirhanmamak-blueforce 2026-09-16 23:21:41 +03:00
parent b6a14ca8e0
commit 2bad7fbb7f
5 changed files with 1574 additions and 274 deletions

File diff suppressed because it is too large Load diff

489
bfos/ui_widgets.py Normal file
View file

@ -0,0 +1,489 @@
"""bfos/ui_widgets.py -- reusable presentational components for the console.
What this module is
-------------------
The small widget vocabulary the redesigned operator console is built from:
line icons (Tk canvas, shipped locally, no assets), a segmented scope
control, a collapsible panel, a two-column meta grid, a scrollable row
container, an elapsed timer, and two safety dialogs (plan confirmation and
typed-token gate).
What this module is NOT
-----------------------
It knows nothing about operations, targets, gates, commands, auditing or
execution. It receives plain strings and token colours and returns plain
widget handles and booleans. Every behavioural decision (which gates exist,
what a token must equal, when a command may run) stays in ``bfos/ui_gui.py``
and the backend behind it.
Design rules (docs/DESIGN.md, docs/34-CONSOLE-VISUAL-CONCEPT.md):
* no colour literals -- every fill/foreground comes from ``ui_theme``,
* no user-visible English -- caller-facing labels are Turkish,
* status is word + colour, never colour alone (callers pass the words),
* tkinter is optional: the module imports without it so static tests never
need a display (mirrors the ``ui_gui.py`` guard).
"""
from __future__ import annotations
import time
from pathlib import Path
from typing import Any, Callable, Mapping, Sequence
try: # pragma: no cover - environment dependent
import tkinter as tk
from tkinter import ttk
HAVE_TKINTER = True
except Exception: # pragma: no cover - environment dependent
tk = ttk = None # type: ignore[assignment]
HAVE_TKINTER = False
# The view layer only calls these with a live display; Any keeps the optional
# import from tripping the type checker (same pattern as bfos/ui_gui.py).
tk: Any
ttk: Any
try:
from bfos import ui_theme as theme
except ImportError: # pragma: no cover - loaded outside the package
import sys
_WIDGETS_DIR = str(Path(__file__).resolve().parent)
if _WIDGETS_DIR not in sys.path:
sys.path.insert(0, _WIDGETS_DIR)
import ui_theme as theme # type: ignore[no-redef]
# ---------------------------------------------------------------------------
# Line icons -- one stroke vocabulary, drawn on a Tk canvas
# ---------------------------------------------------------------------------
def draw_icon(canvas, name: str, size: int, colour: str) -> None:
"""Draw a ``name`` line icon centred on ``canvas`` (already sized).
Coordinates are derived from ``size`` so every icon scales together; the
stroke is a single colour passed by the caller (a theme token), never a
literal. Unknown names fall back to a neutral square so a typo degrades
to a box instead of an exception.
"""
pad = max(2, size // 6)
x0, y0, x1, y1 = pad, pad, size - pad, size - pad
w = 2
line = lambda *pts: canvas.create_line(*pts, fill=colour, width=w)
rect = lambda a, b, c, d: canvas.create_rectangle(
a, b, c, d, outline=colour, width=w
)
if name == "device": # monitor: screen + stand
rect(x0, y0, x1, y1 - 5)
mid = (x0 + x1) / 2
line(mid, y1 - 5, mid, y1 - 1)
line(mid - 4, y1 - 1, mid + 4, y1 - 1)
elif name == "install": # arrow down into a tray
mid = (x0 + x1) / 2
line(mid, y0, mid, y1 - 5)
line(mid, y0, mid - 4, y0 + 4)
line(mid, y0, mid + 4, y0 + 4)
line(x0, y1 - 2, x1, y1 - 2)
line(x0, y1 - 2, x0, y1 - 5)
line(x1, y1 - 2, x1, y1 - 5)
elif name == "health": # check mark in a circle
canvas.create_oval(x0, y0, x1, y1, outline=colour, width=w)
line(x0 + 3, (y0 + y1) / 2, (x0 + x1) / 2 - 1, y1 - 4)
line((x0 + x1) / 2 - 1, y1 - 4, x1 - 3, y0 + 4)
elif name == "info": # small i in a circle
canvas.create_oval(x0, y0, x1, y1, outline=colour, width=w)
mid = (x0 + x1) / 2
line(mid, y0 + 4, mid, y0 + 5)
line(mid, y0 + 7, mid, y1 - 4)
elif name == "tool": # sliders: two horizontal tracks with knobs
third = (y1 - y0) / 3
line(x0, y0 + third, x1, y0 + third)
line(x0, y0 + 2 * third, x1, y0 + 2 * third)
canvas.create_oval(x0 + 2, y0 + third - 3, x0 + 8, y0 + third + 3,
outline=colour, width=w)
canvas.create_oval(x1 - 8, y0 + 2 * third - 3, x1 - 2,
y0 + 2 * third + 3, outline=colour, width=w)
elif name == "wave": # signal arcs from a dot
canvas.create_oval(x0 + 1, y1 - 5, x0 + 5, y1 - 1,
fill=colour, outline=colour)
canvas.create_arc(x0 - 1, y0 + 3, x1 - 3, y1 + 1, start=270, extent=90,
outline=colour, width=w, style="arc")
canvas.create_arc(x0 + 3, y0 - 1, x1 + 1, y1 - 3, start=270, extent=90,
outline=colour, width=w, style="arc")
elif name == "fleet": # four small squares: many machines
mx, my = (x0 + x1) / 2, (y0 + y1) / 2
rect(x0, y0, mx - 1, my - 1)
rect(mx + 1, y0, x1, my - 1)
rect(x0, my + 1, mx - 1, y1)
rect(mx + 1, my + 1, x1, y1)
elif name == "activity": # clock: recent runs
canvas.create_oval(x0, y0, x1, y1, outline=colour, width=w)
mid = ((x0 + x1) / 2, (y0 + y1) / 2)
line(*mid, *mid, mid[0], y0 + 5)
line(*mid, *mid, mid[0] + 4, mid[1] + 1)
elif name == "warn": # triangle with a mark
mid = (x0 + x1) / 2
line(mid, y0 + 1, x1 - 1, y1 - 1)
line(x1 - 1, y1 - 1, x0 + 1, y1 - 1)
line(x0 + 1, y1 - 1, mid, y0 + 1)
line(mid, y0 + 6, mid, y0 + 10)
line(mid, y1 - 5, mid, y1 - 4)
elif name == "shield": # privilege outline
mid = (x0 + x1) / 2
line(x0 + 1, y0 + 1, mid, y0 + 1)
line(mid, y0 + 1, x1 - 1, y0 + 1)
line(x1 - 1, y0 + 1, x1 - 1, y0 + 7)
line(x1 - 1, y0 + 7, mid, y1 - 1)
line(mid, y1 - 1, x0 + 1, y0 + 7)
line(x0 + 1, y0 + 7, x0 + 1, y0 + 1)
elif name == "chevron": # row affordance: there is a next step
line(x0 + 2, y0 + 2, x1 - 2, (y0 + y1) / 2)
line(x1 - 2, (y0 + y1) / 2, x0 + 2, y1 - 2)
else: # neutral box: a missing icon is visible, never fatal
rect(x0 + 2, y0 + 2, x1 - 2, y1 - 2)
def line_icon(parent, name: str, size: int = 22, colour: str | None = None,
bg: str | None = None):
"""A canvas carrying one line icon (offline, no image assets)."""
canvas = tk.Canvas(
parent,
width=size,
height=size,
background=bg or theme.BG_PANEL,
highlightthickness=0,
borderwidth=0,
)
draw_icon(canvas, name, size, colour or theme.TEXT_MUTED)
return canvas
# ---------------------------------------------------------------------------
# Segmented control -- exactly one scope is on
# ---------------------------------------------------------------------------
class SegmentedControl:
"""Three-way scope switch: [Cihaz | Dalga | Tüm filo].
Exactly one segment is selected and the selection is always visible, so
the operator can never believe a device is targeted while the fleet is.
Implemented on ttk buttons (keyboard-focusable, native tab order); the
selected segment carries the accent style, the rest the quiet style.
"""
def __init__(self, parent, styles: Mapping[str, str],
options: Sequence[tuple[str, str]],
initial: str,
command: Callable[[str], None] | None = None) -> None:
self.styles = styles
self.command = command
self.frame = ttk.Frame(parent, style=styles["panel"])
self._buttons: dict[str, Any] = {}
self._value = initial
for key, label in options:
button = ttk.Button(self.frame, text=label,
command=lambda k=key: self.set(k))
button.pack(side="left", padx=(0, theme.SP_1))
self._buttons[key] = button
self._paint()
def _paint(self) -> None:
for key, button in self._buttons.items():
if key == self._value:
button.configure(style=self.styles["accent_button"],
state="disabled")
else:
button.configure(style=self.styles["button"], state="normal")
def set(self, key: str, notify: bool = True) -> None:
if key not in self._buttons or key == self._value:
return
self._value = key
self._paint()
if notify and self.command is not None:
self.command(key)
def get(self) -> str:
return self._value
def grid(self, **kwargs) -> None: # honour the caller's geometry
self.frame.grid(**kwargs)
def pack(self, **kwargs) -> None:
self.frame.pack(**kwargs)
# ---------------------------------------------------------------------------
# Collapsible panel -- long detail hidden until asked for
# ---------------------------------------------------------------------------
class CollapsiblePanel:
"""A titled section whose body folds away (command preview, details)."""
def __init__(self, parent, styles: Mapping[str, str], title: str,
*, expanded: bool = False,
show_label: str = "Göster",
hide_label: str = "Gizle") -> None:
self.styles = styles
self.show_label = show_label
self.hide_label = hide_label
self.expanded = expanded
self.frame = ttk.Frame(parent, style=styles["panel"])
self.toggle = ttk.Button(self.frame, style=styles["quiet_button"],
command=self.toggle_body)
self.toggle.pack(fill="x")
self.body = ttk.Frame(self.frame, style=styles["panel"])
self._title = title
self._paint()
def _paint(self) -> None:
marker = "▾" if self.expanded else "▸"
action = self.hide_label if self.expanded else self.show_label
self.toggle.configure(text=f"{marker} {self._title} — {action}")
if self.expanded:
self.body.pack(fill="both", expand=True,
pady=(theme.SP_2, 0))
else:
self.body.pack_forget()
def toggle_body(self) -> None:
self.expanded = not self.expanded
self._paint()
def set_expanded(self, expanded: bool) -> None:
if expanded != self.expanded:
self.toggle_body()
def pack(self, **kwargs) -> None:
self.frame.pack(**kwargs)
def grid(self, **kwargs) -> None:
self.frame.grid(**kwargs)
# ---------------------------------------------------------------------------
# Meta grid -- label/value pairs with one shared label column
# ---------------------------------------------------------------------------
def meta_grid(parent, styles: Mapping[str, str],
pairs: Sequence[tuple[str, str]],
label_width: int = 14) -> Any:
"""Two-column fact block (İŞLEM/HEDEF/KAPSAM/ETKİ); labels right-aligned."""
frame = ttk.Frame(parent, style=styles["panel"])
frame.columnconfigure(1, weight=1)
for row, (label, value) in enumerate(pairs):
ttk.Label(frame, text=label, style=styles["field_label"],
width=label_width, anchor="e").grid(
row=row, column=0, sticky="ne", padx=(0, theme.SP_2),
pady=theme.SP_1)
text = ttk.Label(frame, text=value, style=styles["panel_label"],
justify="left")
text.grid(row=row, column=1, sticky="nw", pady=theme.SP_1)
return frame
# ---------------------------------------------------------------------------
# Scrollable row container -- the operation browser lives here
# ---------------------------------------------------------------------------
def scrollable_rows(parent, styles: Mapping[str, str]):
"""A canvas + scrollbar pair with an inner frame rows pack into."""
frame = ttk.Frame(parent, style=styles["panel"])
canvas = tk.Canvas(frame, background=theme.BG_PANEL,
highlightthickness=0, borderwidth=0)
scroll = ttk.Scrollbar(frame, orient="vertical", command=canvas.yview)
canvas.configure(yscrollcommand=scroll.set)
scroll.pack(side="right", fill="y")
canvas.pack(side="left", fill="both", expand=True)
inner = ttk.Frame(canvas, style=styles["panel"])
window = canvas.create_window((0, 0), window=inner, anchor="nw")
def on_inner_configure(_event=None) -> None:
canvas.configure(scrollregion=canvas.bbox("all"))
def on_canvas_configure(event) -> None:
canvas.itemconfigure(window, width=event.width)
inner.bind("<Configure>", on_inner_configure)
canvas.bind("<Configure>", on_canvas_configure)
return frame, inner
# ---------------------------------------------------------------------------
# Elapsed timer -- mm:ss for the running state, no widget of its own
# ---------------------------------------------------------------------------
class Elapsed:
"""Monotonic stopwatch; the drawer polls :meth:`text` via ``after``."""
def __init__(self) -> None:
self._start = 0.0
self._end = 0.0
def start(self) -> None:
self._start = time.monotonic()
self._end = 0.0
def stop(self) -> None:
self._end = time.monotonic()
def seconds(self) -> float:
if not self._start:
return 0.0
return (self._end or time.monotonic()) - self._start
def text(self) -> str:
total = int(self.seconds())
return f"{total // 60:02d}:{total % 60:02d}"
# ---------------------------------------------------------------------------
# Safety dialogs -- the confirm gate and the typed-token gate, as windows
# ---------------------------------------------------------------------------
def _base_dialog(root, title: str, min_width: int = 560):
dialog = tk.Toplevel(root)
dialog.title(title)
dialog.configure(bg=theme.BG_APP)
dialog.transient(root)
dialog.grab_set()
dialog.minsize(min_width, 200)
dialog.bind("<Escape>", lambda _e: dialog.destroy())
return dialog
def _dialog_buttons(dialog, styles: Mapping[str, str],
confirm_label: str, danger: bool,
on_confirm, cancel_label: str = "Vazgeç") -> Any:
row = ttk.Frame(dialog, style=styles["frame"])
row.pack(fill="x", padx=theme.SP_4, pady=theme.SP_4)
cancel = ttk.Button(row, text=cancel_label,
style=styles["quiet_button"],
command=dialog.destroy)
cancel.pack(side="right")
confirm = ttk.Button(
row, text=confirm_label,
style=styles["danger_button"] if danger else styles["accent_button"],
command=on_confirm,
)
confirm.pack(side="right", padx=(0, theme.SP_2))
return confirm, cancel
def ask_confirmation(root, styles: Mapping[str, str], *,
title: str,
operation: str,
target_label: str,
scope_label: str,
effect: str,
preview_lines: Sequence[str],
confirm_label: str,
scope_warning: str | None = None,
danger: bool = False) -> bool:
"""Plan confirmation: OPERATION / TARGET / SCOPE / EFFECT + preview.
Returns True only on an explicit confirm click. For destructive plans
the Cancel control takes the initial focus and the Return key, so Enter
can never confirm destruction; the dialog closes (False) on Escape.
"""
result: dict[str, bool] = {"value": False}
dialog = _base_dialog(root, title)
ttk.Label(dialog, text=title, style=styles["dialog_title"],
wraplength=520, justify="left").pack(
padx=theme.SP_4, pady=(theme.SP_4, theme.SP_2), anchor="w")
if scope_warning:
banner = ttk.Frame(dialog, style=styles["warn_band"])
banner.pack(fill="x", padx=theme.SP_4, pady=(0, theme.SP_2))
inner = ttk.Frame(banner, style=styles["warn_band"])
inner.pack(fill="x", padx=theme.SP_3, pady=theme.SP_2)
ttk.Label(inner, text=scope_warning,
style=styles["warn_label"],
wraplength=480, justify="left").pack(anchor="w")
grid = meta_grid(dialog, styles, (
("İŞLEM", operation),
("HEDEF", target_label),
("KAPSAM", scope_label),
("ETKİ", effect),
))
grid.pack(fill="x", padx=theme.SP_4, pady=(0, theme.SP_2))
preview = CollapsiblePanel(dialog, styles, "Komut önizlemesi",
expanded=danger)
preview.pack(fill="both", expand=True, padx=theme.SP_4)
text = tk.Text(
preview.body,
height=min(12, max(4, len(preview_lines))),
wrap="word",
font=theme.data_font(theme.SIZE_SM),
background=theme.BG_SUNKEN,
foreground=theme.TEXT,
relief="flat",
borderwidth=0,
highlightthickness=0,
padx=theme.SP_3,
pady=theme.SP_2,
cursor="arrow",
takefocus=0,
)
text.pack(fill="both", expand=True)
for line in preview_lines:
text.insert("end", line + "\n")
text.configure(state="disabled")
def confirm() -> None:
result["value"] = True
dialog.destroy()
confirm_btn, cancel_btn = _dialog_buttons(
dialog, styles, confirm_label, danger, confirm)
if danger:
cancel_btn.focus_set() # Enter must never confirm destruction
else:
confirm_btn.focus_set()
dialog.bind("<Return>", lambda _e: confirm() if not danger else None)
dialog.wait_window()
return result["value"]
def ask_token(root, styles: Mapping[str, str], *,
title: str,
message: str,
scope_warning: str | None = None,
confirm_label: str = "Onayla") -> str | None:
"""Typed-token gate: the operator retypes the exact token to proceed.
Empty input or window close aborts (None). The comparison itself stays
with the caller in ``ui_gui.py``; this window only collects the text.
"""
result: dict[str, str | None] = {"value": None}
dialog = _base_dialog(root, title, min_width=480)
ttk.Label(dialog, text=title, style=styles["dialog_title"]).pack(
padx=theme.SP_4, pady=(theme.SP_4, theme.SP_2), anchor="w")
if scope_warning:
banner = ttk.Frame(dialog, style=styles["warn_band"])
banner.pack(fill="x", padx=theme.SP_4, pady=(0, theme.SP_2))
inner = ttk.Frame(banner, style=styles["warn_band"])
inner.pack(fill="x", padx=theme.SP_3, pady=theme.SP_2)
ttk.Label(inner, text=scope_warning,
style=styles["warn_label"],
wraplength=420, justify="left").pack(anchor="w")
ttk.Label(dialog, text=message, style=styles["dialog"],
wraplength=440, justify="left").pack(
padx=theme.SP_4, pady=(0, theme.SP_3), anchor="w")
var = tk.StringVar()
entry = ttk.Entry(dialog, textvariable=var, width=32,
style=styles["entry"])
entry.pack(padx=theme.SP_4, anchor="w")
entry.focus_set()
def confirm() -> None:
result["value"] = var.get().strip() or None
dialog.destroy()
confirm_btn, _cancel_btn = _dialog_buttons(
dialog, styles, confirm_label, False, confirm)
confirm_btn.focus_set()
dialog.bind("<Return>", lambda _e: confirm())
dialog.wait_window()
return result["value"]

View file

@ -15,13 +15,13 @@
## 1. Amaç ## 1. Amaç
**Bu araç nedir?** `bf`, cihaza `sudo bf` yazınca açılan **grafik operatör uygulamasıdır**. Varsayılan olarak **basit mod** gelir: ekranda üç büyük düğme vardır — `Kur`, `Durum`, `Çıkış`. Üst çubuktaki `Detaylı mod` düğmesiyle **5 kategori sekmesi** açılır: Kurulum, Cihaz işlemleri, Merkez hazırlığı, Araçlar, Cihaz bilgisi. Listeden bir işlem seçip `Komutu göster ve çalıştır` düğmesine basarsın; uygulama senin adına doğru komutu üretir, çalıştırmadan **önce gösterip onay ister** ve ne yaptığını Türkçe satırlarla akıtır. **Bu araç nedir?** `bf`, cihaza `sudo bf` yazınca açılan **grafik operatör uygulamasıdır**. Varsayılan olarak **basit mod** gelir: ekranda üç büyük düğme vardır — `Kur`, `Durum`, `Çıkış`. Üst çubuktaki `Detaylı mod` düğmesiyle **kenar çubuklu kabuk** açılır: 5 katalog bölümü (Kurulum, Cihaz işlemleri, Merkez hazırlığı, Araçlar, Cihaz bilgisi) + Etkinlik. Tarayıcıdan bir işlem seçip `Komutu göster ve çalıştır` düğmesine basarsın; uygulama senin adına doğru komutu üretir, çalıştırmadan **önce gösterip onay ister** ve ne yaptığını Türkçe satırlarla akıtır.
**Neyi kolaylaştırır?** Normalde cihazı kurmak, durumunu okumak, teşhis paketi almak veya bir filo işlemi başlatmak için onlarca komutu ve doğru sırasını bilmek gerekir. `bf` bunları sekmeler ve arama yapılabilir listeler altında toplar, her komutu **çalıştırmadan önce** gösterir, tehlikeli işlemlerde ikinci kez sorar, `production` dalgası ve sürüm kapısı için ayrı onay ister ve her işlemi bir denetim kaydına (`/var/log/blueforce-console.log`, mod `600`) yazar. Kısacası: ezberleyeceğin tek şey `sudo bf` komutudur. **Neyi kolaylaştırır?** Normalde cihazı kurmak, durumunu okumak, teşhis paketi almak veya bir filo işlemi başlatmak için onlarca komutu ve doğru sırasını bilmek gerekir. `bf` bunları bölümler ve arama yapılabilir listeler altında toplar, her komutu **çalıştırmadan önce** gösterir, tehlikeli işlemlerde ikinci kez sorar, `production` dalgası ve sürüm kapısı için ayrı onay ister ve her işlemi bir denetim kaydına (`/var/log/blueforce-console.log`, mod `600`) yazar. Kısacası: ezberleyeceğin tek şey `sudo bf` komutudur.
## 2. Kapsam ## 2. Kapsam
- Kapsam içi: Ubuntu'da ilk kurulum (git'ten indir + bootstrap), basit mod ve 5 kategori sekmesi, üst çubuktan hedef seçimi (bayi no / dalga / tüm filo), uçtan uca örnek seans, yetki modeli (`sudo bf` = tam yetki), şifre dosyası (`bf-creds`) davranışı, grafik arayüz açılamadığında ne olacağı, en sık hatalar ve güvenlik kuralları. - Kapsam içi: Ubuntu'da ilk kurulum (git'ten indir + bootstrap), basit mod ve kenar çubuklu 5 bölüm, parçalı seçiciden hedef seçimi (cihaz / dalga / tüm filo), uçtan uca örnek seans, yetki modeli (`sudo bf` = tam yetki), şifre dosyası (`bf-creds`) davranışı, grafik arayüz açılamadığında ne olacağı, en sık hatalar ve güvenlik kuralları.
- Kapsam dışı: ISO/firstboot/offline provisioning yolu (26, 27 — artık **ikincil/opsiyonel**), merkez sunucularının kurulumu (07, 09 — arayüz yalnız **hazırlar ve denetler**, kurmaz), update onay zincirinin merkez tarafı (10, 21), rol bazlı eğitim müfredatı (31). - Kapsam dışı: ISO/firstboot/offline provisioning yolu (26, 27 — artık **ikincil/opsiyonel**), merkez sunucularının kurulumu (07, 09 — arayüz yalnız **hazırlar ve denetler**, kurmaz), update onay zincirinin merkez tarafı (10, 21), rol bazlı eğitim müfredatı (31).
## 3. Kararlar ## 3. Kararlar
@ -29,7 +29,7 @@
```text ```text
KARAR: Cihaz ve filo işlemlerinin tek operatör giriş noktası `sudo bf` grafik uygulamasıdır KARAR: Cihaz ve filo işlemlerinin tek operatör giriş noktası `sudo bf` grafik uygulamasıdır
(`bfos/ui_gui.py`, tkinter). Operatör komut ezberlemez; basit modda üç düğmeden, (`bfos/ui_gui.py`, tkinter). Operatör komut ezberlemez; basit modda üç düğmeden,
detaylı modda kategori sekmesinden seçer. detaylı modda kenar çubuğundaki bölümden seçer.
GEREKÇE: Operatör profili Linux/DevOps'a yeni başlayan kişidir; serbest komut yazımı 700 cihazda GEREKÇE: Operatör profili Linux/DevOps'a yeni başlayan kişidir; serbest komut yazımı 700 cihazda
yazım hatasını filo arızasına çevirir (K-11 blast-radius). Arayüz her komutu yazım hatasını filo arızasına çevirir (K-11 blast-radius). Arayüz her komutu
çalıştırmadan önce gösterir ve onay ister; dalga kapılı playbook'lara bayrak sessizce çalıştırmadan önce gösterir ve onay ister; dalga kapılı playbook'lara bayrak sessizce
@ -139,9 +139,9 @@ Git tabanlı kurulum "hangi sürüm sahada?" sorusunu `git log` ile yanıtlanabi
- Katalog tablosu ile playbook'lar arasında kapsam (scope) uyuşmazlığı olursa komut üretilmez ve arayüz **çalıştırmayı reddeder**; bu, playbook güncellemesiyle katalog kaydının birlikte güncellenmesini zorunlu kılar. - Katalog tablosu ile playbook'lar arasında kapsam (scope) uyuşmazlığı olursa komut üretilmez ve arayüz **çalıştırmayı reddeder**; bu, playbook güncellemesiyle katalog kaydının birlikte güncellenmesini zorunlu kılar.
- Grafik arayüz bir pencere sistemine ihtiyaç duyar: `DISPLAY`/`WAYLAND_DISPLAY` yoksa (başsız sunucu, X yönlendirmesiz SSH) açılmaz — o durumda terminal konsoluna düşülür. - Grafik arayüz bir pencere sistemine ihtiyaç duyar: `DISPLAY`/`WAYLAND_DISPLAY` yoksa (başsız sunucu, X yönlendirmesiz SSH) açılmaz — o durumda terminal konsoluna düşülür.
- Cihaz işlemleri sekmesindeki filo işlemleri için makinede `ansible-playbook` gerekir; yoksa arayüz envanter notunda `UYARI: ansible-playbook bulunamadı; filo işlemleri çalışmaz` satırını gösterir ve komut üretilemez. - Cihaz işlemleri bölümündeki filo işlemleri için makinede `ansible-playbook` gerekir; yoksa arayüz envanter notunda `UYARI: ansible-playbook bulunamadı; filo işlemleri çalışmaz` satırını gösterir ve komut üretilemez.
- Çok özel/tek seferlik işler katalogda yoksa operatör kabuğa döner (kataloğa ekleme PR ile yapılır, `bfos/operations.py`). - Çok özel/tek seferlik işler katalogda yoksa operatör kabuğa döner (kataloğa ekleme PR ile yapılır, `bfos/operations.py`).
- Grafik katalog, terminal konsolunun kapsamının tamamını taşır: 48 işlemin hepsi `Detaylı mod` sekmelerinden erişilebilir (ölçüldü; `Araçlar` sekmesinde 9 işlem, `Cihaz bilgisi` sekmesinde ortam/kimlik/ağ/kaynak/servis ekranları). `sudo bf --tui` kolaylık için vardır, eksik bir yetenek için değil. - Grafik katalog, terminal konsolunun kapsamının tamamını taşır: 48 işlemin hepsi `Detaylı mod` bölümlerinden erişilebilir (ölçüldü; `Araçlar` bölümünde 9 işlem, `Cihaz bilgisi` bölümünde ortam/kimlik/ağ/kaynak/servis ekranları). `sudo bf --tui` kolaylık için vardır, eksik bir yetenek için değil.
- Her onay ayrı bir denetim satırı üretir; komutun ham çıktısı kayda kopyalanmaz (K-14). - Her onay ayrı bir denetim satırı üretir; komutun ham çıktısı kayda kopyalanmaz (K-14).
## 8. Riskler ## 8. Riskler
@ -284,23 +284,26 @@ Yerel ekranda çalıştırmak için oturum açık bir makinede deneyin:
python3 -m bfos.ui_gui python3 -m bfos.ui_gui
``` ```
### 9.2 Arayüzü kullanma — sekme sekme ### 9.2 Arayüzü kullanma — bölüm bölüm
`sudo bf` çalıştırınca **basit mod** açılır. Pencere **koyu bir başlık bandıyla** açılır; bandda solda `Blueforce Field OS — Yönetim` ve `Blueforce saha cihazı yönetimi (Kur / Durum / Bakım)`, ortada **hedef kimliği** (`HEDEF`: `— seçilmedi` / `cihaz BF-12010193 (bf-12010193)` / `dalga 'pilot_1'` / `tüm filo (envanterdeki her cihaz)`), sağda **cihaz durumu** rozeti (`KURULDU` / `KAYITLI` / `HAZIR` / `HATA` / `YOK`) ve **yetki** rozeti (`TAM YETKİ` ya da `SALT-OKUNUR`) durur. Yetki bir durumdur, gömülü bir cümle değildir: yetkisiz açılışta bandın hemen altında turuncu bir şerit aynı bilgiyi tam cümleyle yineler. Bandın altında `HEDEF` paneli (bayi no, dalga, tüm filo), gövde, komut çıktısı ve eylem çubuğu gelir; depo yolu ile kullanılan katalog kaynağı (örn. `bfos.operations + bfos.runner`) bandın içinde sessiz bir satırdır. `sudo bf` çalıştırınca **basit mod** açılır. Pencere **koyu bir başlık bandıyla** açılır; bandda solda `Blueforce Field OS — Yönetim` ve `Blueforce saha cihazı yönetimi (Kur / Durum / Bakım)`, ortada **hedef kimliği** (`HEDEF`: `— seçilmedi` / `cihaz BF-12010193 (bf-12010193)` / `dalga 'pilot_1'` / `tüm filo (envanterdeki her cihaz)`), sağda **cihaz durumu** rozeti (`KURULDU` / `KAYITLI` / `HAZIR` / `HATA` / `YOK`) ve **yetki** rozeti (`TAM YETKİ` ya da `SALT-OKUNUR`) durur. Yetki bir durumdur, gömülü bir cümle değildir: yetkisiz açılışta bandın hemen altında turuncu bir şerit aynı bilgiyi tam cümleyle yineler. Bandın altında `HEDEF` paneli (parçalı kapsam seçici + bağlamsal girdi), gövde, yürütme çekmecesi (başlık + komut çıktısı) ve eylem çubuğu gelir; depo yolu ile kullanılan katalog kaynağı (örn. `bfos.operations + bfos.runner`) bandın içinde sessiz bir satırdır.
**Basit mod (varsayılan):** ekranda `Ne yapmak istiyorsunuz?` sorusu ve üç büyük düğme — `Kur`, `Durum`, `Çıkış`. `Kur` bir seçim penceresi açar: `Ön kontrol (--check)`, `Kurulumu başlat`, `Vazgeç`. `Durum` salt-okunur özeti (`info-summary`) çalıştırır. Daha fazlası için üst çubuktaki **`Detaylı mod`** düğmesine bas. **Basit mod (varsayılan):** ekranda **Genel Bakış** vardır: dört durum kartı — `HEDEF` (büyük kimlik + tür satırı), `CİHAZ DURUMU` (rozet + sözcük), `YETKİ` (rozet + `sudo bf` ipucu), `ENVANTER` (dalga sayısı + ilk dalgalar) — ve altında `Hızlı işlemler` kutusunda üç büyük düğme — `Kur`, `Durum`, `Çıkış`. `Kur` bir seçim penceresi açar: `Ön kontrol (--check)`, `Kurulumu başlat`, `Vazgeç`. `Durum` salt-okunur özeti (`info-summary`) çalıştırır. Yetkisiz oturumda `Kur` kapalıdır ve nedeni düğmenin üstünde yazar. Daha fazlası için üst çubuktaki **`Detaylı mod`** düğmesine bas.
```text ```text
Ne yapmak istiyorsunuz? Genel Bakış
[HEDEF: BF-12010193] [DURUM: HAZIR] [YETKİ: ...] [ENVANTER: 6 dalga]
Hızlı işlemler
[ Kur ] bu cihazı kurar (bayi no gerekir) [ Kur ] bu cihazı kurar (bayi no gerekir)
[ Durum ] salt-okunur özet [ Durum ] salt-okunur özet
[ Çıkış ] pencereyi kapatır [ Çıkış ] pencereyi kapatır
``` ```
**Detaylı mod:** 5 kategori **sekmesi** açılır. Sekmelerin üstünde bir **özet şerit** vardır: büyük rakam işlem sayısıdır (`48 işlem`), yanında `13 yıkıcı` ve `35 salt-okunur` sessiz satırlar olarak durur; yetkisiz oturumda şeridin sağında `yazma işlemleri kapalı` yazar. Her sekmede solda işlem listesi (sütunlar: `İşlem`, `Kapsam`, `Tür`, `Durum`) ve üstünde bir `Süz:` alanı, sağda `SEÇENEKLER VE AÇIKLAMA` paneli vardır; seçtiğin işlemin kapsamı, playbook yolu ve varsa zorunlu alanları orada görünür. `Tür` sütunu yıkıcı işlemlerde kırmızı `YIKICI`, diğerlerinde `salt-okunur` yazar; `Durum` sütunu işlemin çalıştırılabilir olduğunu (`açık`) ya da kapalı olduğunu (`kapalı — sudo bf`) söyler. Eylem çubuğu iki satırdır: üstte `Komutu göster ve çalıştır` (yıkıcı işlem seçiliyse kırmızı, yetkisizse kapalı ve nedeni yanında yazılı) ve sağında durum göstergesi (`HAZIR` / `ÇALIŞIYOR` / `TAMAM` / `HATA` — sözcük + renk); altta `Çıktıyı temizle`, `Çalışanı iptal et`, `Yalnız denetim (--check)`, `Çıktı panelini gizle` ve `Çıkış`. **Detaylı mod:** solda bir **kenar çubuğu** açılır: beş katalog bölümü (adetleriyle — `Kurulum`, `Cihaz işlemleri`, `Merkez hazırlığı`, `Araçlar`, `Cihaz bilgisi`) ve `Etkinlik` sayfası. Bölümlerin üstünde bir **özet şerit** vardır: büyük rakam işlem sayısıdır (`48 işlem`), yanında `13 yıkıcı` ve `35 salt-okunur` sessiz satırlar olarak durur; yetkisiz oturumda şeridin sağında `yazma işlemleri kapalı` yazar. Her bölümde solda **işlem tarayıcı** (üstte `Süz:` alanı; satırlar: ikon + ad + tek satır açıklama + tür ve durum rozetleri + ok), sağda **`İŞLEM AYRINTISI`** paneli vardır: başlık, açıklama, `HEDEF`/`KAPSAM`/`YETKİ`/`TÜR` ızgarası, koşullu uyarılar, seçenek formu, katlanır **komut önizlemesi** (`Önizlemeyi oluştur` düğmesi komutu gerçek backend ile üretir, çalıştırmaz) ve birincil `Komutu göster ve çalıştır` düğmesi. Rozetler: türde yıkıcı işlemlerde kırmızı `YIKICI`, diğerlerinde `salt-okunur`; durumda `açık` ya da `kapalı — sudo bf`. Yazma işlemleri listede kalır, solar, nedenini yazar — gizlenmez. `Etkinlik` sayfası bu oturumda biten çalışmaları (zaman, işlem, hedef, sonuç) listeler; sır göstermez. Eylem çubuğu iki satırdır: üstte `Komutu göster ve çalıştır` (yıkıcı işlem seçiliyse kırmızı, yetkisizse kapalı ve nedeni yanında yazılı) ve sağında durum göstergesi (`HAZIR` / `ÇALIŞIYOR` / `TAMAM` / `HATA` — sözcük + renk); altta `Çıktıyı temizle`, `Çalışanı iptal et`, `Yalnız denetim (--check)`, `Çıktı panelini gizle` ve `Çıkış`. Çıktı panelinin üstünde **yürütme başlığı** vardır: boşken `BOŞTA` + `Etkin işlem yok.`, çalışırken `ÇALIŞIYOR` + `işlem · hedef` + geçen süre, bitince `TAMAM`/`HATA` + süre + çıkış kodu.
| Sekme | İşlem sayısı | Yazma (yıkıcı) | Salt-okunur | | Bölüm | İşlem sayısı | Yazma (yıkıcı) | Salt-okunur |
|---|---|---|---| |---|---|---|---|
| **Kurulum** | 6 | 3 (`Bu cihazı kur`, `Başarısız kuruluma devam et`, `Tek modül çalıştır`) | 3 (ön kontrol, modül durumu, kurulum günlüğü) | | **Kurulum** | 6 | 3 (`Bu cihazı kur`, `Başarısız kuruluma devam et`, `Tek modül çalıştır`) | 3 (ön kontrol, modül durumu, kurulum günlüğü) |
| **Cihaz işlemleri** | 21 | 9 (servis yeniden başlatmalar, grafik oturumu aç/kapat, betik, reboot, paket güncellemesi) | 12 (11 filo raporu + log toplama) | | **Cihaz işlemleri** | 21 | 9 (servis yeniden başlatmalar, grafik oturumu aç/kapat, betik, reboot, paket güncellemesi) | 12 (11 filo raporu + log toplama) |
@ -309,7 +312,7 @@ Yerel ekranda çalıştırmak için oturum açık bir makinede deneyin:
| **Cihaz bilgisi** | 8 | 0 | 8 | | **Cihaz bilgisi** | 8 | 0 | 8 |
| **Toplam** | **48** | **13** | **35** | | **Toplam** | **48** | **13** | **35** |
Sekme sekme hangi işler var: Bölüm bölüm hangi işler var:
**1) Kurulum (bu cihaz)** — yalnız **senin oturduğun makineyi** kurar; hiçbir filo cihazına dokunmaz. Üst çubukta **bayi no (8 hane)** dolu olmalıdır; boşsa `Komutu göster ve çalıştır` bir uyarı penceresi açar. **1) Kurulum (bu cihaz)** — yalnız **senin oturduğun makineyi** kurar; hiçbir filo cihazına dokunmaz. Üst çubukta **bayi no (8 hane)** dolu olmalıdır; boşsa `Komutu göster ve çalıştır` bir uyarı penceresi açar.
@ -368,7 +371,7 @@ Kurulum seçenekleri: `Yerel çevrimdışı depodan kur (--offline)` ve `Zaten O
| 3 | Yapılandırılmış merkez uç noktaları | WireGuard hub, izleme ve depo anahtarları (**secret gösterilmez**) | | 3 | Yapılandırılmış merkez uç noktaları | WireGuard hub, izleme ve depo anahtarları (**secret gösterilmez**) |
| 4 | Merkez hazırlık kontrol listesi | Kayıttan önce merkezde var olması gerekenler | | 4 | Merkez hazırlık kontrol listesi | Kayıttan önce merkezde var olması gerekenler |
> `--tui` menüsünde bu sekmenin karşılığında ayrıca "Merkez hazırlık durumu", "uç noktaları dene" (DNS+TCP) ve "dalga geçiş kapısı durumu" ekranları vardır. > `--tui` menüsünde bu bölümün karşılığında ayrıca "Merkez hazırlık durumu", "uç noktaları dene" (DNS+TCP) ve "dalga geçiş kapısı durumu" ekranları vardır.
**4) Araçlar** — repo ve yerel yardımcılar. Yalnız `Bu kopyayı güncelle (git pull --ff-only)` yazma işlemidir; kalan 8'i salt-okunurdur: `İşlem kataloğunu göster (--list)`, `Yerel çevrimdışı kapısı (bf-check-local)`, `Kayıt kapısı (bf-check-enrollment)`, `Teslim kapısı (bf-check-ready)`, `Derin teşhis (bf-diagnostics)`, `Field OS sürüm kimliği (bf-release)`, `Destek paketi oluştur`, `Konsol denetim günlüğü (son 40 satır)`. **4) Araçlar** — repo ve yerel yardımcılar. Yalnız `Bu kopyayı güncelle (git pull --ff-only)` yazma işlemidir; kalan 8'i salt-okunurdur: `İşlem kataloğunu göster (--list)`, `Yerel çevrimdışı kapısı (bf-check-local)`, `Kayıt kapısı (bf-check-enrollment)`, `Teslim kapısı (bf-check-ready)`, `Derin teşhis (bf-diagnostics)`, `Field OS sürüm kimliği (bf-release)`, `Destek paketi oluştur`, `Konsol denetim günlüğü (son 40 satır)`.
@ -378,20 +381,21 @@ Kurulum seçenekleri: `Yerel çevrimdışı depodan kur (--offline)` ve `Zaten O
### 9.3 Hedef seçimi: tek cihaz mı dalga mı ### 9.3 Hedef seçimi: tek cihaz mı dalga mı
Filo işlemlerinde hedef **üst çubuktan** seçilir; işlem seçildiğinde ekran hedefi ayrıca sormaz, çubuktaki değeri kullanır: Filo işlemlerinde hedef **üst çubuktaki parçalı seçiciden** verilir; üç kapsam birbirini dışlar (`[Cihaz | Dalga | Tüm filo]`), görünür kapsam tektir ve işlem seçildiğinde ekran hedefi ayrıca sormaz, seçicideki değeri kullanır:
```text ```text
HEDEF [ Detaylı mod ] HEDEF [ Detaylı mod ]
Bayi no (8 hane) [ 12010193 ] Dalga [ pilot_1 ▾ ] [ ] Tüm filo (dikkat) [ Cihaz ] [ Dalga ] [ Tüm filo ]
Bayi no (8 hane) [ 12010193 ]
Bayi no geçerli: BF-12010193 Bayi no geçerli: BF-12010193
``` ```
| Hedef türü | Çubukta ne yaparsın | Örnek | Boyut | Tipik kullanım | | Hedef türü | Seçicide ne yaparsın | Örnek | Boyut | Tipik kullanım |
|---|---|---|---|---| |---|---|---|---|---|
| Tek cihaz | Bayi no alanına 8 hane yaz | `12010193` | 1 | Arıza teşhisi, tek cihaz bakımı | | Tek cihaz | `Cihaz` parçasını seç, bayi no alanına 8 hane yaz | `12010193` | 1 | Arıza teşhisi, tek cihaz bakımı |
| Dalga / grup | `Dalga` açılır listesinden bir dalga seç | `lab` | 2 | Yeni playbook denemesi (önce LAB kuralı) | | Dalga / grup | `Dalga` parçasını seç, listeden bir dalga seç | `lab` | 2 | Yeni playbook denemesi (önce LAB kuralı) |
| Dalga / grup | `Dalga` listesi | `pilot_1` / `pilot_2` / `wave_1` / `wave_2` | 5 / 20 / 50 / 100 | Kademeli yayılım (K-11) | | Dalga / grup | `Dalga` parçası | `pilot_1` / `pilot_2` / `wave_1` / `wave_2` | 5 / 20 / 50 / 100 | Kademeli yayılım (K-11) |
| Tüm filo | `Tüm filo (dikkat)` kutusunu işaretle | — | kalan | Yalnız onaylı; dalga dalga ilerler | | Tüm filo | `Tüm filo` parçasını seç (şerit açılır) | — | kalan | Yalnız onaylı; dalga dalga ilerler |
Bayi no alanına yazdıkça anında doğrulanır; geçerliyse durum satırı `Bayi no geçerli: BF-12010193` yazar, geçersizse kırmızı uyarı görünür (örn. `Geçersiz bayi numarası '1201019': tam 8 hane olmalıdır (verilen uzunluk: 7). Örnek: 12010193`). Bayi no alanına yazdıkça anında doğrulanır; geçerliyse durum satırı `Bayi no geçerli: BF-12010193` yazar, geçersizse kırmızı uyarı görünür (örn. `Geçersiz bayi numarası '1201019': tam 8 hane olmalıdır (verilen uzunluk: 7). Örnek: 12010193`).
@ -474,13 +478,13 @@ Pencere açılır: `Blueforce Field OS — Yönetim` / `Blueforce saha cihazı y
[OK] Komut başarıyla tamamlandı (çıkış kodu 0). [OK] Komut başarıyla tamamlandı (çıkış kodu 0).
``` ```
Kurulum sonunda cihaz `PROVISIONED_OFFLINE` olur; **`READY` değildir** — merkeze kayıt (enrollment) ve merkezi kanal kanıtı gerekir (K-21). Bunu görmek için `Cihaz bilgisi` sekmesinden `Kurulum/kayıt durumu (state.json beyaz liste)` ya da `Durum özeti` açılır. Kurulum sonunda cihaz `PROVISIONED_OFFLINE` olur; **`READY` değildir** — merkeze kayıt (enrollment) ve merkezi kanal kanıtı gerekir (K-21). Bunu görmek için `Cihaz bilgisi` bölümünden `Kurulum/kayıt durumu (state.json beyaz liste)` ya da `Durum özeti` açılır.
**Seans B — Günlük kontrol (salt-okunur rapor) ve teşhis paketi** **Seans B — Günlük kontrol (salt-okunur rapor) ve teşhis paketi**
```text ```text
Üst çubuk: Hedef: [ 12010193 ] Dalga [ pilot_1 ▾ ] Üst çubuk: Hedef: [ 12010193 ] Dalga [ pilot_1 ▾ ]
Detaylı mod → Cihaz işlemleri sekmesi → listeden "Erişilebilirlik testi (ping)" satırını seç Detaylı mod → Cihaz işlemleri bölümü → tarayıcıdan "Erişilebilirlik testi (ping)" satırını seç
Alt çubuk: [ Komutu göster ve çalıştır ] Alt çubuk: [ Komutu göster ve çalıştır ]
``` ```
@ -505,7 +509,7 @@ Onay penceresi:
[OK] Komut başarıyla tamamlandı (çıkış kodu 0). [OK] Komut başarıyla tamamlandı (çıkış kodu 0).
``` ```
Teşhis paketi için: `Araçlar` sekmesi → `Destek paketi oluştur`. Paket yolu çıktı alanında yazılır ve paket **parola, private key veya token içermez** (K-14). Denetim kaydını görmek için aynı sekmedeki `Konsol denetim günlüğü (son 40 satır)` açılır. Teşhis paketi için: `Araçlar` bölümü → `Destek paketi oluştur`. Paket yolu çıktı alanında yazılır ve paket **parola, private key veya token içermez** (K-14). Denetim kaydını görmek için aynı bölümdeki `Konsol denetim günlüğü (son 40 satır)` açılır.
### 9.5 Kimlik bilgileri / şifre dosyası (opsiyonel — varsayılan KAPALI) ### 9.5 Kimlik bilgileri / şifre dosyası (opsiyonel — varsayılan KAPALI)
@ -528,10 +532,10 @@ Teşhis paketi için: `Araçlar` sekmesi → `Destek paketi oluştur`. Paket yol
| # | Belirti (ekranda ne görürsün) | Neden | Çözüm | | # | Belirti (ekranda ne görürsün) | Neden | Çözüm |
|---|---|---|---| |---|---|---|---|
| 1 | `bf: command not found` | Bootstrap çalışmamış veya araçlar bağlanmamış | Bootstrap'ı tekrar koş: `sudo admin/bf-bootstrap.sh`; sonra tam yoldan dene: `sudo /usr/local/bin/bf`. Python'suz cihazda yedek: `sudo bf-menu` | | 1 | `bf: command not found` | Bootstrap çalışmamış veya araçlar bağlanmamış | Bootstrap'ı tekrar koş: `sudo admin/bf-bootstrap.sh`; sonra tam yoldan dene: `sudo /usr/local/bin/bf`. Python'suz cihazda yedek: `sudo bf-menu` |
| 2 | Başlık bandında turuncu `SALT-OKUNUR` rozeti ve bandın altında turuncu şerit: `Yönetici yetkisi yok: salt-okunur raporlar çalışır, kurulum ve değişiklik gerektiren işlemler kapalıdır. Tam yetki için: sudo bf` | Arayüz `sudo` olmadan açıldı; yazma işlemleri kapalı (listede kalır, `Durum` sütunu `kapalı — sudo bf` yazar) | Pencereyi kapat ve `sudo bf` ile aç (§9.8) | | 2 | Başlık bandında turuncu `SALT-OKUNUR` rozeti ve bandın altında turuncu şerit: `Yönetici yetkisi yok: salt-okunur raporlar çalışır, kurulum ve değişiklik gerektiren işlemler kapalıdır. Tam yetki için: sudo bf` | Arayüz `sudo` olmadan açıldı; yazma işlemleri kapalı (listede kalır, durum rozeti `kapalı — sudo bf` yazar) | Pencereyi kapat ve `sudo bf` ile aç (§9.8) |
| 3 | Hata penceresi: `Geçersiz bayi numarası '1201019': tam 8 hane olmalıdır (verilen uzunluk: 7). Örnek: 12010193` | Bayi no 8 haneli ve yalnız rakam olmalı | Tam 8 hane, sadece rakam gir; baştaki sıfırı silme. Örnek: `12010193` | | 3 | Hata penceresi: `Geçersiz bayi numarası '1201019': tam 8 hane olmalıdır (verilen uzunluk: 7). Örnek: 12010193` | Bayi no 8 haneli ve yalnız rakam olmalı | Tam 8 hane, sadece rakam gir; baştaki sıfırı silme. Örnek: `12010193` |
| 4 | Envanter notunda `UYARI: ansible-playbook bulunamadı; filo işlemleri çalışmaz (sudo apt install ansible).` | Makinede Ansible yok | `sudo apt install ansible` (reponun kurulumu `ansible-core`'u zaten kurar) ve araçları bağla: `sudo admin/bf-bootstrap.sh` | | 4 | Envanter notunda `UYARI: ansible-playbook bulunamadı; filo işlemleri çalışmaz (sudo apt install ansible).` | Makinede Ansible yok | `sudo apt install ansible` (reponun kurulumu `ansible-core`'u zaten kurar) ve araçları bağla: `sudo admin/bf-bootstrap.sh` |
| 5 | `Komut üretilemedi`: `Playbook bulunamadı: ansible/playbooks/...` veya `... konsol kapsamı 'X' beklerken playbook 'Y' beyan ediyor.` | Katalog ile repo checkout'u uyuşmuyor (eski checkout veya `--repo` yanlış) | `Araçlar` sekmesinden `Bu kopyayı güncelle (git pull --ff-only)` veya doğru repoyu ver: `sudo bf --repo /opt/blueforce-linux`; arayüzü yeniden başlat | | 5 | `Komut üretilemedi`: `Playbook bulunamadı: ansible/playbooks/...` veya `... konsol kapsamı 'X' beklerken playbook 'Y' beyan ediyor.` | Katalog ile repo checkout'u uyuşmuyor (eski checkout veya `--repo` yanlış) | `Araçlar` bölümünden `Bu kopyayı güncelle (git pull --ff-only)` veya doğru repoyu ver: `sudo bf --repo /opt/blueforce-linux`; arayüzü yeniden başlat |
Ek olarak sık görülen dört durum: Ek olarak sık görülen dört durum:

View file

@ -0,0 +1,182 @@
# 34 — Konsol Görsel Konsepti (Uygulanan Tasarım)
## 1. Amaç
Operatör konsolunun (`bfos/ui_gui.py` + `bfos/ui_widgets.py`) görsel
dünyasını tek belgede sabitlemek: uygulama kabuğu, Genel Bakış, hedef
seçici, işlem tarayıcı, ayrıntı paneli, yürütme çekmecesi ve güvenlik
pencereleri. Kavram `docs/33-OPERATOR-CONSOLE.md` §9.2'nin uygulanan
hâlini anlatır; davranış sözleşmesini değiştirmez.
## 2. Kapsam
Yalnızca sunum katmanı: koyu başlık bandı, hedef paneli, kenar çubuğu,
sayfalar, rozetler, diyaloglar. Renk/boşluk/yazı değerleri
`bfos/ui_theme.py` üzerinden `web/static/tokens.css` aynasından gelir;
bu belgede renk kodu yoktur. Backend kuralları (`bfos/operations.py`,
`bfos/runner.py`), kapılar ve denetim kaydı kapsam dışıdır — aynen korunur.
## 3. Kararlar
1. **Uygulama kabuğu:** başlık bandı + hedef paneli her modda sabittir;
detaylı modda solda 220px kenar çubuğu (5 kategori + Etkinlik), sağda
sayfa alanı, altta yürütme çekmecesi ve eylem çubuğu.
2. **Genel Bakış ilk ekrandır:** 4 durum kartı (Hedef / Cihaz durumu /
Yetki / Envanter) + 3 hızlı işlem (Kur / Durum / Çıkış). Form yok.
3. **Hedef seçici parçalıdır:** `[Cihaz | Dalga | Tüm filo]`; görünür
kapsam tektir, girdiler bağlama duyarlıdır, tüm filo yüksek etkili
uyarı şeridi açar.
4. **İşlem tarayıcı satırlıdır:** ikon + ad + tek satır açıklama +
tür rozeti + durum rozeti + ok. Engelli işlem listede kalır, solar,
nedenini yazar.
5. **Ayrıntı paneli:** başlık, açıklama, HEDEF/KAPSAM/YETKİ/TÜR ızgarası,
koşullu uyarılar, seçenek formu, istek üzerine gerçek backend ile
üretilen komut önizlemesi, birincil eylem.
6. **Onay penceresi özeldir:** İŞLEM/HEDEF/KAPSAM/ETKİ bloğu + monospace
komut + `[Vazgeç]` / işlem adlı düğme. Yıkıcıda odak Vazgeç'tedir.
7. **Yürütme çekmecesi:** durum rozeti + `işlem · hedef · süre`; log
kuyusu salt-okunur kanıttır. Bitiş TAMAM/HATA + süre + çıkış kodudur.
8. **Etkinlik sayfası:** yalnız bu oturumun biten çalışmaları (zaman,
işlem, hedef, sonuç). Sır gösterilmez.
## 4. Neden Bu Karar?
Operatör 3 saniyede altı soruya yanıt almalıdır: hedef ne, durumu ne,
yetkim ne, ne yapabilirim, sorun var mı, bir şey çalışıyor mu? Eski
sekme+tablo düzeni bu soruları form kalabalığına gömüyordu. Kabuk her
soruya sabit bir yer verir; güvenlik bilgisi (kapsam, yetki, yıkıcılık)
her ekranda aynı görsel dilde yinelenir.
## 5. Alternatifler
- **Sekmeler yerinde kalsın:** reddedildi — 5 sekme + tablo, 48 işlemi
ayırt edilemez kılar ve Genel Bakış'a yer bırakmaz.
- **Web/Electron kabuğu:** reddedildi — çevrimdışı kurulum, `sudo bf`
ile başlatma ve `python3-tk` bağımlılığı korunmalıdır.
- **Koyu tema:** ertelendi — atölye ışığında açık zemin okunur; token
yapısı koyu temaya hazırdır ama öncelik açık temadır.
## 6. Avantajlar
- Kapsam her ekranda görünür (başlık + hedef paneli + onay penceresi).
- Yazma işlemleri gizlenmez, kapatılır ve nedeni yazılır (yetki öğretir).
- Tehlike stili yalnız tehlikeli eylemdedir; panel kırmızıya boyanmaz.
- Tüm simgeler Tk canvas çizimidir: çevrimdışı, lisanssız, varlıksız.
- Klavye: sekme sırası görsel sırayı izler, satırlar odaklanabilir,
`Enter` çalıştırır, `Esc` diyalogu kapatır (yıkıcıda asla onaylamaz).
## 7. Dezavantajlar
- Özel satır bileşeni `ttk.Treeview` yerleşik sıralamasından vazgeçer:
sütun sıralaması yoktur (arama + kategori yeterlidir).
- Ayrıntıdaki hedef satırı, seçenekli işlemlerde hedef değişince otomatik
tazelenmez (girdi korunur); güncel hedef çalışma anı penceresinde
gösterilir.
## 8. Riskler
- **Kapsam yanılgısı:** parçalı denetim + başlık aynası + onaydaki kapsam
şeridi üç katmanlı savunmadır; `current_target()` görünür kapsama göre
hedef üretir (bayi metni dalgayı ezemez).
- **Eski alışkanlık:** sekmeleri arayan operatör için §9.2 haritası ve
Genel Bakış'taki yönlendirme metni geçişi anlatır.
## 9. Uygulama Planı
### 9.1 Kabuk iskeleti
```text
+---------------------------------------------------------------+
| koyu bant: ürün | hedef | cihaz durumu | yetki |
+---------------------------------------------------------------+
| şerit (salt-okunur / deneme kipi; etkinse) |
+---------------------------------------------------------------+
| HEDEF: [Cihaz|Dalga|Tüm filo] + bağlamsal girdi |
+----------------+----------------------------------------------+
| kenar çubuğu | sayfa (tarayıcı + ayrıntı / etkinlik) |
| +----------------------------------------------+
| | yürütme çekmecesi (başlık + log kuyusu) |
+---------------------------------------------------------------+
| eylem çubuğu: [birincil] neden ... durum / araçlar |
+---------------------------------------------------------------+
```
### 9.2 Yedi ekran
1. **Genel Bakış** (basit mod): 4 kart + Kur/Durum/Çıkış. Yetkisizde Kur
kapalı, nedeni düğmede.
2. **Hedef seçimi:** Cihaz → 8 hane doğrulamalı giriş; Dalga → salt-okunur
liste (`production` seçilince jeton notu); Tüm filo → tehlike şeridi.
3. **İşlem tarayıcı:** arama + satırlar (ikon, ad, açıklama, `YIKICI` /
`salt-okunur`, `açık` / `kapalı — sudo bf`, ok). Seçili satır aksan
dolgulu.
4. **Ayrıntı:** meta ızgara + uyarılar + seçenekler + katlanır komut
önizlemesi + birincil düğme (yıkıcıda kırmızı, engellide kapalı).
5. **Çalışma:** `ÇALIŞIYOR` rozeti + ad + hedef + `mm:ss` + iptal; bitiş
`TAMAM`/`HATA` + süre + çıkış kodu; kayıt Etkinlik'e düşer.
6. **Yıkıcı onay:** İŞLEM/HEDEF/KAPSAM/ETKİ + filo/üretim şeridi +
açık önizleme + `[Vazgeç]` (odak) / işlem adlı kırmızı düğme.
7. **Salt-okunur:** başlık rozeti + turuncu şerit; tüm yazma düğmeleri
kapalı + gerekçeli; `_start_run` kapıyı yeniden denetler.
### 9.3 Token ve yazı
Zemin `--bg-app`, paneller beyaz, tek koyu bant; vurgu mavisi yalnız
birincil eylem + seçili satır. Başlık 22, bölüm 18, gövde 15, ikincil 13,
üstveri 12, veri monospace 13. Ritim 4/8/12/16/24/32/48. Kenar çubuğu
220px, satır 44px, denetim 36px. Durum söz + renk + rozet (renk yalnız
asla).
## 10. Test Planı
- `tests/test_bfos.py` (155) ve `tests/test_bfos_gui_static.py` (95):
pencere açmaz; katalog, kapılar, önizleme, renk aynası sözleşmeleri.
- Canlı QA (`DISPLAY=:0`): `/tmp/bf-qa.py` geometri taraması (0 hatalı
öğe), 6 sayfa × tarayıcı/ayrıntı, hedef akışları, `import -window`
yakalamaları + Tesseract OCR metin denetimi.
- Mutasyon kanıtı: `ui_widgets.py` içine renk sayesi → renk testi
kırmızıya döner; geri alınır.
- `bash tests/run-all.sh` 12/12; `python3 -m bfos --tui` duman testi.
## 11. Rollback
Sunum katmanıdır: `bfos/ui_gui.py`, `bfos/ui_widgets.py`,
`docs/33`, `docs/34` ve bir test iddiası değişir. Geri alma = bu dosyaları
önceki sürüme döndürmek; backend, envanter ve denetim kaydı etkilenmez.
Commitlenmeden çalışılır; değişiklikler çalışma ağacında durur.
## 12. Kontrol Listesi
- [x] Kabuk + Genel Bakış + hedef seçici + tarayıcı + ayrıntı + çekmece
- [x] Onay/jeton pencereleri (odak ve varsayılan güvenli)
- [x] Salt-okunur sunumu (gizleme yok, gerekçe her yerde)
- [x] Etkinlik sayfası (oturum içi, sırsız)
- [x] Token/sözdizimi aynası (`tokens.css` ↔ `ui_theme.py`) korunur
- [x] Türkçe metin taraması (teknik adlar dışında İngilizce yok)
- [x] Ekran görüntüleri (`/tmp/bf-shots/r2-*`, `r3-*`, `r4-*`)
- [ ] Vision servisiyle görsel inceleme (servis 500 veriyor; OCR ile
doğrulandı, insan gözüyle bakılmadı)
## 13. Açık Sorular
- Koyu tema gerekli mi? (Token yapısı hazır; talep yok.)
- Etkinlik sayfası denetim günlüğünden güvenli özet okumalı mı?
(Bugün yalnız oturum içi; güvenlik garantisi değişmeden olmaz.)
## Ek: Kabuk durum akışı
```mermaid
flowchart TD
A[Genel Bakış] -->|Detaylı mod| B[Kenar çubuğu: 5 kategori + Etkinlik]
B --> C[İşlem tarayıcı]
C --> D[İşlem ayrıntısı]
D --> E[Komut önizlemesi]
E --> F{Onay penceresi}
F -->|Vazgeç| D
F -->|Onayla| G[Kapı jetonları]
G -->|iptal| D
G -->|tamam| H[Kesin komut onayı]
H -->|Vazgeç| D
H -->|Onayla| I[Yürütme çekmecesi: ÇALIŞIYOR]
I --> J[TAMAM / HATA + Etkinlik kaydı]
```

View file

@ -1050,6 +1050,10 @@ class TestConsoleLayoutContract(StaticCase):
def test_the_view_has_no_hardcoded_colour(self) -> None: def test_the_view_has_no_hardcoded_colour(self) -> None:
"""Renk token'dir, literal degil (docs/DESIGN.md kural 1).""" """Renk token'dir, literal degil (docs/DESIGN.md kural 1)."""
self.assertEqual(re.findall(r"#[0-9a-fA-F]{3,8}\b", self.source), []) self.assertEqual(re.findall(r"#[0-9a-fA-F]{3,8}\b", self.source), [])
widgets_path = REPO_ROOT / "bfos" / "ui_widgets.py"
widgets_source = widgets_path.read_text(encoding="utf-8")
self.assertEqual(
re.findall(r"#[0-9a-fA-F]{3,8}\b", widgets_source), [])
def test_there_is_exactly_one_exit_control(self) -> None: def test_there_is_exactly_one_exit_control(self) -> None:
"""'Cik' tek kontrol: ya basit modda ya alt cubukta, ayni anda degil.""" """'Cik' tek kontrol: ya basit modda ya alt cubukta, ayni anda degil."""