feat(diagnostic): Satelliten und Host-Agenten umbenennen

Beide hiessen bisher so, wie es in IHRER .env steht. Umbenennen hiess also: auf
den Rechner, Datei aendern, Dienst neu starten — beim Fire TV sogar neu
provisionieren. In der Oberflaeche gab es dafuer gar keinen Weg.

Jetzt ein ✎ pro Eintrag. Der Alias liegt zentral in
/shared/config/display_names.json ({"host": {...}, "satellite": {...}}) und
wirkt sofort fuer JEDEN schon ausgerollten Agenten, ohne ihn anzufassen.

Der entscheidende Teil ist die Rueckrichtung: die Bridge loest den Alias beim
ADRESSIEREN wieder auf (_resolve_host / _resolve_satellite). Ohne das haette man
etwas umbenennen koennen und es danach nicht mehr ansprechen — der Agent
vergleicht die Adresse gegen seine EIGENE Kennung und kennt den Alias nicht.
Angesprochen werden kann weiterhin alles: Alias, Eigenname oder technische ID.

Der Eigenname bleibt in der Liste klein daneben sichtbar, damit man den Bezug
zum Geraet nicht verliert. Leerer Name entfernt den Alias.

Diagnostic und Bridge teilen sich die Datei ueber /shared — kein neuer
Nachrichtentyp, keine Allow-List-Aenderung, eine Wahrheit.

Nebenbei behoben: _display_name_set legte fest "/shared/config" an, obwohl der
Dateipfad konfigurierbar ist. Damit scheiterte das Schreiben stillschweigend,
sobald der Pfad abwich. Verzeichnis wird jetzt aus dem Pfad abgeleitet.

17 Pruefungen: Aliasvergabe, Liste, Aufloesung in beide Richtungen,
Gross/Kleinschreibung, alter Name bleibt gueltig, anderer Agent unberuehrt,
beide Bereiche in einer Datei ohne sich zu stoeren, Entfernen, unbekanntes Ziel
wird nicht still umgebogen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-04 18:54:36 +02:00
co-authored by Claude Opus 5
parent 6404b99510
commit 58d60adbaf
3 changed files with 221 additions and 11 deletions
+143 -7
View File
@@ -4769,6 +4769,18 @@ class ARIABridge:
int(data.get("port") or 0),
int(data.get("localPort") or 0))
await _send_response(writer, 200, res)
elif method == "POST" and path == "/internal/display-name":
# Anzeigename fuer einen Host-Agenten oder Satelliten.
# body: {kind: host|satellite, id, name}
try:
data = json.loads(body.decode("utf-8", "ignore"))
except Exception as exc:
await _send_response(writer, 400, {"error": f"bad json: {exc}"})
return
res = self._display_name_set((data.get("kind") or "").strip(),
(data.get("id") or "").strip(),
data.get("name") or "")
await _send_response(writer, 200, res)
elif method == "POST" and path == "/internal/satellite-name":
# Brain/Diagnostic: Geraetenamen vergeben. body: {satellite, mac|ip, name}
try:
@@ -5156,15 +5168,37 @@ class ARIABridge:
def _satellite_list(self) -> list[dict]:
"""Bekannte Satelliten (frisch = in den letzten 5 Min gesehen)."""
now = time.time()
aliases = self._display_names("satellite")
out = []
for s in self._satellites.values():
own = s.get("location") or s["id"]
alias = aliases.get(s["id"], "")
out.append({
"id": s["id"], "location": s.get("location") or s["id"],
"id": s["id"], "location": alias or own,
"agentName": own, "alias": alias,
"caps": s.get("caps") or [], "control": bool(s.get("control")),
"online": (now - s.get("last_seen", 0)) < 300,
})
return out
def _resolve_satellite(self, who: str) -> str:
"""Anzeigename ODER echte Kennung -> echte Kennung.
Gleicher Grund wie bei _resolve_host: der Satellit vergleicht die
Adresse gegen seine eigene ID/Location und kennt den Alias nicht.
"""
w = (who or "").strip()
if not w or w in self._satellites:
return w
wl = w.lower()
for sid, alias in self._display_names("satellite").items():
if alias.strip().lower() == wl:
return sid
for sid, s in self._satellites.items():
if (s.get("location") or "").strip().lower() == wl:
return sid
return w
# worker_ping kommt alle ~10s; nach 35s ohne Ping gilt ein Worker als offline.
WORKER_OFFLINE_S = 35
@@ -5228,6 +5262,8 @@ class ARIABridge:
Muster identisch zu _flux_generate (requestId → Future)."""
if self.ws_rvs is None:
return {"ok": False, "error": "RVS-Verbindung nicht aktiv"}
# Anzeigename -> echte Kennung, sonst erkennt sich der Satellit nicht.
satellite = self._resolve_satellite(satellite)
request_id = str(uuid.uuid4())
loop = asyncio.get_event_loop()
future: asyncio.Future = loop.create_future()
@@ -5255,15 +5291,113 @@ class ARIABridge:
finally:
self._pending_sat.pop(request_id, None)
# ── Anzeigenamen fuer Host-Agenten ───────────────────────────────
#
# Der Name eines Agenten kommt von ihm selbst: beim Desktop-Agenten aus
# HOST_NAME in seiner .env, beim Android-Agenten aus der App-Konfiguration.
# Umbenennen hiesse also: auf den Rechner, Datei aendern, neu starten — bzw.
# beim Fire TV neu provisionieren.
#
# Darum hier ein Alias: hostId -> Anzeigename, zentral in der Bridge. Wirkt
# sofort und fuer JEDEN schon ausgerollten Agenten, ohne ihn anzufassen.
#
# WICHTIG: der Alias wird beim Adressieren wieder aufgeloest (siehe
# _resolve_host). Sonst koennte man einen Agenten zwar umbenennen, ihn unter
# dem neuen Namen aber nicht mehr ansprechen — er vergleicht intern gegen
# seine EIGENE Kennung.
# Eine Datei fuer beides: {"host": {id: name}, "satellite": {id: name}}
_DISPLAY_NAMES_FILE = "/shared/config/display_names.json"
def _display_names(self, kind: str) -> dict:
try:
with open(self._DISPLAY_NAMES_FILE) as f:
data = json.load(f)
sub = (data or {}).get(kind)
return sub if isinstance(sub, dict) else {}
except FileNotFoundError:
return {}
except Exception as exc:
logger.warning("[names] display_names.json unlesbar: %s", exc)
return {}
def _display_name_set(self, kind: str, ident: str, name: str) -> dict:
ident = (ident or "").strip()
if kind not in ("host", "satellite"):
return {"ok": False, "error": "kind muss host oder satellite sein"}
if not ident:
return {"ok": False, "error": "id ist Pflicht"}
try:
with open(self._DISPLAY_NAMES_FILE) as f:
data = json.load(f)
if not isinstance(data, dict):
data = {}
except Exception:
data = {}
sub = data.get(kind) if isinstance(data.get(kind), dict) else {}
if (name or "").strip():
sub[ident] = (name or "").strip()[:60]
else:
sub.pop(ident, None)
data[kind] = sub
try:
# Verzeichnis aus dem Dateipfad ableiten statt fest zu verdrahten —
# sonst laesst sich _DISPLAY_NAMES_FILE nicht umbiegen (Test, anderes
# Deployment) und das Schreiben scheitert stillschweigend.
os.makedirs(os.path.dirname(self._DISPLAY_NAMES_FILE) or ".", exist_ok=True)
tmp = self._DISPLAY_NAMES_FILE + ".tmp"
with open(tmp, "w") as f:
json.dump(data, f, indent=2)
os.replace(tmp, self._DISPLAY_NAMES_FILE)
except Exception as exc:
return {"ok": False, "error": str(exc)}
logger.info("[names] %s %s -> %r", kind, ident, sub.get(ident) or "(entfernt)")
return {"ok": True, "kind": kind, "id": ident, "name": sub.get(ident, "")}
def _host_names(self) -> dict:
return self._display_names("host")
def _resolve_host(self, who: str) -> str:
"""Anzeigename ODER echte Kennung -> echte Kennung.
Ohne das waere ein umbenannter Agent nicht mehr ansprechbar: er
vergleicht die Adresse gegen seine eigene HOST_ID/HOST_NAME und kennt
den Alias nicht.
"""
w = (who or "").strip()
if not w:
return w
if w in self._hosts:
return w
wl = w.lower()
for hid, alias in self._host_names().items():
if alias.strip().lower() == wl:
return hid
for hid, h in self._hosts.items():
if (h.get("name") or "").strip().lower() == wl:
return hid
return w
def _host_list(self) -> list[dict]:
"""Bekannte Host-Agenten (frisch = in den letzten 5 Min gesehen)."""
now = time.time()
return [{
"hostId": h["hostId"], "name": h.get("name") or h["hostId"],
"os": h.get("os") or "", "caps": h.get("caps") or [],
"control": bool(h.get("control")),
"online": (now - h.get("last_seen", 0)) < 300,
} for h in self._hosts.values()]
aliases = self._host_names()
out = []
for h in self._hosts.values():
hid = h["hostId"]
own = h.get("name") or hid
alias = aliases.get(hid, "")
out.append({
"hostId": hid,
"name": alias or own,
# Eigener Name des Agenten — damit in der Oberflaeche sichtbar
# bleibt, dass da ein Alias drueberliegt.
"agentName": own,
"alias": alias,
"os": h.get("os") or "", "caps": h.get("caps") or [],
"control": bool(h.get("control")),
"online": (now - h.get("last_seen", 0)) < 300,
})
return out
_AGENT_HISTORY_DIR = "/shared/config/agent_history"
_AGENT_HISTORY_MAX = 200
@@ -5317,6 +5451,8 @@ class ARIABridge:
host_result. Muster identisch zu _satellite_request."""
if self.ws_rvs is None:
return {"ok": False, "error": "RVS-Verbindung nicht aktiv"}
# Anzeigename -> echte Kennung, sonst erkennt sich der Agent nicht.
host = self._resolve_host(host)
request_id = str(uuid.uuid4())
loop = asyncio.get_event_loop()
future: asyncio.Future = loop.create_future()