diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index f324811..378ed67 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -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() diff --git a/diagnostic/index.html b/diagnostic/index.html index 4ba62ab..a83dc59 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -5045,7 +5045,10 @@ '
' + '
' + '💻 ' + escapeHtml(h.name || h.hostId) + ' ' + + (h.alias ? '(' + escapeHtml(h.agentName || '') + ') ' : '') + 'id=' + escapeHtml(h.hostId) + '
' + + '' + '' + '' + @@ -5379,6 +5382,26 @@ send({ action: 'sat_discover', satellite: id }); } + /** Anzeigenamen fuer einen Satelliten oder Host-Agenten vergeben. + * + * Warum nicht direkt am Geraet? Ein Agent nennt sich so, wie es in SEINER + * .env steht — umbenennen hiesse: auf den Rechner, Datei aendern, neu + * starten (beim Fire TV sogar neu provisionieren). Der Alias hier wirkt + * sofort und fuer jeden schon ausgerollten Agenten. Die Bridge loest ihn + * beim Ansprechen wieder auf, damit sich der Agent weiter angesprochen + * fuehlt. */ + function renameEntity(kind, id, currentAlias, ownName) { + const was = kind === 'satellite' ? 'Satellit' : 'Agent'; + const name = prompt( + was + ' umbenennen\n\n' + + 'Heißt sich selbst: ' + ownName + '\n' + + 'ARIA spricht ihn danach unter dem neuen Namen an.\n' + + 'Leer lassen = eigener Name gilt wieder.', + currentAlias || ''); + if (name === null) return; + send({ action: 'set_display_name', kind: kind, id: id, name: name.trim() }); + } + /** Geraetenamen vergeben. Danach kann ARIA das Geraet so ansprechen * ("mach mir Port 5000 von duffy-desktop auf") statt ueber eine IP. * Gespeichert wird an der MAC — ein IP-Wechsel per DHCP verliert den @@ -5463,9 +5486,11 @@ return '
' + '
' + '
' + - '' + escapeHtml(s.location || s.id) + ' ' + + '🛰️ ' + escapeHtml(s.location || s.id) + ' ' + + (s.alias ? '(' + escapeHtml(s.agentName || '') + ') ' : '') + 'id=' + escapeHtml(s.id) + '
' + - '' + + '' + '
' + '
kann: ' + escapeHtml(caps) + ' · ' + ctrl + diff --git a/diagnostic/server.js b/diagnostic/server.js index 76c5dae..274c4a2 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -619,13 +619,50 @@ function broadcastState() { broadcast({ type: "state", state }); } +// ── Anzeigenamen fuer Satelliten + Host-Agenten ────────────────── +// +// Ein Agent/Satellit nennt sich selbst so, wie es in SEINER .env steht. +// Umbenennen hiesse also: auf den Rechner, Datei aendern, neu starten — beim +// Fire TV sogar neu provisionieren. Darum ein Alias, der hier zentral liegt. +// +// Dieselbe Datei nutzt auch die Bridge (sie liegt im geteilten /shared), und +// die loest den Alias beim Adressieren wieder auf. Sonst koennte man etwas +// umbenennen, es danach aber nicht mehr ansprechen. +const DISPLAY_NAMES_FILE = "/shared/config/display_names.json"; + +function readDisplayNames() { + try { + const d = JSON.parse(fs.readFileSync(DISPLAY_NAMES_FILE, "utf-8")); + return (d && typeof d === "object") ? d : {}; + } catch { return {}; } +} + +function setDisplayName(kind, id, name) { + if (!["host", "satellite"].includes(kind)) throw new Error("kind muss host oder satellite sein"); + if (!id) throw new Error("id ist Pflicht"); + const data = readDisplayNames(); + const sub = (data[kind] && typeof data[kind] === "object") ? data[kind] : {}; + const clean = String(name || "").trim().slice(0, 60); + if (clean) sub[id] = clean; else delete sub[id]; + data[kind] = sub; + fs.mkdirSync("/shared/config", { recursive: true }); + const tmp = DISPLAY_NAMES_FILE + ".tmp"; + fs.writeFileSync(tmp, JSON.stringify(data, null, 2)); + fs.renameSync(tmp, DISPLAY_NAMES_FILE); + log("info", "server", `Anzeigename ${kind} ${id} -> ${clean || "(entfernt)"}`); + return clean; +} + // ── Satelliten-Registry (Aussenposten in fremden Netzen) ────────── const satellites = new Map(); // id → {id, location, caps, control, last_seen} function satelliteList() { const now = Date.now(); + const alias = readDisplayNames().satellite || {}; return Array.from(satellites.values()).map(s => ({ - id: s.id, location: s.location, caps: s.caps, control: s.control, net: s.net || null, + id: s.id, location: alias[s.id] || s.location, + agentName: s.location, alias: alias[s.id] || "", + caps: s.caps, control: s.control, net: s.net || null, online: (now - (s.last_seen || 0)) < 300000, })); } @@ -639,8 +676,11 @@ const hosts = new Map(); // hostId → {hostId, name, os, caps, control, last_se function hostList() { const now = Date.now(); + const alias = readDisplayNames().host || {}; return Array.from(hosts.values()).map(h => ({ - hostId: h.hostId, name: h.name, os: h.os, caps: h.caps, control: h.control, + hostId: h.hostId, name: alias[h.hostId] || h.name, + agentName: h.name, alias: alias[h.hostId] || "", + os: h.os, caps: h.caps, control: h.control, ip: h.ip || "", online: (now - (h.last_seen || 0)) < 300000, })); } @@ -3005,6 +3045,15 @@ wss.on("connection", (ws) => { sendToRVS_raw({ type: "sat_discover", payload: { satellite: msg.satellite || "", force: true }, timestamp: Date.now() }); + } else if (msg.action === "set_display_name") { + // Anzeigename fuer einen Satelliten oder Host-Agenten. + try { + setDisplayName(msg.kind, msg.id, msg.name); + if (msg.kind === "satellite") broadcastSatellites(); else broadcastHosts(); + } catch (err) { + ws.send(JSON.stringify({ type: "log", level: "error", source: "server", + message: "Umbenennen: " + err.message })); + } } else if (msg.action === "sat_name_set") { // Geraetename vergeben/entfernen. Der Satellit haengt ihn an die MAC, // damit ein IP-Wechsel den Namen nicht verliert.