diff --git a/diagnostic/index.html b/diagnostic/index.html
index 4e71bda..4ba62ab 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -5378,6 +5378,20 @@
renderSatellites();
send({ action: 'sat_discover', satellite: id });
}
+
+ /** 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
+ * Namen dann nicht. */
+ function renameSatDevice(satId, mac, ip, current) {
+ const name = prompt(
+ 'Name für ' + (ip || mac) + '\n\n'
+ + 'ARIA spricht das Gerät danach so an. Leer lassen = Namen entfernen.',
+ current || '');
+ if (name === null) return; // abgebrochen
+ send({ action: 'sat_name_set', satellite: satId, mac: mac, ip: ip, name: name.trim() });
+ setTimeout(() => scanSatellite(satId), 400);
+ }
function renderSatellites() {
const box = document.getElementById('sat-list');
if (!box) return;
@@ -5409,6 +5423,12 @@
if (d.adbCapable) tags.push('ADB');
if (d.mac) tags.push(escapeHtml(d.mac));
const tagStr = tags.length ? ' (' + tags.join(' · ') + ')' : '';
+ // Namen vergeben — danach ist das Geraet fuer ARIA ansprechbar
+ // ("duffy-desktop" statt einer IP).
+ const nameBtn = (d.ip || d.mac) ? '' : '';
const hasCreds = d.ip && (satCreds[s.id] || {})[d.ip] && (satCreds[s.id][d.ip] || []).length;
const keyBtn = d.ip ? '' : '';
- return '
' +
- '
' +
- '' + escapeHtml(d.name || d.id || '?') + ' ' +
+ // Der Rechner, auf dem der Satellit selbst laeuft. Er ist ein
+ // Host wie jeder andere (und oft der interessanteste — dort
+ // laufen meist noch weitere Dienste), nur eben zusaetzlich
+ // Traeger des Satelliten. Darum normal auflisten + markieren.
+ const satBadge = d.isSatellite
+ ? '🛰️'
+ : '';
+ const rowBg = d.isSatellite ? '#111426' : '#0D0D1A';
+ const rowBorder = d.isSatellite ? ';border-left:2px solid #0096FF' : '';
+ return '
';
}
diff --git a/diagnostic/server.js b/diagnostic/server.js
index 1dc00fc..76c5dae 100644
--- a/diagnostic/server.js
+++ b/diagnostic/server.js
@@ -3005,6 +3005,14 @@ wss.on("connection", (ws) => {
sendToRVS_raw({ type: "sat_discover",
payload: { satellite: msg.satellite || "", force: true },
timestamp: Date.now() });
+ } 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.
+ sendToRVS_raw({ type: "sat_name_set",
+ payload: { satellite: msg.satellite || "", mac: msg.mac || "",
+ ip: msg.ip || "", name: msg.name || "",
+ requestId: "dn_" + Date.now() },
+ timestamp: Date.now() });
} else if (msg.action === "sat_creds_list") {
sendToRVS_raw({ type: "sat_creds_list",
payload: { satellite: msg.satellite || "", requestId: "dc_" + Date.now() },
diff --git a/satellite/.env.example b/satellite/.env.example
index 1b1d1e5..958112f 100644
--- a/satellite/.env.example
+++ b/satellite/.env.example
@@ -96,6 +96,15 @@ DEVICE_CACHE_TTL_SEC=120 # wie lange ein Inventar als "frisch" gilt
# Bridge macht daraus einen lokalen Port (aria-bridge:19000...).
#
# DEFAULT AUS: das ist ein Weg in dein Netz, den schaltet man bewusst ein.
+# Soll der Rechner, auf dem der Satellit laeuft, SELBST in der Geraeteliste
+# auftauchen? Er wird per mDNS/ARP nicht gefunden (man findet sich nicht
+# selbst), ist aber oft der interessanteste Host — dort laufen typischerweise
+# weitere Dienste (FreeCAD-MCP, Web-App, Dateiserver). Er erscheint dann als
+# normales Geraet, zusaetzlich mit 🛰️ gekennzeichnet, und laesst sich genauso
+# ansprechen. Nebeneffekt: er gilt damit als "gefunden" und ist ohne Eintrag in
+# TUNNEL_ALLOW tunnelbar.
+LIST_SELF=true
+
TUNNEL_ENABLED=false
# Welche Ziele sind erlaubt? LEER = nur Geraete, die der Satellit bei der Suche
diff --git a/satellite/satellite.py b/satellite/satellite.py
index 0d0fabe..c7e0269 100644
--- a/satellite/satellite.py
+++ b/satellite/satellite.py
@@ -151,6 +151,12 @@ CONTROL_ALLOWLIST = [
# DEFAULT AUS. Das ist ein Loch in ein fremdes Netz — das schaltet man bewusst
# ein, nicht aus Versehen.
TUNNEL_ENABLED = _env_bool("TUNNEL_ENABLED", False)
+
+# Soll der Rechner, auf dem der Satellit laeuft, selbst in der Geraeteliste
+# auftauchen? Default JA — meist ist er ein ganz normaler Host mit weiteren
+# Diensten. Wer den Satelliten-Rechner bewusst heraushalten will, setzt das
+# auf false.
+LIST_SELF = _env_bool("LIST_SELF", True)
# Welche Ziele? Leer = nur Geraete, die der Satellit bei der Suche auch
# wirklich GEFUNDEN hat (beliebiger Port). Damit kann der Tunnel nichts
# ansteuern, was nicht ohnehin im LAN sichtbar ist — insbesondere nichts im
@@ -613,8 +619,11 @@ def _merge_devices(*lists) -> list[dict]:
merged = {**d, **{k: v for k, v in cur.items() if v}}
if order.get(d.get("via"), 0) >= order.get(cur.get("via"), 0):
merged.update({k: v for k, v in d.items() if v})
- # DIAL-URL / mac aus beiden behalten
- for key in ("dialAppUrl", "mac", "model", "manufacturer"):
+ # DIAL-URL / mac aus beiden behalten. isSatellite/satelliteId
+ # muessen mit: der Satellit findet sich selbst auch per mDNS,
+ # und dann duerfte die Kennzeichnung nicht verlorengehen.
+ for key in ("dialAppUrl", "mac", "model", "manufacturer",
+ "isSatellite", "satelliteId", "hostname"):
merged[key] = d.get(key) or cur.get(key) or merged.get(key)
by_ip[ip] = {k: v for k, v in merged.items() if v not in (None, "")}
return list(by_ip.values()) + loose
@@ -655,6 +664,36 @@ def _names_save() -> None:
logger.warning("device_names.json schreiben fehlgeschlagen: %s", exc)
+def _self_device() -> dict:
+ """Der Rechner, auf dem der Satellit SELBST laeuft, als normales Geraet.
+
+ Er taucht in der Suche nicht auf (man findet sich nicht per mDNS/ARP
+ selbst), ist aber oft der interessanteste Host im Netz: hier laufen
+ typischerweise noch andere Dienste — ein FreeCAD-MCP, eine Web-App, ein
+ Dateiserver. Ohne diesen Eintrag waere ausgerechnet der Rechner unsichtbar,
+ auf dem ARIA schon eine Verbindung hat.
+
+ `isSatellite` kennzeichnet ihn; ansonsten ist er ein Geraet wie jedes
+ andere und laesst sich genauso ansprechen.
+ """
+ ip = (NET or {}).get("primary_ip") or ""
+ if not ip:
+ return {}
+ d = {
+ "ip": ip,
+ "mac": (NET or {}).get("mac") or "",
+ "via": "self",
+ "isSatellite": True,
+ "satelliteId": SATELLITE_ID,
+ "type": "host",
+ }
+ try:
+ d["hostname"] = socket.gethostname()
+ except Exception:
+ pass
+ return {k: v for k, v in d.items() if v not in (None, "")}
+
+
def _name_key(dev: dict) -> str:
mac = (dev.get("mac") or "").strip().lower()
return mac if mac else (dev.get("ip") or "").strip()
@@ -1778,7 +1817,9 @@ class Satellite:
mdns = await loop.run_in_executor(None, _discover_mdns, DISCOVER_TIMEOUT_SEC)
ssdp = await loop.run_in_executor(None, _discover_ssdp, DISCOVER_TIMEOUT_SEC)
arp = await loop.run_in_executor(None, _discover_arp)
- self._devices = _apply_names(_merge_devices(mdns, ssdp, arp))
+ _self = [_self_device()] if LIST_SELF else []
+ _self = [d for d in _self if d]
+ self._devices = _apply_names(_merge_devices(mdns, ssdp, arp, _self))
if SNMP_DISCOVERY:
await self._enrich_snmp(self._devices)
await self._enrich_adb(self._devices)