Python Code Style
Rules
- Functions NEVER call sys.exit(). Return None, raise exception, or return error code.
- .env parsing MUST strip surrounding quotes from values.
- Every function MUST have type hints.
- Use Path from pathlib for file operations.
- Logging: % formatting for logs, f-strings for non-logging.
Patterns
Function error handling
def find_ansible_playbook(playbook: str) -> str:
if not found:
raise FileNotFoundError(f"Playbook not found: {playbook}")
return path
def get_cached_address() -> str | None:
path = Path('.state/addresses.json')
if not path.exists():
return None
return json.loads(path.read_text()).get('PRIMARY_HOST')
def probe_host(host: str, port: int, timeout: int = 5) -> bool:
try:
with socket.create_connection((host, port), timeout):
return True
except (socket.timeout, ConnectionRefusedError):
return False
.env quote stripping
value = value.strip()
if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
value = value[1:-1]
env[key.strip()] = value
Path handling
def ensure_file_exists(path: Path) -> Path:
path = path.expanduser().resolve()
if not path.is_file():
raise FileNotFoundError(f"Not a file: {path}")
return path
def ensure_dir(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
return path
Dictionary merging
def merge_dicts(*dicts: dict) -> dict:
result = {}
for d in dicts:
result.update(d)
return result
config = merge_dicts(default_config, env_config, cli_config)
Logging
import logging
log = logging.getLogger(__name__)
log.debug("Probing host %s", host) # Use % formatting
msg = f"Built command for {playbook}" # OK for non-logging
Domain model classes (MVC pattern)
ALWAYS separate business logic from UI rendering using domain classes in
the data layer. Domain objects own derived state — the UI only reads properties.
# Domain class in data.py — owns identity, deploy history, AND live telemetry
class Host:
def __init__(self, name: str, ip: str, *, is_lan: bool = False) -> None:
self.name = name
self.ip = ip
self.is_lan = is_lan
self.deploys: list[DeployRecord] = []
self.telemetry: HostTelemetry | None = None
def attach_telemetry(self, t: HostTelemetry) -> None:
self.telemetry = t
self._guests = _parse_guests(t.services)
@property
def healthy(self) -> bool: ... # from deploy history
@property
def errors(self) -> list[str]: ... # deploy + offline errors
@property
def online(self) -> bool: ... # from telemetry (False if None)
@property
def disk_pct(self) -> float: ... # 0.0 if no telemetry
@property
def guests(self) -> list[GuestInfo]: ... # [] if no telemetry
# Aggregate class — delegates to children, adds fleet-wide metrics
class Fleet:
def __init__(self, hosts: list[Host]) -> None:
self.hosts = hosts
@property
def healthy(self) -> bool: ...
@property
def online_count(self) -> int: ...
@property
def total_guests(self) -> int: ...
@property
def health_score(self) -> int: ... # 0-100
@property
def worst_disk(self) -> Host | None: ...
def get_host(self, name: str) -> Host | None: ...
# Factory function — wires ALL data sources into domain objects
def build_fleet(env: dict[str, str], state_dir: Path) -> Fleet:
hosts = [Host(name=i.name, ip=i.ip, ...) for i in get_known_hosts(env)]
for record in load_deploy_history(state_dir):
for host in hosts:
if _deploy_targets_host(record, host.name):
host.deploys.append(record)
for node in load_node_registry(state_dir):
host = ... # match by hostname
if host:
host.attach_telemetry(HostTelemetry(...))
return Fleet(hosts)
# UI page — thin renderer, ZERO business logic
def _deploy_card(fleet: Fleet) -> None:
if fleet.healthy:
ui.badge("success", color="green")
else:
for err in fleet.errors:
ui.label(err)
Rules:
- NEVER compute health/status in page modules. Use
.healthy / .errors properties.
- ALWAYS put exit code mapping, status computation, and error descriptions in
data.py.
- ALWAYS expose
.healthy -> bool and .errors -> list[str] on domain objects.
- ALWAYS use factory functions to wire raw data sources into domain objects.
- ALWAYS return graceful defaults when
telemetry is None (0.0, [], False, "--").
- NEVER read
RegisteredNode directly in page modules — use Host/Fleet.
- Test domain logic with plain pytest — no NiceGUI required.
UI string constants (NiceGUI web UI)
# ALWAYS centralize UI strings as class constants in data.py
class Labels:
START_DEPLOY = "Start Deploy" # used by both page module AND tests
# Page module:
ui.button(Labels.START_DEPLOY, ...)
# Test:
user.find(Labels.START_DEPLOY).click()
# NEVER duplicate strings across page + test:
# BAD: ui.button("Start Deploy") + user.find("Start Deploy")
# GOOD: both reference Labels.START_DEPLOY
Previous bugs
- sys.exit() in functions → untestable, breaks composition
- Missing quote stripping → SSH fails with literal "192.168.1.100"
- Missing type hints → MyPy errors
- Broad
except Exception in UI code caught too broadly. Use specific exceptions.
format_last_seen_relative crashed on timezone-aware timestamps. Fix: .replace(tzinfo=None).
- 200+ magic strings duplicated between page modules and 6 test files. Changing a
label required updating tests in multiple files. Fix: centralized into
data.py
constants (Routes, PageTitles, Labels, ApiRoutes).
- Health computation (exit code mapping, deploy history analysis) was embedded in
dashboard.py UI functions. Couldn't test health logic without NiceGUI. Fix:
extracted
Host/Fleet domain classes into data.py with .healthy/.errors
properties. Dashboard became a thin renderer. 29 pure-Python tests cover all
health scenarios.
- Redundant
"Host" forward reference in kickstart_callhome(host: "Host") when
from __future__ import annotations is already at the top. With postponed
annotations, all annotations are strings by default — quoting is unnecessary.
kickstart_callhome returned success=True even with partial container restart
failures (errors collected in errors list). This is intentional — success
means "SSH connected and discovered containers"; errors captures per-container
issues. Document this semantic in the docstring when success has a nuanced
meaning.
HostRegistry.register() is the single write path to registry.json. NEVER
duplicate upsert logic. Every caller (env seeding, manual form, TEST_UNITS)
delegates to register(). Immutable-on-create fields (bucket, source,
first_seen) are preserved on upsert. MAC match takes precedence over name
match for identity resolution.
- NEVER register container/service heartbeats into HostRegistry. The registry
is for PHYSICAL HOSTS only (Proxmox nodes). Container heartbeats go into
nodes.json (telemetry) via register_checkin(). build_fleet() groups
container telemetry under parent hosts — containers never appear as
independent fleet members. Previous bug: register_checkin() called
HostRegistry.register() for every heartbeat. Containers (pihole,
wireguard, netdata, etc.) appeared as independent hosts on the dashboard
with LAN IPs (10.10.10.x) instead of being grouped under their physical
host. Fixed by removing the HostRegistry.register() call from
register_checkin().
- 4-tier architecture: Container → NodeManager → ClusterManager → SuperManager.
Containers heartbeat to their local NodeManager. NodeManagers aggregate and
relay to their ClusterManager. ClusterManagers relay to the SuperManager.
NEVER let containers POST directly to the SuperManager's
/api/checkin
in the final architecture.
- NEVER hardcode ports. Use
WEBUI_PORT env var (default 40500). Ports in the
ephemeral range (32768-60999) can collide with outbound connections. Choose a
port in the firewall's allowed range and set it in .env/test.env.
- NEVER duplicate
build.py:get_controller_ip(). Import from build module.
- Deploy output MUST persist to a log file (
.state/deploy_output.log), not just
stream to a NiceGUI ui.log element. Browser disconnects kill the coroutine
and lose all output. The log file is the source of truth.
- NiceGUI UI callbacks (
log.push, label.text = ...) throw RuntimeError when
the browser session is gone. Use a _safe_ui(fn, *args) helper to wrap these
in a single try/except RuntimeError. NEVER scatter bare try/except blocks.
- When importing
Labels in a page module, ALWAYS use the constants for button text
instead of hardcoding strings. If you import Labels but use hardcoded strings,
the import is dead AND the string is untracked. Previous bug: bridge.py and
images.py imported Labels but used hardcoded "Refresh Now", "Build Selected",
etc. — found by dead-import audit, fixed by replacing strings with constants.
- NEVER import a name and leave it unused. If the page needs
Labels.REFRESH_NOW,
import Labels AND use it. If it doesn't need Labels, don't import it.
- Dead code audit checklist: (1) unused imports, (2) functions defined but never
called, (3) local variables assigned but never read, (4) stale docstrings
mentioning old port numbers or URLs, (5) backward-compat fallbacks for removed
features.
1---2name: python-code-style3description: Python code conventions. Functions return errors, .env parsing strips quotes, type hints required.4---56# Python Code Style78## Rules9101. Functions NEVER call sys.exit(). Return None, raise exception, or return error code.112. .env parsing MUST strip surrounding quotes from values.123. Every function MUST have type hints.134. Use Path from pathlib for file operations.145. Logging: % formatting for logs, f-strings for non-logging.1516## Patterns1718### Function error handling1920```python21def find_ansible_playbook(playbook: str) -> str:22 if not found:23 raise FileNotFoundError(f"Playbook not found: {playbook}")24 return path2526def get_cached_address() -> str | None:27 path = Path('.state/addresses.json')28 if not path.exists():29 return None30 return json.loads(path.read_text()).get('PRIMARY_HOST')3132def probe_host(host: str, port: int, timeout: int = 5) -> bool:33 try:34 with socket.create_connection((host, port), timeout):35 return True36 except (socket.timeout, ConnectionRefusedError):37 return False38```3940### .env quote stripping4142```python43value = value.strip()44if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):45 value = value[1:-1]46env[key.strip()] = value47```4849### Path handling5051```python52def ensure_file_exists(path: Path) -> Path:53 path = path.expanduser().resolve()54 if not path.is_file():55 raise FileNotFoundError(f"Not a file: {path}")56 return path5758def ensure_dir(path: Path) -> Path:59 path.mkdir(parents=True, exist_ok=True)60 return path61```6263### Dictionary merging6465```python66def merge_dicts(*dicts: dict) -> dict:67 result = {}68 for d in dicts:69 result.update(d)70 return result7172config = merge_dicts(default_config, env_config, cli_config)73```7475### Logging7677```python78import logging79log = logging.getLogger(__name__)8081log.debug("Probing host %s", host) # Use % formatting82msg = f"Built command for {playbook}" # OK for non-logging83```8485### Domain model classes (MVC pattern)8687ALWAYS separate business logic from UI rendering using domain classes in88the data layer. Domain objects own derived state — the UI only reads properties.8990```python91# Domain class in data.py — owns identity, deploy history, AND live telemetry92class Host:93 def __init__(self, name: str, ip: str, *, is_lan: bool = False) -> None:94 self.name = name95 self.ip = ip96 self.is_lan = is_lan97 self.deploys: list[DeployRecord] = []98 self.telemetry: HostTelemetry | None = None99100 def attach_telemetry(self, t: HostTelemetry) -> None:101 self.telemetry = t102 self._guests = _parse_guests(t.services)103104 @property105 def healthy(self) -> bool: ... # from deploy history106 @property107 def errors(self) -> list[str]: ... # deploy + offline errors108 @property109 def online(self) -> bool: ... # from telemetry (False if None)110 @property111 def disk_pct(self) -> float: ... # 0.0 if no telemetry112 @property113 def guests(self) -> list[GuestInfo]: ... # [] if no telemetry114115# Aggregate class — delegates to children, adds fleet-wide metrics116class Fleet:117 def __init__(self, hosts: list[Host]) -> None:118 self.hosts = hosts119120 @property121 def healthy(self) -> bool: ...122 @property123 def online_count(self) -> int: ...124 @property125 def total_guests(self) -> int: ...126 @property127 def health_score(self) -> int: ... # 0-100128 @property129 def worst_disk(self) -> Host | None: ...130131 def get_host(self, name: str) -> Host | None: ...132133# Factory function — wires ALL data sources into domain objects134def build_fleet(env: dict[str, str], state_dir: Path) -> Fleet:135 hosts = [Host(name=i.name, ip=i.ip, ...) for i in get_known_hosts(env)]136 for record in load_deploy_history(state_dir):137 for host in hosts:138 if _deploy_targets_host(record, host.name):139 host.deploys.append(record)140 for node in load_node_registry(state_dir):141 host = ... # match by hostname142 if host:143 host.attach_telemetry(HostTelemetry(...))144 return Fleet(hosts)145146# UI page — thin renderer, ZERO business logic147def _deploy_card(fleet: Fleet) -> None:148 if fleet.healthy:149 ui.badge("success", color="green")150 else:151 for err in fleet.errors:152 ui.label(err)153```154155Rules:156- NEVER compute health/status in page modules. Use `.healthy` / `.errors` properties.157- ALWAYS put exit code mapping, status computation, and error descriptions in `data.py`.158- ALWAYS expose `.healthy -> bool` and `.errors -> list[str]` on domain objects.159- ALWAYS use factory functions to wire raw data sources into domain objects.160- ALWAYS return graceful defaults when `telemetry is None` (0.0, [], False, "--").161- NEVER read `RegisteredNode` directly in page modules — use `Host`/`Fleet`.162- Test domain logic with plain pytest — no NiceGUI required.163164### UI string constants (NiceGUI web UI)165166```python167# ALWAYS centralize UI strings as class constants in data.py168class Labels:169 START_DEPLOY = "Start Deploy" # used by both page module AND tests170171# Page module:172ui.button(Labels.START_DEPLOY, ...)173174# Test:175user.find(Labels.START_DEPLOY).click()176177# NEVER duplicate strings across page + test:178# BAD: ui.button("Start Deploy") + user.find("Start Deploy")179# GOOD: both reference Labels.START_DEPLOY180```181182## Previous bugs183184- sys.exit() in functions → untestable, breaks composition185- Missing quote stripping → SSH fails with literal "192.168.1.100"186- Missing type hints → MyPy errors187- Broad `except Exception` in UI code caught too broadly. Use specific exceptions.188- `format_last_seen_relative` crashed on timezone-aware timestamps. Fix: `.replace(tzinfo=None)`.189- 200+ magic strings duplicated between page modules and 6 test files. Changing a190 label required updating tests in multiple files. Fix: centralized into `data.py`191 constants (`Routes`, `PageTitles`, `Labels`, `ApiRoutes`).192- Health computation (exit code mapping, deploy history analysis) was embedded in193 dashboard.py UI functions. Couldn't test health logic without NiceGUI. Fix:194 extracted `Host`/`Fleet` domain classes into `data.py` with `.healthy`/`.errors`195 properties. Dashboard became a thin renderer. 29 pure-Python tests cover all196 health scenarios.197- Redundant `"Host"` forward reference in `kickstart_callhome(host: "Host")` when198 `from __future__ import annotations` is already at the top. With postponed199 annotations, all annotations are strings by default — quoting is unnecessary.200- `kickstart_callhome` returned `success=True` even with partial container restart201 failures (errors collected in `errors` list). This is intentional — `success`202 means "SSH connected and discovered containers"; `errors` captures per-container203 issues. Document this semantic in the docstring when `success` has a nuanced204 meaning.205- `HostRegistry.register()` is the single write path to `registry.json`. NEVER206 duplicate upsert logic. Every caller (env seeding, manual form, TEST_UNITS)207 delegates to `register()`. Immutable-on-create fields (bucket, source,208 first_seen) are preserved on upsert. MAC match takes precedence over name209 match for identity resolution.210- NEVER register container/service heartbeats into HostRegistry. The registry211 is for PHYSICAL HOSTS only (Proxmox nodes). Container heartbeats go into212 `nodes.json` (telemetry) via `register_checkin()`. `build_fleet()` groups213 container telemetry under parent hosts — containers never appear as214 independent fleet members. Previous bug: `register_checkin()` called215 `HostRegistry.register()` for every heartbeat. Containers (pihole,216 wireguard, netdata, etc.) appeared as independent hosts on the dashboard217 with LAN IPs (10.10.10.x) instead of being grouped under their physical218 host. Fixed by removing the `HostRegistry.register()` call from219 `register_checkin()`.220- 4-tier architecture: Container → NodeManager → ClusterManager → SuperManager.221 Containers heartbeat to their local NodeManager. NodeManagers aggregate and222 relay to their ClusterManager. ClusterManagers relay to the SuperManager.223 NEVER let containers POST directly to the SuperManager's `/api/checkin`224 in the final architecture.225- NEVER hardcode ports. Use `WEBUI_PORT` env var (default 40500). Ports in the226 ephemeral range (32768-60999) can collide with outbound connections. Choose a227 port in the firewall's allowed range and set it in `.env`/`test.env`.228- NEVER duplicate `build.py:get_controller_ip()`. Import from `build` module.229- Deploy output MUST persist to a log file (`.state/deploy_output.log`), not just230 stream to a NiceGUI `ui.log` element. Browser disconnects kill the coroutine231 and lose all output. The log file is the source of truth.232- NiceGUI UI callbacks (`log.push`, `label.text = ...`) throw `RuntimeError` when233 the browser session is gone. Use a `_safe_ui(fn, *args)` helper to wrap these234 in a single `try/except RuntimeError`. NEVER scatter bare try/except blocks.235- When importing `Labels` in a page module, ALWAYS use the constants for button text236 instead of hardcoding strings. If you import `Labels` but use hardcoded strings,237 the import is dead AND the string is untracked. Previous bug: `bridge.py` and238 `images.py` imported `Labels` but used hardcoded `"Refresh Now"`, `"Build Selected"`,239 etc. — found by dead-import audit, fixed by replacing strings with constants.240- NEVER import a name and leave it unused. If the page needs `Labels.REFRESH_NOW`,241 import `Labels` AND use it. If it doesn't need Labels, don't import it.242- Dead code audit checklist: (1) unused imports, (2) functions defined but never243 called, (3) local variables assigned but never read, (4) stale docstrings244 mentioning old port numbers or URLs, (5) backward-compat fallbacks for removed245 features.