Compare commits

..

No commits in common. "b6a14ca8e06da7933fa34087044d3365703bbcf8" and "d2672eb776709238bead7de2f32c206a8e3a337f" have entirely different histories.

1375 changed files with 213 additions and 10930 deletions

17
.gitignore vendored
View file

@ -40,20 +40,15 @@ packages.lock.tsv
!provisioning/offline-repo/built/ !provisioning/offline-repo/built/
!provisioning/offline-repo/built/** !provisioning/offline-repo/built/**
# #
# The bundle's pool/ IS committed, deliberately. blueforce-install.sh resolves # Except the bundle's own pool/ copy of the .deb files: those packages are
# every pinned package as "<bundle>/pool/<source>" (its lock rows carry # already tracked once under provisioning/offline-repo/packages/, and the same
# "pool/<file>.deb"), so a device that clones the repository needs those files # ~890 MB does not need a second path in the repository. What the installer
# on disk. Excluding them left the installer with an index but no packages and # needs from this directory is the index (Packages, Packages.gz,
# it stopped with "Kilitte yazılı paket depoda yok". The same blobs also live # packages.lock.tsv, SHA256SUMS) plus repo-manifest.yaml.
# under packages/, which is the intake directory the maintainer commits from; provisioning/offline-repo/built/pool/
# Git LFS stores one object per content hash, so the duplication is a path, not
# a second copy of the bytes.
# Documentation site (generated) # Documentation site (generated)
docs-site/node_modules/ docs-site/node_modules/
# Design tooling scratch (local to a workstation, not shippable)
.impeccable/
docs-site/build/ docs-site/build/
docs-site/.docusaurus/ docs-site/.docusaurus/
docs-site/docs/ docs-site/docs/

View file

@ -1,69 +0,0 @@
# Product
<!-- impeccable:product-schema 1 -->
## Platform
web
Two surfaces share this record. The **central panel** is a web surface (browser, server-rendered). The **device console** is a native desktop surface: a Python/tkinter application that ships on each field PC and runs with no network. Visual work on the device console follows the native-platform guidance for its own platform; this value names the surface under design here.
## Stack
Python standard library only (`http.server` + server-rendered templates). No framework and no third-party dependency: the fleet installs offline from a committed package pool, and `fastapi`/`uvicorn`/`flask` are not in that pool. Adding one would mean the panel cannot be installed on the devices it manages. Confirmed with the user over FastAPI for this reason.
## Users
**Field technician** — a dealer/authorised service person who receives a Windows 10 PC and must turn it into a Blueforce Field OS device. Works at the device, often in a workshop or a customer site, with limited time and no developer background. Job: run the install, know whether it succeeded, and recover when it did not.
**Central operator** — the person responsible for the whole fleet (~700 devices) from an office browser. Job: see which devices are installed or not, which rollout wave each is in, and push an approved update. Handles many devices per session and needs status, not detail.
Both operate the same fleet from opposite ends: the technician from the device, the operator from the browser.
## Product Purpose
Convert a fleet of Windows 10 field PCs into machines running Blueforce Field OS, and keep them correct afterwards. Success means a technician finishes an install without internet access, without guessing, and without a silent partial state — and an operator can say at any moment which devices are ready and which are not.
## Positioning
The install is provably offline and fail-closed. The device carries its own signed package pool, the installer refuses to continue when the contract does not hold rather than half-installing, and every destructive step demands explicit approval with a root gate. Neither property can be copied by a neighbouring tool that assumes internet access or treats a failed check as a warning.
## Operating Context
- Fleet: ~700 Windows 10 field PCs, converted in waves LAB(2) → P1(5) → P2(20) → W1(50) → W2(100) → PROD, production behind a separate typed override.
- Install target: Ubuntu 26.04.1 LTS ("Resolute Raccoon"), GNOME desktop installed but not the boot default — the device boots to a terminal and the desktop is switched on when required.
- Network: the field estate is reached over WireGuard; four remote channels are supported (SSH-over-WG, xRDP, RustDesk OSS, MeshCentral). No commercial remote-desktop licence (AnyDesk was rejected).
- Fleet tooling: Ansible CLI + Semaphore Community for orchestration, Prometheus + Node Exporter + Grafana OSS + Uptime Kuma for monitoring, Docusaurus for documentation.
- Device state machine: `PROVISIONED_OFFLINE → ENROLLED → READY`, with no skipping and no going back; `READY` is refused unless every channel reports `verified`.
- Documentation set: 33 numbered Turkish documents plus README/SUMMARY, served through Docusaurus.
## Capabilities and Constraints
**Confirmed functionality** — the device console exposes 48 operations: 13 write (all requiring root) and 35 read-only. The installer is `scripts/install/blueforce-install.sh`, driven by an offline bundle at `provisioning/offline-repo/built/` (index + `pool/` of 1321 `.deb` files, ~887 MB). Evidence of fleet state is an inventory and a enrolment token per device.
**Hard constraints** — no internet during install or normal operation; every user-visible string is Turkish (code, comments and commit messages stay English); destructive operations need an explicit typed confirmation and a root gate; a check that fails stops the operation, it never degrades to a warning.
**Deliberately undecided** — where the central panel is hosted and how it authenticates to the fleet. Not invented here: the panel is being designed before its deployment model is fixed.
## Brand Commitments
Name: **Blueforce Field OS**. Existing operator documentation and console copy are Turkish and already carry the product voice — plain, factual, no marketing register. No logo or palette is binding; no visual identity exists yet.
## Evidence on Hand
- `docs/` — 33 numbered Turkish documents, including `01-ARCHITECTURE`, `24-DECISION-LOG` (K-01…K-14), `27-OFFLINE-PROVISIONING`, `33-OPERATOR-CONSOLE`.
- `bfos/` — the device console (`operations.py` catalogue of 48 operations, `runner.py`, `tui.py`, `ui_gui.py`).
- `provisioning/offline-repo/` — the committed package pool and its manifests.
- No analytics, no customer logos, no testimonials, no benchmark numbers, and no screenshots of a working web panel exist. Nothing of the sort may be fabricated.
## Product Principles
1. **Fail closed, never half-install.** A device that cannot be verified stays unfinished and says so; a degraded success is worse than a clear refusal.
2. **Offline is a design property, not a deployment accident.** Anything the fleet needs travels with the fleet.
3. **Destructive work is explicit.** Approval and root are gates, not warnings.
4. **State is legible at a glance.** Which wave, which stage, which channel — readable in seconds, from a device screen or across 700 devices.
5. **Turkish for operators, English for the codebase.** No mixed-language surface.
## Accessibility & Inclusion
Field devices are operated in workshops and customer sites, sometimes on small or poorly lit screens; the console is used with a keyboard more often than a mouse. Readable contrast and non-colour-only status signalling are requirements, not enhancements. No formal standard (WCAG level) was agreed; none is claimed.

View file

@ -77,25 +77,7 @@ EOF
say() { printf 'bf-bootstrap: %s\n' "$*"; } say() { printf 'bf-bootstrap: %s\n' "$*"; }
warn() { printf 'bf-bootstrap: WARN: %s\n' "$*" >&2; } warn() { printf 'bf-bootstrap: WARN: %s\n' "$*" >&2; }
die() { printf 'bf-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; } die() { printf 'bf-bootstrap: ERROR: %s\n' "$*" >&2; exit 1; }
# run: executes a mutating command, or only reports it under --check.
# Git-LFS smudge is disabled for every git operation this script performs.
#
# Why: the offline package pool is ~884 MB of .deb payload tracked through Git
# LFS, and a default clone materialises all of it (measured: 2.7 GB working tree
# against 20 MB with smudge off, a 135x difference). A device that only needs to
# reach the console and the manifest pays that cost on every clone for bytes it
# may never install from.
#
# The trade is explicit: with smudge off the pool files on disk are 131-byte
# pointer texts, and the installer refuses such a bundle rather than installing
# from it ("Nokta dosyası" below). Fetching the payload is therefore a separate,
# deliberate step -- `git lfs pull --include='provisioning/offline-repo/built/pool/*'`
# -- so that "I cloned the repository" and "I have the packages" stay two facts.
#
# This is deliberately the smudge-off half only. Enabling a full `git lfs pull`
# here would reintroduce the 884 MB download this prevents.
export GIT_LFS_SKIP_SMUDGE=1
run() { run() {
if [[ "$CHECK_MODE" -eq 1 ]]; then if [[ "$CHECK_MODE" -eq 1 ]]; then
printf 'bf-bootstrap: check: would run: %s\n' "$*" printf 'bf-bootstrap: check: would run: %s\n' "$*"

File diff suppressed because it is too large Load diff

View file

@ -1,743 +0,0 @@
"""bfos/ui_theme.py -- design tokens for the device console.
The values in this file MIRROR ``web/static/tokens.css``, which is the single
source of truth for both operator surfaces (the device console and the central
panel). Nothing here invents a colour: a colour means a device state, a
severity, or the action accent, and a fifth hue would be a bug (docs/DESIGN.md,
"Rules"). The mirror is not a comment-level promise -- ``TOKENS`` carries the
CSS custom-property name of every value, and ``tests/test_bfos_gui_static.py``
parses ``tokens.css`` and compares the two files, so a token changed on one
surface and not the other fails the suite instead of drifting.
Why light, not dark
-------------------
The console runs in workshops, dealer service areas and customer sites: bright
overhead light, daylight from a roller door, a 14" laptop panel. A near-black
console loses its own body text there and its state colours collapse into
equal-brightness smudges. So the ground is light (``BG_APP``), the panels are
white (``BG_PANEL``), the one dark region is the header (``BG_HEADER``), and
saturation is spent only where it carries meaning.
Faces
-----
No webfont, and no font download: the fleet installs offline, so the UI face is
whatever the platform ships and the data face is monospace (ids, counts, exit
codes, timestamps line up in a column). ``apply_theme`` picks the first family
the platform actually has, from a candidate list -- never a missing font name,
which is a silently different layout.
Sizes and the 4px rhythm
------------------------
Type sizes and space steps are kept in the token's own unit (CSS px) so the
mirror is exact. Tk font sizes are points, so ``point_size`` converts with the
96 dpi relation the tokens assume; padding and row heights are already pixels
in Tk, so they are used as ``SP_*`` values directly.
Usage
-----
theme = ui_theme.apply_theme(root) # once, before any widget is built
ttk.Label(parent, style=theme.panel_label)
"""
from __future__ import annotations
from typing import Dict, Mapping, Sequence
try: # pragma: no cover - environment dependent
from tkinter import ttk
HAVE_TKINTER = True
except Exception: # pragma: no cover - environment dependent
ttk = None # type: ignore[assignment]
HAVE_TKINTER = False
# ---------------------------------------------------------------------------
# Ground and structure -- tokens.css: "Ground and structure"
# ---------------------------------------------------------------------------
BG_APP = "#f6f7f8" # window ground
BG_PANEL = "#ffffff" # raised surfaces, cards, table body
BG_SUNKEN = "#eef0f2" # wells: log output, code, inactive rows
BG_HEADER = "#1d2530" # app header; the one dark region
BORDER = "#d5d9de" # hairline between regions
BORDER_STRONG = "#b6bcc4" # input borders, table rules
# ---------------------------------------------------------------------------
# Text
# ---------------------------------------------------------------------------
TEXT = "#1a1f26" # body
TEXT_MUTED = "#5c6570" # secondary lines, labels
TEXT_FAINT = "#8b939c" # timestamps, hints, placeholders
TEXT_ON_DARK = "#e8ebee" # header text
TEXT_ON_ACCENT = "#ffffff"
# ---------------------------------------------------------------------------
# Action -- one accent: the action the operator came to take, and the
# selected row. Nothing else.
# ---------------------------------------------------------------------------
ACCENT = "#1d4ed8"
ACCENT_HOVER = "#1a43bd"
ACCENT_ACTIVE = "#16389e"
ACCENT_SOFT = "#e8eeff" # selected row, focus ring fill
FOCUS_RING = "#1d4ed8"
# ---------------------------------------------------------------------------
# State severities -- four, each with a strong form for text and a soft form
# for fills, so a badge is built from one palette without inventing a second.
# ---------------------------------------------------------------------------
OK = "#15803d"
OK_SOFT = "#e6f4ea"
WARN = "#b45309"
WARN_SOFT = "#fdf3e3"
DANGER = "#b91c1c"
DANGER_SOFT = "#fdecec"
INFO = "#1d4ed8"
INFO_SOFT = "#e8eeff"
IDLE = "#5c6570" # not yet acted on: not a failure
IDLE_SOFT = "#eef0f2"
# ---------------------------------------------------------------------------
# Device state machine -- PROVISIONED_OFFLINE -> ENROLLED -> READY, plus the
# failure and the absence that sit outside the sequence. Named so no surface
# repaints a stage by accident.
#
# PROVISIONED is deliberately NOT the action accent (tokens.css states the
# reason): the accent means "the action you came to take" and "the selected
# row", so a KURULDU badge built from it made a state and a selection the same
# blue. It is a slate of its own, one step along the path and distinct from the
# neutral --idle.
# ---------------------------------------------------------------------------
STATE_PROVISIONED = "#475569"
STATE_ENROLLED = WARN
STATE_READY = OK
STATE_FAILED = DANGER
STATE_ABSENT = IDLE
# ---------------------------------------------------------------------------
# Type (px, as the tokens declare them)
# ---------------------------------------------------------------------------
SIZE_XS = 12
SIZE_SM = 13
SIZE_MD = 15
SIZE_LG = 18
SIZE_XL = 22
SIZE_2XL = 30
LEADING_TIGHT = 1.25
LEADING_NORMAL = 1.5
# ---------------------------------------------------------------------------
# Space -- a 4px rhythm, named so a component never hand-picks 7px
# ---------------------------------------------------------------------------
SP_1 = 4
SP_2 = 8
SP_3 = 12
SP_4 = 16
SP_5 = 24
SP_6 = 32
SP_7 = 48
RADIUS = 6
RADIUS_LG = 10
# ---------------------------------------------------------------------------
# Metrics
# ---------------------------------------------------------------------------
ROW_H = 44 # fleet table row: comfortable at arm's length
CONTROL_H = 36 # buttons and inputs share one height
HEADER_H = 56
SIDEBAR_W = 220
# Elevation is a token with no Tk equivalent (Tk has no box-shadow); the names
# are kept so the two token sets stay comparable and the absence is explicit.
SHADOW_SM = "0 1px 2px rgba(16, 24, 40, .06)"
SHADOW_MD = "0 4px 12px rgba(16, 24, 40, .10)"
# ---------------------------------------------------------------------------
# Fonts -- candidates in preference order, resolved against the platform
# ---------------------------------------------------------------------------
FONT_UI_CANDIDATES: Sequence[str] = (
"Noto Sans",
"DejaVu Sans",
"Ubuntu",
"Liberation Sans",
"Segoe UI",
"Helvetica",
)
FONT_DATA_CANDIDATES: Sequence[str] = (
"DejaVu Sans Mono",
"Liberation Mono",
"Noto Sans Mono",
"Consolas",
"Menlo",
"Courier New",
)
FONT_UI_FALLBACK = "TkDefaultFont"
FONT_DATA_FALLBACK = "TkFixedFont"
#: CSS px -> Tk points, at the 96 dpi the tokens assume. Table, not a formula:
#: rounding is per size so the console keeps the token's steps instead of
#: collapsing two of them onto the same point size.
POINT_SIZES: Mapping[int, int] = {
SIZE_XS: 9,
SIZE_SM: 10,
SIZE_MD: 11,
SIZE_LG: 14,
SIZE_XL: 16,
SIZE_2XL: 22,
}
# ---------------------------------------------------------------------------
# The mirror table -- CSS custom property -> value. Kept as data so the parity
# test reads the same list the code uses.
# ---------------------------------------------------------------------------
TOKENS: Mapping[str, str] = {
"--bg-app": BG_APP,
"--bg-panel": BG_PANEL,
"--bg-sunken": BG_SUNKEN,
"--bg-header": BG_HEADER,
"--border": BORDER,
"--border-strong": BORDER_STRONG,
"--text": TEXT,
"--text-muted": TEXT_MUTED,
"--text-faint": TEXT_FAINT,
"--text-on-dark": TEXT_ON_DARK,
"--text-on-accent": TEXT_ON_ACCENT,
"--accent": ACCENT,
"--accent-hover": ACCENT_HOVER,
"--accent-active": ACCENT_ACTIVE,
"--accent-soft": ACCENT_SOFT,
"--focus-ring": FOCUS_RING,
"--ok": OK,
"--ok-soft": OK_SOFT,
"--warn": WARN,
"--warn-soft": WARN_SOFT,
"--danger": DANGER,
"--danger-soft": DANGER_SOFT,
"--info": INFO,
"--info-soft": INFO_SOFT,
"--idle": IDLE,
"--idle-soft": IDLE_SOFT,
"--state-provisioned": STATE_PROVISIONED,
"--state-enrolled": STATE_ENROLLED,
"--state-ready": STATE_READY,
"--state-failed": STATE_FAILED,
"--state-absent": STATE_ABSENT,
}
#: Space tokens, mirrored by name (the parity test compares these too).
SPACE_TOKENS: Mapping[str, int] = {
"--sp-1": SP_1,
"--sp-2": SP_2,
"--sp-3": SP_3,
"--sp-4": SP_4,
"--sp-5": SP_5,
"--sp-6": SP_6,
"--sp-7": SP_7,
}
#: Type-size tokens, mirrored by name.
SIZE_TOKENS: Mapping[str, int] = {
"--size-xs": SIZE_XS,
"--size-sm": SIZE_SM,
"--size-md": SIZE_MD,
"--size-lg": SIZE_LG,
"--size-xl": SIZE_XL,
"--size-2xl": SIZE_2XL,
}
#: Metric tokens, mirrored by name.
METRIC_TOKENS: Mapping[str, int] = {
"--row-h": ROW_H,
"--control-h": CONTROL_H,
"--header-h": HEADER_H,
"--sidebar-w": SIDEBAR_W,
}
#: Every mirrored value, by CSS token name (used by the parity test).
#: Lengths are stored WITHOUT their unit: the unit is the token's (px at
#: 96 dpi), and the console passes the number straight to Tk padding, which is
#: already in pixels. The parity test strips the CSS unit to compare.
ALL_TOKENS: Mapping[str, str] = {
**TOKENS,
**{name: str(value) for name, value in SPACE_TOKENS.items()},
**{name: str(value) for name, value in SIZE_TOKENS.items()},
**{name: str(value) for name, value in METRIC_TOKENS.items()},
}
# ---------------------------------------------------------------------------
# Severity vocabulary -- the fill/text pair a badge or a row is built from
# ---------------------------------------------------------------------------
SEVERITIES: Sequence[str] = ("ok", "warn", "danger", "info", "idle")
SEVERITY_FILL: Mapping[str, str] = {
"ok": OK_SOFT,
"warn": WARN_SOFT,
"danger": DANGER_SOFT,
"info": INFO_SOFT,
"idle": IDLE_SOFT,
}
SEVERITY_TEXT: Mapping[str, str] = {
"ok": OK,
"warn": WARN,
"danger": DANGER,
"info": INFO,
"idle": IDLE,
}
#: State vocabulary, exactly the words both surfaces use (docs/DESIGN.md).
STATE_WORDS: Mapping[str, str] = {
"provisioned": "KURULDU",
"enrolled": "KAYITLI",
"ready": "HAZIR",
"failed": "HATA",
"absent": "YOK",
}
STATE_FILL: Mapping[str, str] = {
# The tokens give no soft form for provisioned; the console borrows the
# sunken well fill rather than inventing a hue, so a KURULDU badge cannot be
# mistaken for the accent-soft selection (see tokens.css, device state).
"provisioned": BG_SUNKEN,
"enrolled": WARN_SOFT,
"ready": OK_SOFT,
"failed": DANGER_SOFT,
"absent": IDLE_SOFT,
}
STATE_TEXT_COLOUR: Mapping[str, str] = {
"provisioned": STATE_PROVISIONED,
"enrolled": STATE_ENROLLED,
"ready": STATE_READY,
"failed": STATE_FAILED,
"absent": STATE_ABSENT,
}
#: Privilege is a state, not a log line (docs/DESIGN.md, "Privilege").
PRIVILEGE_STYLE: Mapping[bool, str] = {True: "ok", False: "warn"}
#: ttk style names, resolved once by apply_theme and read by the view.
_FAMILIES: Dict[str, str] = {}
# ---------------------------------------------------------------------------
# Small helpers
# ---------------------------------------------------------------------------
def point_size(px: int) -> int:
"""Token px -> Tk point size (see POINT_SIZES)."""
return POINT_SIZES.get(px, max(7, round(px * 0.75)))
def ui_family() -> str:
"""The UI face resolved by :func:`apply_theme` (platform default before)."""
return _FAMILIES.get("ui", FONT_UI_FALLBACK)
def data_family() -> str:
"""The monospace face resolved by :func:`apply_theme`."""
return _FAMILIES.get("data", FONT_DATA_FALLBACK)
def ui_font(size: int = SIZE_MD, weight: str = "normal") -> tuple:
"""A Tk font tuple in the platform UI face."""
if weight == "normal":
return (ui_family(), point_size(size))
return (ui_family(), point_size(size), weight)
def data_font(size: int = SIZE_SM, weight: str = "normal") -> tuple:
"""A Tk font tuple in the data (monospace) face."""
if weight == "normal":
return (data_family(), point_size(size))
return (data_family(), point_size(size), weight)
#: State key -> badge style, built from STATE_WORDS so the word and the colour
#: can never be paired up wrongly on one surface.
STATE_BADGE_STYLE: Mapping[str, str] = {
key: f"State{key.capitalize()}.Badge.TLabel" for key in STATE_WORDS
}
#: Privilege level -> badge style ("Privilege" for full, "ReadOnly" else).
PRIVILEGE_BADGE_STYLE: Mapping[str, str] = {
"full": "Privilege.Badge.TLabel",
"read_only": "ReadOnly.Badge.TLabel",
}
def severity_style(severity: str) -> str:
"""ttk style name for a severity pill (ok / warn / danger / info / idle)."""
return f"{severity.capitalize()}.Status.TLabel"
def _resolve_family(root, candidates: Sequence[str], fallback: str) -> str:
"""First family the platform actually has; the Tk default otherwise."""
try:
from tkinter import font as tkfont
available = {name.lower() for name in tkfont.families(root)}
except Exception: # pragma: no cover - font database unavailable
return fallback
for name in candidates:
if name.lower() in available:
return name
return fallback
def _configure(style, name: str, **options) -> None:
"""Configure a ttk style, ignoring options the platform's theme rejects.
ttk style elements differ between Tk builds (clam vs default, Tk 8.6 vs
9.x): a missing element option raises TclError. Losing one border colour is
a cosmetic difference; crashing the console on startup is not.
"""
try:
style.configure(name, **options)
except Exception:
for key, value in options.items():
try:
style.configure(name, **{key: value})
except Exception:
continue
def _map(style, name: str, **options) -> None:
"""State map for a ttk style, same tolerance as :func:`_configure`."""
try:
style.map(name, **options)
except Exception:
for key, value in options.items():
try:
style.map(name, **{key: value})
except Exception:
continue
# ---------------------------------------------------------------------------
# The one setup function, called once at startup
# ---------------------------------------------------------------------------
def apply_theme(root) -> Mapping[str, str]:
"""Install the token set on ``root``'s ttk styles; returns the style names.
Idempotent and cheap: the GUI calls it once before building any widget, and
a second call simply re-applies the same values (tests may build several
roots in one process).
"""
if not HAVE_TKINTER: # pragma: no cover - environment dependent
raise RuntimeError("tkinter yok: tema uygulanamaz")
assert ttk is not None # narrowing for the type checker
_FAMILIES["ui"] = _resolve_family(root, FONT_UI_CANDIDATES, FONT_UI_FALLBACK)
_FAMILIES["data"] = _resolve_family(root, FONT_DATA_CANDIDATES, FONT_DATA_FALLBACK)
try:
root.configure(bg=BG_APP)
except Exception: # pragma: no cover - non-Tk root
pass
style = ttk.Style(root)
# clam honours background/bordercolor on the widget elements we restyle;
# the 'default' theme on Linux ignores most of them, which is how a
# console ends up grey whatever the tokens say.
try:
if "clam" in style.theme_names():
style.theme_use("clam")
except Exception: # pragma: no cover - theme list unavailable
pass
base_font = ui_font(SIZE_MD)
_configure(
style, ".",
background=BG_APP,
foreground=TEXT,
font=base_font,
bordercolor=BORDER,
lightcolor=BORDER,
darkcolor=BORDER,
troughcolor=BG_SUNKEN,
focuscolor=FOCUS_RING,
)
# -- frames: the ground, the raised panel, the well, the one dark band --
_configure(style, "TFrame", background=BG_APP, borderwidth=0)
_configure(style, "Panel.TFrame", background=BG_PANEL, borderwidth=1,
relief="solid", bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER)
_configure(style, "Sunken.TFrame", background=BG_SUNKEN, borderwidth=1,
relief="solid", bordercolor=BORDER, lightcolor=BORDER, darkcolor=BORDER)
_configure(style, "Header.TFrame", background=BG_HEADER, borderwidth=0)
_configure(style, "Warn.TFrame", background=WARN_SOFT, borderwidth=1,
relief="solid", bordercolor=WARN, lightcolor=WARN_SOFT,
darkcolor=WARN_SOFT)
_configure(style, "Info.TFrame", background=INFO_SOFT, borderwidth=1,
relief="solid", bordercolor=INFO, lightcolor=INFO_SOFT,
darkcolor=INFO_SOFT)
_configure(style, "Accent.TFrame", background=ACCENT_SOFT, borderwidth=1,
relief="solid", bordercolor=ACCENT, lightcolor=ACCENT_SOFT,
darkcolor=ACCENT_SOFT)
# -- labels -------------------------------------------------------------
_configure(style, "TLabel", background=BG_APP, foreground=TEXT, font=base_font)
_configure(style, "Panel.TLabel", background=BG_PANEL, foreground=TEXT)
_configure(style, "Sunken.TLabel", background=BG_SUNKEN, foreground=TEXT)
_configure(style, "Muted.TLabel", background=BG_APP, foreground=TEXT_MUTED,
font=ui_font(SIZE_SM))
_configure(style, "PanelMuted.TLabel", background=BG_PANEL,
foreground=TEXT_MUTED, font=ui_font(SIZE_SM))
_configure(style, "Faint.TLabel", background=BG_APP, foreground=TEXT_FAINT,
font=ui_font(SIZE_XS))
_configure(style, "PanelFaint.TLabel", background=BG_PANEL,
foreground=TEXT_FAINT, font=ui_font(SIZE_XS))
_configure(style, "FieldLabel.TLabel", background=BG_PANEL, foreground=TEXT_MUTED,
font=ui_font(SIZE_SM), anchor="e")
_configure(style, "Section.TLabel", background=BG_APP, foreground=TEXT,
font=ui_font(SIZE_LG, "bold"))
_configure(style, "PanelTitle.TLabel", background=BG_PANEL, foreground=TEXT_MUTED,
font=ui_font(SIZE_XS, "bold"))
# The one loud figure per view.
_configure(style, "Figure.TLabel", background=BG_APP, foreground=TEXT,
font=ui_font(SIZE_2XL, "bold"))
_configure(style, "Ok.TLabel", background=BG_PANEL, foreground=OK,
font=ui_font(SIZE_SM))
_configure(style, "Warn.TLabel", background=WARN_SOFT, foreground=WARN,
font=ui_font(SIZE_SM))
_configure(style, "InfoText.TLabel", background=INFO_SOFT, foreground=INFO,
font=ui_font(SIZE_SM))
_configure(style, "Danger.TLabel", background=BG_PANEL, foreground=DANGER,
font=ui_font(SIZE_SM))
_configure(style, "AppWarn.TLabel", background=BG_APP, foreground=WARN,
font=ui_font(SIZE_SM, "bold"))
_configure(style, "PanelOk.TLabel", background=BG_PANEL, foreground=OK,
font=ui_font(SIZE_SM, "bold"))
_configure(style, "PanelDanger.TLabel", background=BG_PANEL, foreground=DANGER,
font=ui_font(SIZE_SM, "bold"))
# Dialogs sit on the app ground, not on a panel.
_configure(style, "Dialog.TLabel", background=BG_APP, foreground=TEXT,
font=ui_font(SIZE_MD))
_configure(style, "DialogTitle.TLabel", background=BG_APP, foreground=TEXT,
font=ui_font(SIZE_LG, "bold"))
_configure(style, "DialogMuted.TLabel", background=BG_APP,
foreground=TEXT_MUTED, font=ui_font(SIZE_SM))
# Header (dark) region.
_configure(style, "HeaderTitle.TLabel", background=BG_HEADER,
foreground=TEXT_ON_DARK, font=ui_font(SIZE_XL, "bold"))
_configure(style, "HeaderMeta.TLabel", background=BG_HEADER,
foreground=TEXT_FAINT, font=ui_font(SIZE_SM))
_configure(style, "HeaderTarget.TLabel", background=BG_HEADER,
foreground=TEXT_ON_DARK, font=data_font(SIZE_SM))
_configure(style, "HeaderFigure.TLabel", background=BG_HEADER,
foreground=TEXT_ON_DARK, font=data_font(SIZE_MD, "bold"))
# -- badges: state word + colour, never colour alone -------------------
_configure(style, "Badge.TLabel", background=IDLE_SOFT, foreground=IDLE,
font=ui_font(SIZE_SM, "bold"), padding=(SP_2, SP_1))
for key, word_style in (("provisioned", "Provisioned"), ("enrolled", "Enrolled"),
("ready", "Ready"), ("failed", "Failed"),
("absent", "Absent")):
_configure(
style, f"State{word_style}.Badge.TLabel",
background=STATE_FILL[key], foreground=STATE_TEXT_COLOUR[key],
font=ui_font(SIZE_SM, "bold"), padding=(SP_2, SP_1),
)
_configure(style, "Privilege.Badge.TLabel", background=OK_SOFT, foreground=OK,
font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_1))
_configure(style, "ReadOnly.Badge.TLabel", background=WARN_SOFT, foreground=WARN,
font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_1))
_configure(style, "Type.Destructive.Badge.TLabel", background=DANGER_SOFT,
foreground=DANGER, font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1))
_configure(style, "Type.ReadOnly.Badge.TLabel", background=IDLE_SOFT,
foreground=IDLE, font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1))
_configure(style, "Open.Badge.TLabel", background=OK_SOFT, foreground=OK,
font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1))
_configure(style, "Closed.Badge.TLabel", background=WARN_SOFT, foreground=WARN,
font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1))
_configure(style, "Info.Badge.TLabel", background=INFO_SOFT, foreground=INFO,
font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_1))
_configure(style, "Neutral.Badge.TLabel", background=BG_SUNKEN, foreground=IDLE,
font=ui_font(SIZE_XS, "bold"), padding=(SP_2, SP_1))
# -- status pill (footer): the console's own state ---------------------
_configure(style, "Status.TLabel", background=IDLE_SOFT, foreground=IDLE,
font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_2))
for severity in SEVERITIES:
_configure(
style, f"{severity.capitalize()}.Status.TLabel",
background=SEVERITY_FILL[severity], foreground=SEVERITY_TEXT[severity],
font=ui_font(SIZE_SM, "bold"), padding=(SP_3, SP_2),
)
# -- inputs: one height with the buttons ---------------------------------
entry_opts = dict(
fieldbackground=BG_PANEL,
background=BG_PANEL,
foreground=TEXT,
bordercolor=BORDER_STRONG,
lightcolor=BORDER_STRONG,
darkcolor=BORDER_STRONG,
insertcolor=TEXT,
padding=(SP_2, SP_1),
font=ui_font(SIZE_MD),
)
_configure(style, "TEntry", **entry_opts)
_map(style, "TEntry",
bordercolor=[("focus", FOCUS_RING), ("disabled", BORDER)],
foreground=[("disabled", TEXT_FAINT)],
fieldbackground=[("disabled", BG_SUNKEN)])
_configure(style, "Invalid.TEntry", **entry_opts)
_configure(style, "Invalid.TEntry", bordercolor=DANGER, lightcolor=DANGER,
darkcolor=DANGER)
_map(style, "Invalid.TEntry", bordercolor=[("focus", DANGER)])
_configure(style, "TCombobox", **entry_opts)
_configure(style, "TCombobox", arrowcolor=TEXT_MUTED, selectbackground=ACCENT_SOFT,
selectforeground=TEXT, padding=(SP_2, SP_1))
_map(style, "TCombobox",
fieldbackground=[("readonly", BG_PANEL), ("disabled", BG_SUNKEN)],
foreground=[("disabled", TEXT_FAINT)],
bordercolor=[("focus", FOCUS_RING)])
_configure(style, "TCheckbutton", background=BG_APP, foreground=TEXT,
font=ui_font(SIZE_SM), focuscolor=BG_APP, padding=(SP_1, SP_1))
_map(style, "TCheckbutton",
background=[("active", BG_APP)],
foreground=[("disabled", TEXT_FAINT)],
indicatorcolor=[("selected", ACCENT), ("pressed", ACCENT_ACTIVE)])
_configure(style, "Panel.TCheckbutton", background=BG_PANEL, foreground=TEXT,
font=ui_font(SIZE_SM), padding=(SP_1, SP_1))
_map(style, "Panel.TCheckbutton",
background=[("active", BG_PANEL), ("disabled", BG_PANEL)],
foreground=[("disabled", TEXT_FAINT)])
# -- buttons: hairline secondary, one accent primary, danger for
# destructive work ------------------------------------------------
_configure(style, "TButton", background=BG_PANEL, foreground=TEXT,
bordercolor=BORDER_STRONG, lightcolor=BG_PANEL, darkcolor=BG_PANEL,
font=ui_font(SIZE_MD), padding=(SP_3, SP_2), relief="solid",
focusthickness=1, focuscolor=FOCUS_RING, anchor="center")
_map(style, "TButton",
background=[("disabled", BG_SUNKEN), ("pressed", BG_SUNKEN),
("active", ACCENT_SOFT)],
foreground=[("disabled", TEXT_FAINT), ("pressed", ACCENT_ACTIVE)],
bordercolor=[("focus", FOCUS_RING)],
lightcolor=[("active", ACCENT_SOFT), ("pressed", BG_SUNKEN)],
darkcolor=[("active", ACCENT_SOFT), ("pressed", BG_SUNKEN)])
_configure(style, "Accent.TButton", background=ACCENT, foreground=TEXT_ON_ACCENT,
bordercolor=ACCENT, lightcolor=ACCENT, darkcolor=ACCENT)
_map(style, "Accent.TButton",
background=[("disabled", IDLE_SOFT), ("pressed", ACCENT_ACTIVE),
("active", ACCENT_HOVER)],
foreground=[("disabled", TEXT_FAINT)],
bordercolor=[("focus", ACCENT_ACTIVE), ("active", ACCENT_HOVER)],
lightcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)],
darkcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)])
_configure(style, "Danger.TButton", background=DANGER, foreground=TEXT_ON_ACCENT,
bordercolor=DANGER, lightcolor=DANGER, darkcolor=DANGER)
_map(style, "Danger.TButton",
background=[("disabled", IDLE_SOFT), ("pressed", DANGER_SOFT),
("active", DANGER_SOFT)],
foreground=[("disabled", TEXT_FAINT), ("active", DANGER),
("pressed", DANGER)],
bordercolor=[("focus", DANGER)],
lightcolor=[("active", DANGER_SOFT), ("pressed", DANGER_SOFT)],
darkcolor=[("active", DANGER_SOFT), ("pressed", DANGER_SOFT)])
# Simple mode: the three big buttons, one of them the action accent.
_configure(style, "Big.TButton", font=ui_font(SIZE_LG),
padding=(SP_5, SP_4), anchor="w")
_configure(style, "Accent.Big.TButton", background=ACCENT,
foreground=TEXT_ON_ACCENT, bordercolor=ACCENT, lightcolor=ACCENT,
darkcolor=ACCENT)
_map(style, "Accent.Big.TButton",
background=[("disabled", IDLE_SOFT), ("pressed", ACCENT_ACTIVE),
("active", ACCENT_HOVER)],
foreground=[("disabled", TEXT_FAINT)],
lightcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)],
darkcolor=[("active", ACCENT_HOVER), ("pressed", ACCENT_ACTIVE)])
_configure(style, "Quiet.TButton", background=BG_SUNKEN, bordercolor=BORDER)
# -- table: the fleet/operation list ------------------------------------
_configure(style, "Treeview", background=BG_PANEL, fieldbackground=BG_PANEL,
foreground=TEXT, bordercolor=BORDER, lightcolor=BORDER,
darkcolor=BORDER, font=ui_font(SIZE_SM), rowheight=ROW_H)
_configure(style, "Treeview.Heading", background=BG_SUNKEN,
foreground=TEXT_MUTED, font=ui_font(SIZE_XS, "bold"),
relief="flat", padding=(SP_2, SP_2))
_map(style, "Treeview",
background=[("selected", ACCENT_SOFT)],
foreground=[("selected", TEXT)])
_map(style, "Treeview.Heading", background=[("active", BG_SUNKEN)])
# -- tabs, separators, scrollbars --------------------------------------
_configure(style, "TNotebook", background=BG_APP, bordercolor=BORDER,
lightcolor=BORDER, darkcolor=BORDER, tabmargins=(0, 0, 0, 0))
_configure(style, "TNotebook.Tab", background=BG_SUNKEN, foreground=TEXT_MUTED,
font=ui_font(SIZE_SM), padding=(SP_3, SP_2), bordercolor=BORDER)
_map(style, "TNotebook.Tab",
background=[("selected", BG_PANEL), ("active", BG_PANEL)],
foreground=[("selected", TEXT), ("active", TEXT)],
expand=[("selected", (0, 0, 0, 0))])
_configure(style, "TSeparator", background=BORDER)
for orient in ("Vertical", "Horizontal"):
_configure(style, f"{orient}.TScrollbar", background=BG_SUNKEN,
troughcolor=BG_APP, bordercolor=BORDER, arrowcolor=TEXT_MUTED,
lightcolor=BG_SUNKEN, darkcolor=BG_SUNKEN)
_map(style, f"{orient}.TScrollbar",
background=[("active", BORDER_STRONG)])
return {
"frame": "TFrame",
"panel": "Panel.TFrame",
"sunken": "Sunken.TFrame",
"header": "Header.TFrame",
"warn_band": "Warn.TFrame",
"info_band": "Info.TFrame",
"label": "TLabel",
"panel_label": "Panel.TLabel",
"panel_title": "PanelTitle.TLabel",
"muted": "Muted.TLabel",
"panel_muted": "PanelMuted.TLabel",
"faint": "Faint.TLabel",
"panel_faint": "PanelFaint.TLabel",
"field_label": "FieldLabel.TLabel",
"section": "Section.TLabel",
"figure": "Figure.TLabel",
"ok_label": "Ok.TLabel",
"warn_label": "Warn.TLabel",
"info_label": "InfoText.TLabel",
"danger_label": "Danger.TLabel",
"app_warn": "AppWarn.TLabel",
"panel_ok": "PanelOk.TLabel",
"panel_danger": "PanelDanger.TLabel",
"header_title": "HeaderTitle.TLabel",
"header_meta": "HeaderMeta.TLabel",
"header_target": "HeaderTarget.TLabel",
"header_figure": "HeaderFigure.TLabel",
"badge": "Badge.TLabel",
"badge_info": "Info.Badge.TLabel",
"badge_neutral": "Neutral.Badge.TLabel",
"badge_destructive": "Type.Destructive.Badge.TLabel",
"badge_readonly": "Type.ReadOnly.Badge.TLabel",
"badge_open": "Open.Badge.TLabel",
"badge_closed": "Closed.Badge.TLabel",
"privilege": "Privilege.Badge.TLabel",
"readonly": "ReadOnly.Badge.TLabel",
"status": "Status.TLabel",
"button": "TButton",
"accent_button": "Accent.TButton",
"danger_button": "Danger.TButton",
"big_button": "Big.TButton",
"big_accent_button": "Accent.Big.TButton",
"quiet_button": "Quiet.TButton",
"entry": "TEntry",
"invalid_entry": "Invalid.TEntry",
"panel_check": "Panel.TCheckbutton",
"check": "TCheckbutton",
"dialog": "Dialog.TLabel",
"dialog_title": "DialogTitle.TLabel",
"dialog_muted": "DialogMuted.TLabel",
}
def DANGER_ACTIVE_SAFE() -> str: # pragma: no cover - trivial
"""The danger colour used for a focused destructive control.
A named function (rather than a bare literal) so the fifth-hue rule stays
visible: focus on a dangerous action reuses the danger token.
"""
return DANGER

View file

@ -1,6 +1,6 @@
# 27 — Offline Provisioning # 27 — Offline Provisioning
> Kısa özet: Paketler **git deposunun içindedir** (`provisioning/offline-repo/built/pool/`, 1321 `.deb`, ~884 MB, Git LFS ile takip edilir). Saha kurulumu internet **gerektirmez**: `sudo bf` → Kurulum sekmesi → "Bu cihazı kur" → 8 haneli bayi no → onay. Paket yükü klonla birlikte gelmez (bilerek; bkz. §4), yerel aynadan taşınır ve kurucu depoyu kendi bulur. Bu yol yalnız `PROVISIONED_OFFLINE` durumuna getirir; cihaz, tek kullanımlık enrollment token ve merkezi kanallar doğrulanmadan `ENROLLED` veya `READY` olmaz. > Kısa özet: Paketler **git deposunun içindedir** (`provisioning/offline-repo/packages/`, 1321 `.deb`, ~888 MB, Git LFS ile takip edilir). Saha kurulumu internet **gerektirmez** ve kuran kişi **hiçbir şey indirmez**: `sudo bf` → Kurulum sekmesi → "Bu cihazı kur" → 8 haneli bayi no → onay. Bu yol yalnız `PROVISIONED_OFFLINE` durumuna getirir; cihaz, tek kullanımlık enrollment token ve merkezi kanallar doğrulanmadan `ENROLLED` veya `READY` olmaz.
> >
> - Dosya: `docs/27-OFFLINE-PROVISIONING.md` > - Dosya: `docs/27-OFFLINE-PROVISIONING.md`
> >
@ -13,7 +13,7 @@
## 1. Amaç ## 1. Amaç
Saha interneti kesik veya güvenilmezken tekrarlanabilir, denetlenebilir ve secretsiz ilk kurulumu tanımlamak. Kuran kişi paket aramaz, indirmez, bayrak ezberlemez: paket seti depoda sürümlenir ve yerel aynadan taşınır. Saha interneti kesik veya güvenilmezken tekrarlanabilir, denetlenebilir ve secretsiz ilk kurulumu tanımlamak. Kuran kişi paket aramaz, indirmez, bayrak ezberlemez: paket seti depoyla birlikte gelir.
## 2. Kapsam ## 2. Kapsam
@ -33,9 +33,7 @@ LİSANS: APT — https://manpages.ubuntu.com/manpages/noble/en/man8/apt.8.html
## 4. Neden Bu Karar? ## 4. Neden Bu Karar?
**Paketler ayrı bir medyada değil, bu git deposunda taşınır:** `provisioning/offline-repo/built/pool/` altında 1321 `.deb` (~884 MB) durur ve `.gitattributes` ile **Git LFS** üzerinden takip edilir (`*.deb filter=lfs ...`). Bu yüzden sahada "hangi paket nereden geldi?" sorusu ne internete ne de dış medyaya bağlıdır; aynı soru `packages.lock.tsv` ve `repo-manifest.yaml` ile yanıtlanır. **Paketler artık ayrı bir medyada değil, bu git deposunda taşınır:** `provisioning/offline-repo/packages/` altında 1321 `.deb` (~888 MB) durur ve `.gitattributes` ile **Git LFS** üzerinden takip edilir (`*.deb filter=lfs ...`). Bu yüzden sahada "hangi paket nereden geldi?" sorusu ne internete ne de dış medyaya bağlıdır; aynı soru `packages.lock.tsv` ve `repo-manifest.yaml` ile yanıtlanır. Kuran kişi paket aramaz ve **indirmez**; klonladığı depo seti zaten taşır.
Kuran kişi paket **aramaz**, ama paket yükünü kendiliğinden de indirmez — bu bilinçli bir ayrımdır. LFS yükü ~884 MB olduğu ve 700 cihazın her biri onu çekerse aylık bant kotası ilk günde tükeneceği için klon hafif tutulur; yük **yerel aynadan** (USB ya da repo sorumlusunun bıraktığı kopya) gelir, sahada internet gerekmez. Yalnız internet varken ve tek seferlik bir çekme uygunsa `git lfs pull --include='provisioning/offline-repo/built/pool/*'` kullanılır.
Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünür yapar: offline kurulum yalnız `PROVISIONED_OFFLINE` üretir. Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünür yapar: offline kurulum yalnız `PROVISIONED_OFFLINE` üretir.
@ -58,7 +56,7 @@ Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünü
## 7. Dezavantajlar ## 7. Dezavantajlar
- Git deposu paket setiyle birlikte büyür (LFS; içerik-hash ile tekilleşir, ~884 MB). Klon hafiftir ama paket yükü ayrıca taşınmalıdır: 700 cihazın her biri LFS'ten çekerse bant kotası tükenir, bu yüzden yük yerel aynadan dağıtılır. - Git deposu paket setiyle birlikte büyür (LFS clone/pull maliyeti); derin clone sahada zaman alabilir.
- Offline cihaz merkezi izleme ve erişim kanalları doğrulanana kadar operasyonel olarak hazır değildir. - Offline cihaz merkezi izleme ve erişim kanalları doğrulanana kadar operasyonel olarak hazır değildir.
- Paket snapshot'ı yenilenmediği sürece güvenlik güncellemeleri geride kalır (onaysız update zaten yasak, K-11). - Paket snapshot'ı yenilenmediği sürece güvenlik güncellemeleri geride kalır (onaysız update zaten yasak, K-11).
@ -83,16 +81,7 @@ Durum makinesi, "kuruldu" ile "merkezce güvenildi" arasındaki farkı görünü
### Kuran kişinin akışı (grafik arayüz — ana yol) ### Kuran kişinin akışı (grafik arayüz — ana yol)
1. Depoyu getir: `git clone <repo>` — **paketler bilerek gelmez.** Bootstrap (ve her git işlemi) `GIT_LFS_SKIP_SMUDGE=1` ile çalışır, çünkü paket yükü ~884 MB'tır ve tam klon sahada 2.7 GB'lık bir çalışma kopyası üretir (ölçüldü; smudge kapalıyken 20 MB). Paket yükünü almanın iki yolu var: 1. Depoyu getir: `git clone <repo>` (paketler Git LFS ile gelir) — veya repo sorumlusunun bıraktığı kopyada `/opt/blueforce/offline-repo` hazırdır.
- **Yerel ayna (önerilen, sahada internet yok):** repo sorumlusunun hazırladığı `/opt/blueforce/offline-repo` kopyasını taşıyın; ya da
- **Hedefli LFS çekme (yalnız internet varken):** depo kökünde
```
git lfs pull --include='provisioning/offline-repo/built/pool/*'
```
Bu yalnız teslim havuzunu indirir (`packages/` altındaki kaynak kopyayı çekmez, aynı baytları ikinci kez indirmez).
Paketler gelmediyse kurucu bunu **tek cümleyle** söyler (`Paketler henüz indirilmemiş: … Git LFS işaretçisi`), çünkü havuzdaki dosyalar `.deb` değil 131 baytlık işaretçi metnidir.
2. `sudo bf` — pencere açılır: **Blueforce Field OS — Yönetim**. 2. `sudo bf` — pencere açılır: **Blueforce Field OS — Yönetim**.
3. Üst çubuk **Hedef > Bayi no (8 hane)** alanına numarayı yaz (örn. `12010193`). 3. Üst çubuk **Hedef > Bayi no (8 hane)** alanına numarayı yaz (örn. `12010193`).
4. **Kurulum** sekmesi → **`Bu cihazı kur`** (hiçbir şey yazmayan ön kontrol için: `Kurulum ön kontrolü (--check)`). 4. **Kurulum** sekmesi → **`Bu cihazı kur`** (hiçbir şey yazmayan ön kontrol için: `Kurulum ön kontrolü (--check)`).
@ -188,9 +177,8 @@ Grafik operatör uygulaması Tkinter ile yazılmıştır ve tkinter Ubuntu'da **
## 12. Kontrol Listesi ## 12. Kontrol Listesi
- [ ] Paket kaynağı repoda: `provisioning/offline-repo/packages/` (1321 `.deb`, Git LFS ile takip ediliyor; teslim kopyası `built/pool/` içindedir). - [ ] Paket seti repoda: `provisioning/offline-repo/packages/` (1321 `.deb`, Git LFS ile takip ediliyor).
- [ ] Bundle indeksi repoda: `provisioning/offline-repo/built/` (`Packages`, `Packages.gz`, `packages.lock.tsv`, `SHA256SUMS`, `pool/`). - [ ] Bundle indeksi repoda: `provisioning/offline-repo/built/` (`Packages`, `Packages.gz`, `packages.lock.tsv`, `SHA256SUMS`, `pool/`).
- [ ] Paket yükü sahaya taşındı: yerel ayna (USB ya da `/opt/blueforce/offline-repo`) — klon LFS yükünü **bilerek** getirmez, bu adım atlanırsa kurucu `Paketler henüz indirilmemiş: … Git LFS işaretçisi` ile durur.
- [ ] `repo-manifest.yaml` güncel; pin listesi ile paket havuzu uyuşuyor. - [ ] `repo-manifest.yaml` güncel; pin listesi ile paket havuzu uyuşuyor.
- [ ] Depoda/repoda cihaz kimliği, private key veya kalıcı token yok. - [ ] Depoda/repoda cihaz kimliği, private key veya kalıcı token yok.
- [ ] `python3-tk` ve bağımlılıkları `manifests/base-packages.txt` içinde pinli; `18-final-check` tkinter'ı denetliyor. - [ ] `python3-tk` ve bağımlılıkları `manifests/base-packages.txt` içinde pinli; `18-final-check` tkinter'ı denetliyor.
@ -212,7 +200,7 @@ Grafik operatör uygulaması Tkinter ile yazılmıştır ve tkinter Ubuntu'da **
```mermaid ```mermaid
flowchart LR flowchart LR
M["git clone (hafif: LFS smudge kapalı)<br/>+ paket yükü yerel aynadan"] --> G["sudo bf → Kurulum → 'Bu cihazı kur'"] M["git checkout (paketler LFS ile)<br/>veya /opt/blueforce/offline-repo"] --> G["sudo bf → Kurulum → 'Bu cihazı kur'"]
G --> D{"Kurucu yerel depoyu arar<br/>BF_OFFLINE_REPO → repo/built → /opt/blueforce/offline-repo"} G --> D{"Kurucu yerel depoyu arar<br/>BF_OFFLINE_REPO → repo/built → /opt/blueforce/offline-repo"}
D -->|"hiçbiri yok / eksik"| X["HATA: Yerel paket deposu yok<br/>aranan yollar + ÇIKIŞ NEDENİ · exit 1<br/>sistem değişmez"] D -->|"hiçbiri yok / eksik"| X["HATA: Yerel paket deposu yok<br/>aranan yollar + ÇIKIŞ NEDENİ · exit 1<br/>sistem değişmez"]
D -->|"eksiksiz aday bulundu"| V{"Doğrulama katmanı:<br/>dosya listesi, SHA256SUMS, kilit, pin ↔ pool"} D -->|"eksiksiz aday bulundu"| V{"Doğrulama katmanı:<br/>dosya listesi, SHA256SUMS, kilit, pin ↔ pool"}

View file

@ -4,7 +4,7 @@
- Dosya: `docs/33-OPERATOR-CONSOLE.md` - Dosya: `docs/33-OPERATOR-CONSOLE.md`
- İlgili kararlar: `24-DECISION-LOG.md#K-09` (golden image + kurucu), `#K-12` (kimlik formatı `^[0-9]{8}$`), `#K-13` (SSH anahtar zorunlu), `#K-11` (onaysız update yasağı), `#K-21` (durum modeli), `#K-22` (RDP her zaman hazır / `bf-gui-*` yalnız yerel GUI) - İlgili kararlar: `24-DECISION-LOG.md#K-09` (golden image + kurucu), `#K-12` (kimlik formatı `^[0-9]{8}$`), `#K-13` (SSH anahtar zorunlu), `#K-11` (onaysız update yasağı), `#K-21` (durum modeli), `#K-22` (RDP her zaman hazır / `bf-gui-*` yalnız yerel GUI)
- Uygulama: Python paketi `bfos` (depo kökü) — `bfos/ui_gui.py` (grafik arayüz, tkinter), `bfos/ui_theme.py` (tasarım belirteçleri: `web/static/tokens.css`'in aynası), `bfos/tui.py` + `bfos/cli.py` (terminal konsolu), `bfos/operations.py` (iş mantığı + işlem kataloğu), `bfos/runner.py` (çalıştırma + denetim kaydı), `bfos/config.py` (Türkçe metinler) - Uygulama: Python paketi `bfos` (depo kökü) — `bfos/ui_gui.py` (grafik arayüz, tkinter), `bfos/tui.py` + `bfos/cli.py` (terminal konsolu), `bfos/operations.py` (iş mantığı + işlem kataloğu), `bfos/runner.py` (çalıştırma + denetim kaydı), `bfos/config.py` (Türkçe metinler)
- Giriş noktası: `admin/bf` (sarmalayıcı) → `sudo bf` ve `sudo bf --tui`. Kurucu: `admin/bf-bootstrap.sh` - Giriş noktası: `admin/bf` (sarmalayıcı) → `sudo bf` ve `sudo bf --tui`. Kurucu: `admin/bf-bootstrap.sh`
- Yedek yol: `admin/bf-menu` + `admin/lib/*` (eski bash konsolu) — ana yol değildir; yalnız Python'suz cihazlar ve olağanüstü durumlar için korunur - Yedek yol: `admin/bf-menu` + `admin/lib/*` (eski bash konsolu) — ana yol değildir; yalnız Python'suz cihazlar ve olağanüstü durumlar için korunur
- Durum: [ ] Taslak — pencere yerleşimi ve örnek satırlar LAB'da alınan gerçek koşuyla güncellenir - Durum: [ ] Taslak — pencere yerleşimi ve örnek satırlar LAB'da alınan gerçek koşuyla güncellenir
@ -252,7 +252,7 @@ Pencere açılıyorsa kurulum tamam. Bundan sonra her iş için aynı komut:
sudo bf sudo bf
``` ```
> **Bundan sonra ezberleyeceğin tek şey bu komuttur.** Pencereden çıkmak için `Çıkış` düğmesi: **basit modda** üçüncü büyük düğme, **detaylı modda** eylem çubuğunun alt satırında. Tek çıkış kontrolü olduğu için ikisi aynı anda görünmez. > **Bundan sonra ezberleyeceğin tek şey bu komuttur.** Pencereden çıkmak için `Çıkış` düğmesi (basit modda üçüncü düğme, alt çubukta da bir `Çıkış` düğmesi var).
**tkinter notu (grafik arayüz için gerekli).** Pencerenin açılabilmesi için `python3-tk` paketi gerekir. Bu paket **çevrimdışı depoda pinlidir** (`provisioning/offline-repo/manifests/base-packages.txt`), yani normal kurulumda senin bir şey yapmana gerek yoktur; kurulum sonu kapısı (`scripts/install/installer/modules/18-final-check.sh`) tkinter'ı denetler. Yine de eksikse uygulama şu Türkçe hatayı verir ve çıkış kodu `3` ile biter: **tkinter notu (grafik arayüz için gerekli).** Pencerenin açılabilmesi için `python3-tk` paketi gerekir. Bu paket **çevrimdışı depoda pinlidir** (`provisioning/offline-repo/manifests/base-packages.txt`), yani normal kurulumda senin bir şey yapmana gerek yoktur; kurulum sonu kapısı (`scripts/install/installer/modules/18-final-check.sh`) tkinter'ı denetler. Yine de eksikse uygulama şu Türkçe hatayı verir ve çıkış kodu `3` ile biter:
@ -286,7 +286,7 @@ Yerel ekranda çalıştırmak için oturum açık bir makinede deneyin:
### 9.2 Arayüzü kullanma — sekme sekme ### 9.2 Arayüzü kullanma — sekme sekme
`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. Başlıkta `Blueforce Field OS — Yönetim`, altında `Blueforce saha cihazı yönetimi (Kur / Durum / Bakım)`, ardından depo yolu ve kullanılan katalog kaynağı (gerçek iş mantığı katmanı, örn. `bfos.operations + bfos.runner`) görünü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 `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.
@ -298,7 +298,7 @@ Yerel ekranda çalıştırmak için oturum açık bir makinede deneyin:
[ Çı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:** 5 kategori **sekmesi** açılır. Her sekmede solda işlem listesi (sütunlar: `İşlem`, `Kapsam`, `Tür`), sağda `Seçenekler ve açıklama` paneli vardır; seçtiğin işlemin kapsamı, playbook yolu ve varsa zorunlu alanları orada görünür. Alt çubukta `Komutu göster ve çalıştır`, `Çıktıyı temizle`, `Çalışanı iptal et`, `Yalnız denetim (--check)`, `Çıkış` ve durum etiketi (`Hazır.`) bulunur.
| Sekme | İşlem sayısı | Yazma (yıkıcı) | Salt-okunur | | Sekme | İşlem sayısı | Yazma (yıkıcı) | Salt-okunur |
|---|---|---|---| |---|---|---|---|
@ -381,9 +381,7 @@ Kurulum seçenekleri: `Yerel çevrimdışı depodan kur (--offline)` ve `Zaten O
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 çubuktan** seçilir; işlem seçildiğinde ekran hedefi ayrıca sormaz, çubuktaki değeri kullanır:
```text ```text
HEDEF [ Detaylı mod ] [Detaylı mod] Hedef: [ 12010193 ] Bayi no (8 hane) [ pilot_1 ▾ ] Dalga [ ] Tüm filo (dikkat)
Bayi no (8 hane) [ 12010193 ] Dalga [ pilot_1 ▾ ] [ ] Tüm filo (dikkat)
Bayi no geçerli: BF-12010193
``` ```
| Hedef türü | Çubukta ne yaparsın | Örnek | Boyut | Tipik kullanım | | Hedef türü | Çubukta ne yaparsın | Örnek | Boyut | Tipik kullanım |
@ -418,9 +416,8 @@ Pencere açılır: `Blueforce Field OS — Yönetim` / `Blueforce saha cihazı y
1) Üst çubuktaki `Bayi no (8 hane)` alanına `12010193` yaz: 1) Üst çubuktaki `Bayi no (8 hane)` alanına `12010193` yaz:
```text ```text
HEDEF Hedef: [ 12010193 ] Bayi no (8 hane) [ lab ▾ ] Dalga [ ] Tüm filo (dikkat)
Bayi no (8 hane) [ 12010193 ] Dalga [ lab ▾ ] [ ] Tüm filo (dikkat) durum: Bayi no geçerli: BF-12010193
Bayi no geçerli: BF-12010193
``` ```
2) Basit modda `Kur` düğmesine bas. Seçim penceresi açılır (başlık: `Kurulum`): 2) Basit modda `Kur` düğmesine bas. Seçim penceresi açılır (başlık: `Kurulum`):
@ -528,7 +525,7 @@ 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 | Pencere açılıyor ama başlıkta 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ı | 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` 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 |
@ -556,8 +553,8 @@ Arayüz beklenen kullanımda **tam yetkili** çalışır: `sudo bf`. Kurulum bet
| Durum | Ne olur | | Durum | Ne olur |
|---|---| |---|---|
| `sudo bf` (tam yetki) | Başlık bandında yeşil `TAM YETKİ` rozeti. 48 işlemin tamamı çalışır: kurulum, filo bakımı, paket güncellemesi, kontrollü reboot ve `--check` koşuları | | `sudo bf` (tam yetki) | 48 işlemin tamamı çalışır: kurulum, filo bakımı, paket güncellemesi, kontrollü reboot ve `--check` koşuları |
| Root değil (yetkisiz açılış) | Başlık bandında turuncu `SALT-OKUNUR` rozeti; 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`. **35 salt-okunur işlem** çalışır (raporlar, envanter denetimi, bilgi ekranları, destek paketi); **13 yıkıcı işlem listede kalır ama kapalıdır** (`Tür` = `YIKICI`, `Durum` = `kapalı — sudo bf`) ve `Komutu göster ve çalıştır` düğmesi kapalı çizilip nedenini kendi yanında yazar | | Root değil (yetkisiz açılış) | Başlıkta turuncu uyarı şeridi: `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`. **35 salt-okunur işlem** çalışır (raporlar, envanter denetimi, bilgi ekranları, destek paketi); **13 yıkıcı işlem kapalıdır** |
| Yetkisizken yazma denenirse | Yazma işlemleri düğme düzeyinde kapalıdır; bir yol bulunup denenirse **çalıştırma katmanı** reddeder ve çıktı alanına `[ENGELLENDİ] Bu işlem yönetici yetkisi gerektirir (sudo bf).` yazılır, işlem çalışmaz (çıkış kodu 2) | | Yetkisizken yazma denenirse | Yazma işlemleri düğme düzeyinde kapalıdır; bir yol bulunup denenirse **çalıştırma katmanı** reddeder ve çıktı alanına `[ENGELLENDİ] Bu işlem yönetici yetkisi gerektirir (sudo bf).` yazılır, işlem çalışmaz (çıkış kodu 2) |
| `--check` kipi | Salt-okunur olduğu için yetki istemez: `Yalnız denetim (--check)` kutusu işaretliyse deneme koşusu yetkisiz oturumda da yapılabilir | | `--check` kipi | Salt-okunur olduğu için yetki istemez: `Yalnız denetim (--check)` kutusu işaretliyse deneme koşusu yetkisiz oturumda da yapılabilir |
@ -572,7 +569,6 @@ Planlama yetki istemez: komut önizlemesi, onay pencereleri ve `--check` üretim
| `sudo admin/bf-bootstrap.sh` iki kez koşulur | İkinci koşu hata üretmez (idempotent), araçlar yerinde, `/usr/local/bin/bf` bağlı | LAB(2) | | `sudo admin/bf-bootstrap.sh` iki kez koşulur | İkinci koşu hata üretmez (idempotent), araçlar yerinde, `/usr/local/bin/bf` bağlı | LAB(2) |
| `sudo bf --list` / `python3 -m bfos.ui_gui --list` | 48 işlem, 5 kategori, 13 YIKICI / 35 salt-okunur, pencere açılmadan listelenir | LAB(2) | | `sudo bf --list` / `python3 -m bfos.ui_gui --list` | 48 işlem, 5 kategori, 13 YIKICI / 35 salt-okunur, pencere açılmadan listelenir | LAB(2) |
| `sudo bf` açılır, `Çıkış` ile kapanır | Pencere açılır, hiçbir değişiklik yapılmaz | LAB(2) | | `sudo bf` açılır, `Çıkış` ile kapanır | Pencere açılır, hiçbir değişiklik yapılmaz | LAB(2) |
| `python3 tests/test_bfos_gui_static.py` | Tema belirteçleri `web/static/tokens.css` ile birebir aynı; durum sözlüğü beş sözcük; yetkisiz oturumda yazma işlemleri listede ve nedeni yazılı; pencere açılmadan koşar | LAB(2) |
| `sudo bf --tui` ile aynı işlem listesi | Terminal konsolu iki giriş noktası ayrışmadan çalışır | LAB(2) | | `sudo bf --tui` ile aynı işlem listesi | Terminal konsolu iki giriş noktası ayrışmadan çalışır | LAB(2) |
| Grafik ekran olmadan (`env -u DISPLAY -u WAYLAND_DISPLAY sudo bf`) | Türkçe hata + `sudo bf --tui` önerisi, çıkış kodu `3` | LAB(2) | | Grafik ekran olmadan (`env -u DISPLAY -u WAYLAND_DISPLAY sudo bf`) | Türkçe hata + `sudo bf --tui` önerisi, çıkış kodu `3` | LAB(2) |
| tkinter'siz ortamda başlatma | Türkçe hata + `python3-tk` ipucu + `--tui` önerisi, çıkış kodu `3`; kurulum sonu kapısı bunu WARN olarak kaydeder | LAB(2) | | tkinter'siz ortamda başlatma | Türkçe hata + `python3-tk` ipucu + `--tui` önerisi, çıkış kodu `3`; kurulum sonu kapısı bunu WARN olarak kaydeder | LAB(2) |
@ -607,7 +603,6 @@ Planlama yetki istemez: komut önizlemesi, onay pencereleri ve `--check` üretim
- [x] Filo işlemleri açık hedefle; wave-scoped filo akışı dalga dalga ilerliyor; production için `production` yazma zorunlu. - [x] Filo işlemleri açık hedefle; wave-scoped filo akışı dalga dalga ilerliyor; production için `production` yazma zorunlu.
- [x] Katalog, playbook kapsamıyla uyuşmazsa çalıştırmayı reddediyor (kapsam playbook dosyasından okunur). - [x] Katalog, playbook kapsamıyla uyuşmazsa çalıştırmayı reddediyor (kapsam playbook dosyasından okunur).
- [x] Yetki modeli: `sudo bf` tam yetki; yetkisiz açılışta yazma işlemleri kapalı ve çalıştırma katmanı `[ENGELLENDİ]` ile reddediyor. - [x] Yetki modeli: `sudo bf` tam yetki; yetkisiz açılışta yazma işlemleri kapalı ve çalıştırma katmanı `[ENGELLENDİ]` ile reddediyor.
- [x] Pencere `docs/DESIGN.md` yönergesine göre çiziliyor: koyu başlık bandı (ürün adı + hedef kimliği + cihaz durumu + yetki), beyaz paneller, `KURULDU`/`KAYITLI`/`HAZIR`/`HATA`/`YOK` rozetleri (sözcük + renk), tek çıkış kontrolü; renk ve boşluk değerleri `bfos/ui_theme.py` üzerinden `web/static/tokens.css`'ten geliyor ve statik testle karşılaştırılıyor.
- [x] Grafik ekran/tkinter yokluğunda Türkçe hata + `sudo bf --tui` önerisi ve çıkış kodu 3. - [x] Grafik ekran/tkinter yokluğunda Türkçe hata + `sudo bf --tui` önerisi ve çıkış kodu 3.
- [x] Şifre üretimi varsayılan **kapalı**; yalnız `bf-creds --generate` ile üretiliyor; parola ekrana/loga gitmiyor. - [x] Şifre üretimi varsayılan **kapalı**; yalnız `bf-creds --generate` ile üretiliyor; parola ekrana/loga gitmiyor.
- [x] `Cihaz işlemleri` içinde onaysız güncelleme seçeneği **yok**; `Bekleyen güvenlik güncellemeleri` yalnız rapor. - [x] `Cihaz işlemleri` içinde onaysız güncelleme seçeneği **yok**; `Bekleyen güvenlik güncellemeleri` yalnız rapor.

View file

@ -1,80 +0,0 @@
# Blueforce Field OS — Design System
Two operator surfaces, one visual language: the **device console** (Python/tkinter,
ships on each field PC) and the **central panel** (Python stdlib `http.server`,
browser). Both describe the same fleet from opposite ends, so they share a status
vocabulary and a token set. Palette values live in `web/static/tokens.css`; the
console reads the same values through `bfos/ui_theme.py`.
## Direction contract
**THESIS.** The device's state is the subject, and it must be legible from a step
back — on a workshop laptop, in daylight, with someone waiting. This surface
refuses the category default it would otherwise inherit: a near-black "ops
console" with grey body text, six hues fighting for meaning, and status buried in
a log. Instead: a light ground, dark text with real contrast, and saturation
spent only where it carries a state.
**OWN-WORLD.** Light neutral ground (`--bg-app #f6f7f8`) with white raised panels
and hairline borders; one dark region, the app header, which anchors the frame.
One action accent (`--accent #1d4ed8`) marking the action the operator came to
take and the selected row, nothing else. Four state severities — ok / warn /
danger / info — each with a strong and a soft form, so a badge or a row can be
built from one palette without inventing a second. Data (ids, counts, exit codes,
timestamps) set in monospace so columns align; UI text in the platform face.
Buttons and inputs share one height; rows share one height; a 4px space rhythm.
**STORY.** The operator sees at a glance whether the device (or the fleet) is
ready, what stage it reached, and what the one next action is. A technician
understands an install's outcome without reading the log; an operator scanning
700 devices finds the ones that need attention without opening them.
**FIRST VIEWPORT.** Header band (dark, 56px) carrying the product name, the
target identity, and privilege level on the right — privilege is never a buried
sentence. Below it, a two-column body: left, the fleet/device list with a state
badge per row, sorted so non-ready devices surface first, with a filter bar and a
summary strip of counts by state (the one large figure per view). Right, the
selected device's detail: state, target id, hostname, wave, and the channel
verification list. The primary action sits in the header, not below the fold.
**FORM.** Operate surface. The world is a *control room board*: fixed regions,
legible readouts, one accent, state as signal. No decorative motion; the only
transition is a state change and a row selection.
**FINISH.** unreviewed and undocumented is unfinished; this build ends with the
finish review, the verdict, DESIGN.md, and every shipping raster carrying its
provenance.
## Status vocabulary
One word per state, Turkish, used identically in both surfaces.
| State | Token | Meaning |
|---|---|---|
| `KURULDU` | `--state-provisioned` | Provisioned offline, not yet enrolled |
| `KAYITLI` | `--state-enrolled` | Enrolled, channels not all verified |
| `HAZIR` | `--state-ready` | Every channel verified; the only shippable state |
| `HATA` | `--state-failed` | A check failed; the device needs a person |
| `YOK` | `--state-absent` | Not reachable / never seen |
Colour is never the only carrier: every badge ships its word, and the fleet table
sorts non-ready first, so the list is readable in greyscale.
## Privilege
Read-only and full privilege are visually distinct states, not a log line: the
header states the current level, and the console disables rather than hides
write operations when unprivileged, with the reason on the control.
## Rules
1. Saturation carries meaning. A colour is a state or the action accent; it is
never decoration, and a fifth hue is a bug.
2. Badges carry words. Greyscale must not lose the state.
3. One loud figure per view. Everything else is quiet and aligned.
4. No webfont. The fleet installs offline; the UI face is whatever the platform
ships and the data face is monospace.
5. Destructive actions look different and ask twice. Approval and the root gate
are design, not an afterthought.
6. The log is evidence, not the interface. Outcomes surface as state and
structure first; raw output stays available, collapsed.

View file

@ -1,61 +0,0 @@
# UI work — remaining, measured
What is left after the first pass on the two operator surfaces. Each item was
observed on a real render, not inferred. Kept as a list so a gap is recorded
rather than remembered.
## Device console (`bfos/ui_gui.py`)
Reviewed against a screenshot of the running window (`spectacle`, 1440x900),
with the token values read back out of `bfos/ui_theme.py`.
**Landed** — grouping into headed regions; the privilege state moved out of a
monospace paragraph into the header and a banner; the duplicate `Çık` removed;
the output well styled as read-only; status colours from the shared tokens. The
theme file carries the same values as `web/static/tokens.css` (`#1d4ed8`,
`#15803d`, `#b45309`) — verified by comparison, not by assertion.
**Still true at the time of writing:**
1. **The widgets still read as default ttk.** Inputs, comboboxes, checkbuttons
and the scrollbar keep the renderer's native chrome. `ui_theme.py` selects
`clam` because it honours `background`/`bordercolor` on the elements it
restyles, but a themed *element* is not a redesigned control. Closing this
needs `Style.element_create` work per control, or hand-drawn widgets, and it
is the single largest remaining gap between this console and the web panel.
2. **The product name appears twice.** `root.title("Blueforce Field OS — Yönetim")`
and the in-window header band carry the same words. One of them should go;
the window title is what the taskbar and window switcher show, so the header
band is the better candidate.
3. **`Cihaz durumu: YOK` is ambiguous** when no target is selected: it reads as
a state a device is in rather than "nothing selected yet".
4. **`HAZIR` in the action bar looks like a disabled button** — it is a status
label, but it is styled in the same register as the controls beside it.
5. **The operation list has no unambiguous selected/disabled distinction.** The
first row carries a focus ring while being unavailable; the ring reads as
"actionable" and the reason is only in the row's trailing text.
## Central panel (`web/`)
Reviewed against served HTML and captured pages at 1440x900.
**Fixed in this pass** — the accent and the `KURULDU` state were the same blue
(`--state-provisioned: var(--info)`, and `--info` equals `--accent`), so a
selected row and a state badge were indistinguishable; both now resolve to
different colours, asserted by a test that resolves aliases before comparing.
The wave cell printed the bare code and then a label that already contains it
("P2 P2 — PILOT-2"); it now names each wave once.
**Still true:**
1. **No scroll affordance on the device table.** The page reports 13/13 while
only part of the list is in view, and nothing signals that more rows exist.
2. **The read-only privilege is repeated** in the header note and a second line
at the far right; one is enough.
3. **The `HAZIR` count appears in the loud figure and again in the breakdown.**
Defensible (summary then detail), but worth a decision rather than an
accident.
4. **Timestamps are ISO-8601** (`2026-09-16T06:20:00+03:00`). Precise and
sortable, but slow to scan for a human at a glance.
5. **A `HATA` device offers no next action** — the panel says a person is needed
and stops. For a fleet panel this is the workflow gap to close next.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more