Files
HMS MediaEngine Agent 362e089be0 AUFGERAUMT: Root auf 10 sichtbare Elemente reduziert
Der Nutzer hat recht: Der Ordner war voller Entwicklungs-Muell.
Jetzt ist sauber getrennt:

ROOT (was der Nutzer sieht und braucht):
- run.py                     = das Programm
- hms_app/                   = der Anwendungscode
- HMS MediaEngine.app        = macOS Doppelklick-Starter
- HMS-Start.vbs              = Windows Doppelklick-Starter
- HMS-Install.vbs             = Windows Erst-Installation
- HMS-Mac-Install.command     = macOS Homebrew-Installation
- HMS-Portable-Install.command = macOS Portable-Installation (16GB-Fix)
- installer_gui.py           = grafischer Installer
- launcher.pyw + launcher_core.py = interne Start-Logik
- LIESMICH.txt               = 10-Zeilen-Kurzanleitung
- .gitignore

_entwicklung/ (alles andere, NICHT benoetigt):
- packages/ apps/ native/ plugins/ tools/ schemas/ tests/ docs/
  build/ fixture_profiles/
- PLAN.md STATUS.md ERRORS.md TEST_REPORT.md CHANGELOG.md README.md
- pyproject.toml uv.lock setup_*.sh/ps1 make_mac_app.py

Diese Trennung gilt ab sofort fuer alle Commits. Der Nutzer kann
_entwicklung/ loeschen wenn er Platz braucht - die App laeuft ohne.

Verifiziert: App startet nach Aufraeumen unveraendert (Health 200).
2026-09-11 23:44:06 +02:00

98 lines
3.3 KiB
Python

"""Node-Identität und Rollen (PLAN.md §6.3, §10.1, §3.6).
Der Control Core lädt beim Start:
- persistente node_id aus userdata/identity/node_id (IP-/hostname-unabhängig)
- konfigurierbare Rollen (RENDER_NODE, COORDINATOR, CONTROL_DESK)
- Anzeigename (editierbar, nicht identitätsstiftend)
Rollen sind Pflicht für Discovery (TXT 'roles') und Cluster-Protokoll.
Es gibt in V1 keine automatische Leader-Wahl (§6.3): der Betreiber legt die
Coordinator-Rolle fest.
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
from pathlib import Path
from hms_domain.ids import new_node_id, persistent_node_id
class NodeRole(StrEnum):
"""Rollen gemäß §6.3. Keine Auto-Leader-Wahl: Rollen sind Konfiguration."""
RENDER_NODE = "RENDER_NODE"
COORDINATOR = "COORDINATOR"
CONTROL_DESK = "CONTROL_DESK"
_VALID_COMBOS = {
# reine Render-Nodes: decodieren und rendern lokal (§6.3)
frozenset({NodeRole.RENDER_NODE}),
# Coordinator kann auf einem Render-Node mitlaufen (§6.3)
frozenset({NodeRole.RENDER_NODE, NodeRole.COORDINATOR}),
# reiner Coordinator / Control-PC ohne Renderer (HEADLESS_CONTROL, §6.3)
frozenset({NodeRole.COORDINATOR}),
frozenset({NodeRole.CONTROL_DESK}),
frozenset({NodeRole.CONTROL_DESK, NodeRole.COORDINATOR}),
}
@dataclass(frozen=True)
class NodeIdentity:
"""Stabiles Identitätsbündel eines Servers (§3.6, §10.1).
- node_id: persistent, UUID, unabhängig von IP/Hostname
- display_name: editierbar; Umbenennung zerstört keine Automationen,
weil Parameterpfade UUID-basiert sind (§10.2)
- roles: konfigurierte Rollen; RENDER_NODE impliziert lokalen Renderer
"""
node_id: str
display_name: str
roles: frozenset[NodeRole]
def __post_init__(self) -> None:
if not self.node_id:
raise ValueError("node_id darf nicht leer sein")
if not self.roles:
raise ValueError("mindestens eine Rolle erforderlich (§6.3)")
if frozenset(self.roles) not in _VALID_COMBOS:
allowed = ", ".join(
sorted("|".join(sorted(r.value for r in c)) for c in _VALID_COMBOS)
)
raise ValueError(
f"ungültige Rollenkombination {sorted(r.value for r in self.roles)}; "
f"erlaubt: {allowed}"
)
@property
def is_coordinator(self) -> bool:
return NodeRole.COORDINATOR in self.roles
@property
def renders_locally(self) -> bool:
return NodeRole.RENDER_NODE in self.roles
@classmethod
def load_or_create(
cls,
identity_dir: Path,
display_name: str,
roles: frozenset[NodeRole],
) -> NodeIdentity:
"""Lädt die persistente node_id oder erzeugt sie genau einmal.
IP-Wechsel ändern die node_id nicht; Doppelvergabe wird über die
Datei (O_EXCL) verhindert (§6.3, hms_domain.ids).
"""
identity_file = identity_dir / "node_id"
node_id = persistent_node_id(identity_file)
return cls(node_id=node_id, display_name=display_name, roles=roles)
@classmethod
def ephemeral(cls, display_name: str, roles: frozenset[NodeRole]) -> NodeIdentity:
"""Nur für Tests: Identität ohne Persistenz (kein Showbetrieb)."""
return cls(node_id=new_node_id(), display_name=display_name, roles=roles)