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:
+143
-7
@@ -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()
|
||||
|
||||
+27
-2
@@ -5045,7 +5045,10 @@
|
||||
'<div style="display:flex;align-items:center;gap:8px;">' +
|
||||
'<div style="flex:1;"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dot + ';margin-right:6px;"></span>' +
|
||||
'<strong style="color:#E0E0F0;">💻 ' + escapeHtml(h.name || h.hostId) + '</strong> ' +
|
||||
(h.alias ? '<span style="color:#666;font-size:11px;" title="Der Agent selbst nennt sich so">(' + escapeHtml(h.agentName || '') + ')</span> ' : '') +
|
||||
'<span style="color:#666;font-size:11px;">id=' + escapeHtml(h.hostId) + '</span></div>' +
|
||||
'<button onclick="renameEntity(\'host\',\'' + escapeHtml(h.hostId) + '\',\'' + escapeHtml(h.alias || '') + '\',\'' + escapeHtml(h.agentName || h.hostId) + '\')" title="Anzeigenamen vergeben — ARIA kann den Agenten danach so ansprechen"' +
|
||||
' style="background:#12121E;color:#8888AA;border:1px solid #1E1E2E;border-radius:6px;padding:4px 9px;font-size:12px;cursor:pointer;">✎</button>' +
|
||||
'<button onclick="openAgentHistory(\'' + escapeHtml(h.hostId) + '\')" style="background:#12121E;color:#FFD60A;border:1px solid #FFD60A44;border-radius:6px;padding:4px 10px;font-size:12px;cursor:pointer;">🕘 Historie</button>' +
|
||||
'<button onclick="forgetHost(\'' + escapeHtml(h.hostId) + '\',\'' + escapeHtml(h.name || h.hostId) + '\')" title="Aus der Liste entfernen (z.B. alter Name nach dem Umbenennen). Meldet sich der Agent wieder, erscheint er von selbst."' +
|
||||
' style="background:#12121E;color:#8888AA;border:1px solid #1E1E2E;border-radius:6px;padding:4px 9px;font-size:12px;cursor:pointer;margin-left:4px;">✕</button>' +
|
||||
@@ -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 '<div style="border:1px solid #1E1E2E;border-radius:8px;padding:10px;margin-bottom:8px;">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;">' +
|
||||
'<div><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' + dot + ';margin-right:6px;"></span>' +
|
||||
'<strong style="color:#E0E0F0;">' + escapeHtml(s.location || s.id) + '</strong> ' +
|
||||
'<strong style="color:#E0E0F0;">🛰️ ' + escapeHtml(s.location || s.id) + '</strong> ' +
|
||||
(s.alias ? '<span style="color:#666;font-size:11px;" title="Der Satellit selbst nennt sich so">(' + escapeHtml(s.agentName || '') + ')</span> ' : '') +
|
||||
'<span style="color:#666;font-size:11px;">id=' + escapeHtml(s.id) + '</span></div>' +
|
||||
'<button class="btn secondary" onclick="scanSatellite(\'' + escapeHtml(s.id) + '\')" style="padding:3px 8px;font-size:11px;"' +
|
||||
'<button class="btn secondary" onclick="renameEntity(\'satellite\',\'' + escapeHtml(s.id) + '\',\'' + escapeHtml(s.alias || '') + '\',\'' + escapeHtml(s.agentName || s.id) + '\')" title="Anzeigenamen vergeben — ARIA kann den Satelliten danach so ansprechen" style="padding:3px 8px;font-size:11px;">✎</button>' +
|
||||
'<button class="btn secondary" onclick="scanSatellite(\'' + escapeHtml(s.id) + '\')" style="padding:3px 8px;font-size:11px;margin-left:4px;"' +
|
||||
(scanning ? ' disabled' : '') + '>Geraete scannen</button>' +
|
||||
'</div>' +
|
||||
'<div style="color:#8888AA;font-size:11px;margin-top:4px;">kann: ' + escapeHtml(caps) + ' · ' + ctrl +
|
||||
|
||||
+51
-2
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user