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:
parent
b6a14ca8e0
commit
2bad7fbb7f
5 changed files with 1574 additions and 274 deletions
1071
bfos/ui_gui.py
1071
bfos/ui_gui.py
File diff suppressed because it is too large
Load diff
489
bfos/ui_widgets.py
Normal file
489
bfos/ui_widgets.py
Normal 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"]
|
||||
|
|
@ -15,13 +15,13 @@
|
|||
|
||||
## 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
|
||||
|
||||
- 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).
|
||||
|
||||
## 3. Kararlar
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
```text
|
||||
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,
|
||||
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
|
||||
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
|
||||
|
|
@ -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.
|
||||
- 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`).
|
||||
- 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).
|
||||
|
||||
## 8. Riskler
|
||||
|
|
@ -284,23 +284,26 @@ Yerel ekranda çalıştırmak için oturum açık bir makinede deneyin:
|
|||
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
|
||||
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)
|
||||
[ Durum ] salt-okunur özet
|
||||
[ Çı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üğü) |
|
||||
| **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 |
|
||||
| **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.
|
||||
|
||||
|
|
@ -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**) |
|
||||
| 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)`.
|
||||
|
||||
|
|
@ -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ı
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
| 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ı |
|
||||
| Dalga / grup | `Dalga` açılır listesinden 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) |
|
||||
| Tüm filo | `Tüm filo (dikkat)` kutusunu işaretle | — | kalan | Yalnız onaylı; dalga dalga ilerler |
|
||||
| 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` parçasını seç, listeden bir dalga seç | `lab` | 2 | Yeni playbook denemesi (önce LAB kuralı) |
|
||||
| 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` 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`).
|
||||
|
||||
|
|
@ -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).
|
||||
```
|
||||
|
||||
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**
|
||||
|
||||
```text
|
||||
Ü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 ]
|
||||
```
|
||||
|
||||
|
|
@ -505,7 +509,7 @@ Onay penceresi:
|
|||
[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)
|
||||
|
||||
|
|
@ -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 |
|
||||
|---|---|---|---|
|
||||
| 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` |
|
||||
| 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:
|
||||
|
||||
|
|
|
|||
182
docs/34-CONSOLE-VISUAL-CONCEPT.md
Normal file
182
docs/34-CONSOLE-VISUAL-CONCEPT.md
Normal 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ı]
|
||||
```
|
||||
|
|
@ -1050,6 +1050,10 @@ class TestConsoleLayoutContract(StaticCase):
|
|||
def test_the_view_has_no_hardcoded_colour(self) -> None:
|
||||
"""Renk token'dir, literal degil (docs/DESIGN.md kural 1)."""
|
||||
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:
|
||||
"""'Cik' tek kontrol: ya basit modda ya alt cubukta, ayni anda degil."""
|
||||
|
|
|
|||
Loading…
Reference in a new issue