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

117 lines
4.0 KiB
Python

"""Servergruppen und Zielrouting (PLAN.md §6.3 Bedienmodelle, §10.1 ServerGroup).
- Coordinator routet Commands an `All`, eine `ServerGroup`, einen einzelnen
`Node` oder einen `Output` (§6.3 Control-Center-Modell)
- Zielregel je Gruppe: all | selected | tag_query | feste Node-Liste
- Zielauflösung ist deterministisch und testbar; Gruppenänderungen zeigen
vor dem Commit, welche Nodes sie erhalten (§17.5)
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import StrEnum
class TargetKind(StrEnum):
"""Command-Ziele (§6.3): All, ServerGroup, Node oder Output."""
ALL = "all"
SERVER_GROUP = "server_group"
NODE = "node"
OUTPUT = "output"
class GroupRule(StrEnum):
"""Zielregel je ServerGroup (§10.1)."""
ALL = "all"
SELECTED = "selected"
TAG_QUERY = "tag_query"
NODE_LIST = "node_list"
@dataclass
class ServerGroup:
"""Servergruppe (§10.1): Node-Auswahl mit Zielregel.
- node_ids: feste Mitglieder bei SELECTED/NODE_LIST
- tags: Node-Tags für TAG_QUERY (Node-Einträge tragen passende Tags)
- output_map: optionaler Layer-/Output-Zuordnungshinweis (§10.1)
"""
id: str
name: str
rule: GroupRule = GroupRule.SELECTED
node_ids: frozenset[str] = field(default_factory=frozenset)
tags: frozenset[str] = field(default_factory=frozenset)
output_map: dict[str, str] = field(default_factory=dict) # layer_id → output_id
class GroupRouter:
"""Löst Command-Ziele auf Node-Mengen auf (§6.3).
Der Coordinator nutzt resolve() vor jedem Senden; die UI kann
resolve() für die Commit-Vorschau verwenden (§17.5: „welche Nodes
erhalten dies?").
"""
def __init__(self) -> None:
self._groups: dict[str, ServerGroup] = {}
self._node_tags: dict[str, frozenset[str]] = {} # node_id → tags
self._node_outputs: dict[str, list[str]] = {} # node_id → output_ids
# ---------- Verwaltung ----------
def upsert_group(self, group: ServerGroup) -> None:
self._groups[group.id] = group
def remove_group(self, group_id: str) -> None:
self._groups.pop(group_id, None)
def get_group(self, group_id: str) -> ServerGroup | None:
return self._groups.get(group_id)
def set_node_tags(self, node_id: str, tags: frozenset[str]) -> None:
self._node_tags[node_id] = frozenset(tags)
def set_node_outputs(self, node_id: str, output_ids: list[str]) -> None:
self._node_outputs[node_id] = list(output_ids)
def known_nodes(self) -> frozenset[str]:
return frozenset(self._node_tags)
# ---------- Zielauflösung (§6.3) ----------
def resolve(self, kind: TargetKind, target_id: str | None = None) -> frozenset[str]:
"""Löst ein Ziel auf eine Node-Menge auf; leer bei unbekanntem Ziel."""
if kind is TargetKind.ALL:
return self.known_nodes()
if kind is TargetKind.NODE:
return frozenset({target_id}) if target_id in self._node_tags else frozenset()
if kind is TargetKind.OUTPUT:
return frozenset(
node_id
for node_id, outputs in self._node_outputs.items()
if target_id in outputs
)
if kind is TargetKind.SERVER_GROUP:
group = self._groups.get(target_id or "")
if group is None:
return frozenset()
if group.rule is GroupRule.ALL:
return self.known_nodes()
if group.rule is GroupRule.SELECTED or group.rule is GroupRule.NODE_LIST:
return group.node_ids & self.known_nodes()
if group.rule is GroupRule.TAG_QUERY:
return frozenset(
node_id
for node_id, tags in self._node_tags.items()
if group.tags & tags
)
return frozenset()
def preview_targets(self, kind: TargetKind, target_id: str | None = None) -> frozenset[str]:
"""Commit-Vorschau: identisch zu resolve (§17.5)."""
return self.resolve(kind, target_id)