feat: update network visualiser with WebGL rendering options and user preferences

This commit is contained in:
Ivan 2026-07-16 10:47:52 -05:00
parent 9566e49d1e
commit cee526b3a8
No known key found for this signature in database
24 changed files with 308 additions and 38 deletions

View file

@ -9,6 +9,7 @@
# actions/setup-python@v6.2.0 a309ff8b426b58ec0e2a45f0f869d46889d02405
# actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4
# actions/download-artifact@v5.0.0 634f93cb2916e3fdff6788551b99b062d0335ce0
# actions/setup-go@v7.0.0 b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
name: CI
@ -36,6 +37,7 @@ env:
NODE_VERSION: "24"
UV_VERSION: "0.11.15"
PNPM_VERSION: "11.1.2"
GO_VERSION: "1.24.x"
RNS_REQUIRED_SIGNER: e46112d44649266d71fe2193e00a4710
jobs:
@ -102,6 +104,44 @@ jobs:
- name: Verify workspace clean
uses: ./.github/actions/verify-workspace-clean
visualiser-wasm:
name: Visualiser WASM (Go)
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
- name: Set up Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
with:
go-version: ${{ env.GO_VERSION }}
- name: Setup Task
run: sh scripts/ci/setup-task.sh 3.49.1
- name: Run visualiser-wasm tests
run: task test:visualiser-wasm
- name: Build visualiser-wasm artifacts
run: task build:visualiser-wasm
- name: Verify integrity.json stamped
run: |
set -euo pipefail
test -f meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(Path("meshchatx/src/frontend/public/vendor/visualiser-wasm/integrity.json").read_text())
assert data.get("version") == "1.2.0", data
assert str(data.get("wasm", "")).startswith("sha384-"), data
assert str(data.get("wasmExec", "")).startswith("sha384-"), data
print("visualiser-wasm integrity OK", data["version"])
PY
backend-tests:
name: Backend tests (Python 3.14) - Shard ${{ matrix.shard }} / 4
runs-on: ubuntu-latest

View file

@ -8,6 +8,7 @@
# actions/setup-python@v6.2.0 a309ff8b426b58ec0e2a45f0f869d46889d02405
# actions/setup-node@v6.1.0 395ad3262231945c25e8478fd5baf05154b1d79f
# actions/upload-artifact@v5.0.0 330a01c490aca151604b8cf639adc76d48f6c5d4
# actions/setup-go@v7.0.0 b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
name: Frontend build
@ -90,6 +91,11 @@ jobs:
node-version: ${{ inputs.node_version }}
pnpm-version: ${{ inputs.pnpm_version }}
- name: Set up Go (visualiser-wasm)
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e
with:
go-version: "1.24.x"
- name: Install frontend dependencies
run: pnpm install --frozen-lockfile

View file

@ -18,6 +18,7 @@ All notable changes to this project will be documented in this file.
- RSM signing and verification for meshchatx.rsm (CI and pre-commit resign)
- Notification sound settings
- LXMFy 1.6.5 vendor refresh, wasmtime, mutation test tasks
- Network visualiser WebGL + WASM renderer (vis-network fallback) and Settings renderer preference
### Changed

Binary file not shown.

View file

@ -95,6 +95,7 @@ import {
persistVisualiserLiveLayout,
VISUALISER_DISPLAY_PREFS_CHANGED,
} from "../../js/settings/settingsVisualiserPrefs.js";
import ToastUtils from "../../js/ToastUtils";
const HOP_MAX_FILTER_STORAGE_KEY = "meshchatx.visualiser.maxHops";
@ -186,6 +187,7 @@ export default {
isUpdating: false,
isLoading: false,
enablePhysics: displayPrefs.enablePhysics,
preferredRenderer: displayPrefs.renderer || "auto",
showDisabledInterfaces: displayPrefs.showDisabledInterfaces,
showDiscoveredInterfaces: displayPrefs.showDiscoveredInterfaces,
loadingStatus: "Initializing...",
@ -366,9 +368,14 @@ export default {
this.isShowingControls = false;
}
this._visualiserPrefsHandler = () => {
this._visualiserPrefsHandler = async () => {
const prevRenderer = this.preferredRenderer;
this.loadVisualiserDisplayPrefs();
this.applyBatterySaverVisualiserPrefs();
if (this.preferredRenderer !== prevRenderer) {
await this.reinitRenderer();
return;
}
if (this.hasRenderer) {
this.processVisualization();
}
@ -805,6 +812,54 @@ export default {
this.showDiscoveredInterfaces = p.showDiscoveredInterfaces;
this.enablePhysics = p.enablePhysics;
this.autoReload = p.autoReload;
this.preferredRenderer = p.renderer || "auto";
},
destroyActiveRenderer() {
this.hoverTooltip = null;
if (this.webglEngine) {
this.webglEngine.destroy();
this.webglEngine = null;
}
if (this.network) {
this.network.destroy();
this.network = null;
}
try {
this.nodes.clear();
this.edges.clear();
} catch {
/* ignore */
}
this.rendererMode = "vis";
},
async reinitRenderer() {
this.destroyActiveRenderer();
await this.init({ skipWarm: true });
},
async tryStartWebGL() {
const canvas = this.$refs.webglCanvas;
if (!canvas || !canUseVisualiserWebGL()) {
return false;
}
try {
this.webglEngine = createVisualiserWebGLEngine(canvas, {
getLiveLayout: () => this.enablePhysics === true,
isDark: () => document.documentElement.classList.contains("dark"),
onNodeActivate: (id, meta) => this.onWebGLNodeActivate(id, meta),
onHover: (id, meta, x, y) => this.onWebGLHover(id, meta, x, y),
});
this.rendererMode = "webgl";
this.engineMode = "webgl";
return true;
} catch (e) {
console.warn("WebGL visualiser failed:", e);
if (this.webglEngine) {
this.webglEngine.destroy();
this.webglEngine = null;
}
this.rendererMode = "vis";
return false;
}
},
refreshPhysicsEnabled() {
if (this.webglEngine) {
@ -892,30 +947,26 @@ export default {
posById[id] = { x: v.x, y: v.y };
return v;
},
async init() {
await warmVisualiserWasm();
const canvas = this.$refs.webglCanvas;
if (canvas && canUseVisualiserWebGL(canvas)) {
try {
this.webglEngine = createVisualiserWebGLEngine(canvas, {
getLiveLayout: () => this.enablePhysics === true,
isDark: () => document.documentElement.classList.contains("dark"),
onNodeActivate: (id, meta) => this.onWebGLNodeActivate(id, meta),
onHover: (id, meta, x, y) => this.onWebGLHover(id, meta, x, y),
});
this.rendererMode = "webgl";
this.engineMode = "webgl";
await this.manualUpdate();
this.restartAutoReloadInterval();
return;
} catch (e) {
console.warn("WebGL visualiser failed, falling back to vis-network:", e);
if (this.webglEngine) {
this.webglEngine.destroy();
this.webglEngine = null;
}
this.rendererMode = "vis";
}
async init(opts = {}) {
if (!opts.skipWarm) {
await warmVisualiserWasm();
}
const preferred = this.preferredRenderer || loadVisualiserDisplayPrefs().renderer || "auto";
if (preferred === "vis") {
await this.initVisNetwork();
return;
}
const started = await this.tryStartWebGL();
if (started) {
await this.manualUpdate();
this.restartAutoReloadInterval();
return;
}
if (preferred === "webgl") {
ToastUtils.warning(this.$t("visualiser.renderer_webgl_unavailable"));
}
await this.initVisNetwork();
},

View file

@ -1896,6 +1896,24 @@
</div>
</header>
<div class="settings-section__body space-y-4">
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("visualiser.renderer_title") }}
</div>
<p class="text-xs text-gray-600 dark:text-gray-400">
{{ $t("visualiser.renderer_desc") }}
</p>
<select
id="settings-visualiser-renderer"
v-model="visualiserRenderer"
class="input-field"
@change="onVisualiserRendererChange"
>
<option value="auto">{{ $t("visualiser.renderer_option_auto") }}</option>
<option value="webgl">{{ $t("visualiser.renderer_option_webgl") }}</option>
<option value="vis">{{ $t("visualiser.renderer_option_vis") }}</option>
</select>
</div>
<label class="setting-toggle">
<Toggle
id="settings-visualiser-offline"
@ -3581,6 +3599,7 @@ import {
loadVisualiserDisplayPrefs,
persistVisualiserShowDisabled,
persistVisualiserShowDiscovered,
persistVisualiserRenderer,
} from "../../js/settings/settingsVisualiserPrefs";
import { loadBatterySaverPrefs, saveBatterySaverPrefs } from "../../js/settings/batterySaverPrefs.js";
import {
@ -3752,6 +3771,7 @@ export default {
gifImportReplaceDuplicates: false,
visualiserShowDisabledInterfaces: false,
visualiserShowDiscoveredInterfaces: false,
visualiserRenderer: "auto",
batterySaver: loadBatterySaverPrefs(),
batteryInterfaceRows: [],
batteryBitrateBusy: false,
@ -4307,6 +4327,7 @@ export default {
const p = loadVisualiserDisplayPrefs();
this.visualiserShowDisabledInterfaces = p.showDisabledInterfaces;
this.visualiserShowDiscoveredInterfaces = p.showDiscoveredInterfaces;
this.visualiserRenderer = p.renderer || "auto";
},
onVisualiserShowDisabledChange(val) {
this.visualiserShowDisabledInterfaces = val;
@ -4316,6 +4337,9 @@ export default {
this.visualiserShowDiscoveredInterfaces = val;
persistVisualiserShowDiscovered(val);
},
onVisualiserRendererChange() {
persistVisualiserRenderer(this.visualiserRenderer);
},
async getTrustedTelemetryPeers() {
try {
const response = await window.api.get("/api/v1/telemetry/trusted-peers");

View file

@ -6,9 +6,14 @@ const KEY_DISABLED = "meshchatx.visualiser.showDisabledInterfaces";
const KEY_DISCOVERED = "meshchatx.visualiser.showDiscoveredInterfaces";
const KEY_LIVE_LAYOUT = "meshchatx.visualiser.enablePhysics";
const KEY_AUTO_RELOAD = "meshchatx.visualiser.autoReload";
const KEY_RENDERER = "meshchatx.visualiser.renderer";
export const VISUALISER_DISPLAY_PREFS_CHANGED = "visualiser-display-prefs-changed";
/** @typedef {"auto" | "webgl" | "vis"} VisualiserRendererPref */
export const VISUALISER_RENDERER_OPTIONS = ["auto", "webgl", "vis"];
/**
* @param {string} key
* @param {boolean} defaultValue
@ -42,12 +47,38 @@ function writeBool(key, val) {
}
}
/**
* @param {unknown} raw
* @returns {VisualiserRendererPref}
*/
export function normalizeVisualiserRenderer(raw) {
if (raw === "webgl" || raw === "vis" || raw === "auto") {
return raw;
}
return "auto";
}
/**
* @returns {VisualiserRendererPref}
*/
function readRenderer() {
try {
if (typeof localStorage === "undefined") {
return "auto";
}
return normalizeVisualiserRenderer(localStorage.getItem(KEY_RENDERER));
} catch {
return "auto";
}
}
/**
* @returns {{
* showDisabledInterfaces: boolean,
* showDiscoveredInterfaces: boolean,
* enablePhysics: boolean,
* autoReload: boolean,
* renderer: VisualiserRendererPref,
* }}
*/
export function loadVisualiserDisplayPrefs() {
@ -57,6 +88,7 @@ export function loadVisualiserDisplayPrefs() {
// Live Layout defaults on when never set.
enablePhysics: readBool(KEY_LIVE_LAYOUT, true),
autoReload: readBool(KEY_AUTO_RELOAD, false),
renderer: readRenderer(),
};
}
@ -97,3 +129,21 @@ export function persistVisualiserAutoReload(val, opts = {}) {
GlobalEmitter.emit(VISUALISER_DISPLAY_PREFS_CHANGED);
}
}
/**
* @param {unknown} val
* @param {{ emit?: boolean }} [opts]
*/
export function persistVisualiserRenderer(val, opts = {}) {
const next = normalizeVisualiserRenderer(val);
try {
if (typeof localStorage !== "undefined") {
localStorage.setItem(KEY_RENDERER, next);
}
} catch {
/* ignore */
}
if (opts.emit !== false) {
GlobalEmitter.emit(VISUALISER_DISPLAY_PREFS_CHANGED);
}
}

View file

@ -3367,7 +3367,13 @@
"search_nodes_placeholder": "Knoten suchen ({count})...",
"clear_search": "Suche leeren",
"engine_webgl": "WebGL",
"engine_webgl_hint": "Graph, Layout und Zeichenpuffer laufen in Go-WebAssembly. Pixelzeichnung mit WebGL2"
"engine_webgl_hint": "Graph, Layout und Zeichenpuffer laufen in Go-WebAssembly. Pixelzeichnung mit WebGL2",
"renderer_title": "Renderer",
"renderer_desc": "Auto bevorzugt WebGL mit WASM und fällt sonst auf vis-network zurück. Erzwingen Sie einen Pfad zum Debuggen oder für ältere GPUs.",
"renderer_option_auto": "Auto (WebGL wenn verfügbar)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (Canvas)",
"renderer_webgl_unavailable": "WebGL-Renderer nicht verfügbar. Nutze vis-network-Fallback."
},
"banishment": {
"title": "Verbannt",

View file

@ -1895,6 +1895,12 @@
"visualiser": {
"title": "Network Visualiser",
"description": "Graph of discovered nodes and links.",
"renderer_title": "Renderer",
"renderer_desc": "Auto prefers WebGL with WASM when available, then falls back to vis-network. Force a path for debugging or older GPUs.",
"renderer_option_auto": "Auto (WebGL when available)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (canvas)",
"renderer_webgl_unavailable": "WebGL renderer unavailable. Using vis-network fallback.",
"reticulum_mesh": "Reticulum Mesh",
"network_visualizer": "Network Visualizer",
"node_info": "Node Info",

View file

@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Buscar nodos ({count})...",
"clear_search": "Borrar busqueda",
"engine_webgl": "WebGL",
"engine_webgl_hint": "Grafo, layout y buffers de dibujo en Go WebAssembly. Pixels con WebGL2"
"engine_webgl_hint": "Grafo, layout y buffers de dibujo en Go WebAssembly. Pixels con WebGL2",
"renderer_title": "Renderizador",
"renderer_desc": "Auto prefiere WebGL con WASM cuando está disponible y luego usa vis-network. Fuerza una ruta para depurar o GPUs antiguas.",
"renderer_option_auto": "Auto (WebGL si está disponible)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (canvas)",
"renderer_webgl_unavailable": "Renderizador WebGL no disponible. Usando reserva vis-network."
},
"banishment": {
"title": "Desterrados",

View file

@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Hae solmuja ({count})...",
"clear_search": "Tyhjennä haku",
"engine_webgl": "WebGL",
"engine_webgl_hint": "Kaavio, asettelu ja piirtopuskurit Go WebAssemblyssa. Pikselit WebGL2:lla"
"engine_webgl_hint": "Kaavio, asettelu ja piirtopuskurit Go WebAssemblyssa. Pikselit WebGL2:lla",
"renderer_title": "Renderöijä",
"renderer_desc": "Auto suosii WebGL+WASM:ia kun saatavilla ja palaa sitten vis-networkiin. Pakota polku debuggaukseen tai vanhoille GPU:ille.",
"renderer_option_auto": "Auto (WebGL jos saatavilla)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (canvas)",
"renderer_webgl_unavailable": "WebGL-renderöijä ei ole saatavilla. Käytetään vis-network-varausta."
},
"banishment": {
"title": "Karkotus",

View file

@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Rechercher des noeuds ({count})...",
"clear_search": "Effacer la recherche",
"engine_webgl": "WebGL",
"engine_webgl_hint": "Graphe, layout et buffers de dessin en Go WebAssembly. Pixels via WebGL2"
"engine_webgl_hint": "Graphe, layout et buffers de dessin en Go WebAssembly. Pixels via WebGL2",
"renderer_title": "Moteur de rendu",
"renderer_desc": "Auto préfère WebGL avec WASM puis revient à vis-network. Forcez un chemin pour le débogage ou les anciens GPU.",
"renderer_option_auto": "Auto (WebGL si disponible)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (canvas)",
"renderer_webgl_unavailable": "Rendu WebGL indisponible. Repli sur vis-network."
},
"banishment": {
"title": "Interdit",

View file

@ -2015,7 +2015,13 @@
"search_nodes_placeholder": "Cerca nodi ({count})...",
"clear_search": "Cancella ricerca",
"engine_webgl": "WebGL",
"engine_webgl_hint": "Grafo, layout e buffer di disegno in Go WebAssembly. Pixel con WebGL2"
"engine_webgl_hint": "Grafo, layout e buffer di disegno in Go WebAssembly. Pixel con WebGL2",
"renderer_title": "Renderer",
"renderer_desc": "Auto preferisce WebGL con WASM quando disponibile, poi passa a vis-network. Forza un percorso per debug o GPU datate.",
"renderer_option_auto": "Auto (WebGL se disponibile)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (canvas)",
"renderer_webgl_unavailable": "Renderer WebGL non disponibile. Uso fallback vis-network."
},
"banishment": {
"title": "Esiliati",

View file

@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "Zoek knooppunten ({count})...",
"clear_search": "Zoekopdracht wissen",
"engine_webgl": "WebGL",
"engine_webgl_hint": "Grafiek, layout en tekenbuffers in Go WebAssembly. Pixels met WebGL2"
"engine_webgl_hint": "Grafiek, layout en tekenbuffers in Go WebAssembly. Pixels met WebGL2",
"renderer_title": "Renderer",
"renderer_desc": "Auto prefereert WebGL met WASM indien beschikbaar, anders vis-network. Forceer een pad voor debug of oudere GPU's.",
"renderer_option_auto": "Auto (WebGL indien beschikbaar)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (canvas)",
"renderer_webgl_unavailable": "WebGL-renderer niet beschikbaar. Gebruik vis-network-fallback."
},
"banishment": {
"title": "Verbannen",

View file

@ -3367,7 +3367,13 @@
"search_nodes_placeholder": "Поиск узлов ({count})...",
"clear_search": "Очистить поиск",
"engine_webgl": "WebGL",
"engine_webgl_hint": "Граф, раскладка и буферы отрисовки в Go WebAssembly. Пиксели через WebGL2"
"engine_webgl_hint": "Граф, раскладка и буферы отрисовки в Go WebAssembly. Пиксели через WebGL2",
"renderer_title": "Рендерер",
"renderer_desc": "Auto предпочитает WebGL с WASM при наличии, иначе vis-network. Принудительный выбор для отладки или старых GPU.",
"renderer_option_auto": "Auto (WebGL если доступен)",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-network (canvas)",
"renderer_webgl_unavailable": "WebGL-рендерер недоступен. Используется запасной vis-network."
},
"banishment": {
"title": "Забаненные",

View file

@ -1963,7 +1963,13 @@
"search_nodes_placeholder": "搜索节点 ({count})...",
"clear_search": "清除搜索",
"engine_webgl": "WebGL",
"engine_webgl_hint": "图构建、布局与绘制缓冲在 Go WebAssembly 中运行。像素由 WebGL2 绘制"
"engine_webgl_hint": "图构建、布局与绘制缓冲在 Go WebAssembly 中运行。像素由 WebGL2 绘制",
"renderer_title": "渲染器",
"renderer_desc": "自动在可用时优先使用 WebGL+WASM否则回退到 vis-network。可强制选择以便调试或兼容旧 GPU。",
"renderer_option_auto": "自动(可用时使用 WebGL",
"renderer_option_webgl": "WebGL + WASM",
"renderer_option_vis": "vis-networkcanvas",
"renderer_webgl_unavailable": "WebGL 渲染器不可用。已回退到 vis-network。"
},
"banishment": {
"title": "放逐",

View file

@ -1,6 +1,6 @@
{
"version": "1.0.0",
"wasm": "sha384-ERsG9mb3L8kjxDEvn36T/mdj3Cz4zPA7U/hNo6Nppt9sjE0VAuNT3XjHsFDx9P/b",
"version": "1.2.0",
"wasm": "sha384-kx5+sGvflCkoO65dt5IcMJVB/8y85IAwT6zUbAKjha1XK/8p3tMp+QFwX5hd2DYN",
"wasmExec": "sha384-PWCs+V4BDf9yY1yjkD/p+9xNEs4iEbuvq+HezAOJiY3XL5GI6VyJXMsvnjiwNbce",
"wasmExecSource": "/usr/lib/go/lib/wasm/wasm_exec.js"
}

View file

@ -16,7 +16,7 @@ const GO_MOD_DIR = path.join(REPO_ROOT, "visualiser-wasm");
const OUT_DIR = path.join(REPO_ROOT, "meshchatx", "src", "frontend", "public", "vendor", "visualiser-wasm");
const WASM_NAME = "visualiser.wasm";
const EXEC_NAME = "wasm_exec.js";
const VERSION = "1.0.0";
const VERSION = "1.2.0";
function computeSri(buf) {
return `sha384-${crypto.createHash("sha384").update(buf).digest("base64")}`;

View file

@ -908,6 +908,10 @@
"method": "POST",
"path": "/api/v1/reticulum/interfaces/add"
},
{
"method": "POST",
"path": "/api/v1/reticulum/interfaces/bitrates"
},
{
"method": "POST",
"path": "/api/v1/reticulum/interfaces/delete"

View file

@ -60,12 +60,14 @@ test.describe("Network visualiser", () => {
await prepareE2eSession(request);
});
test("shows mesh header, graph container, and node search", async ({ page }) => {
test("shows mesh header, graph hosts, and node search", async ({ page }) => {
await page.goto("/#/network-visualiser");
await expect(page).toHaveURL(/#\/network-visualiser/);
await expect(page.getByText("Reticulum Mesh", { exact: true })).toBeVisible({ timeout: 30000 });
await expect(page.locator("#network")).toBeAttached();
await expect(page.locator("#network-webgl")).toBeAttached();
await expect(page.getByPlaceholder(/Search nodes \(\d+\)/)).toBeVisible({ timeout: 30000 });
await expect(page.getByText(/WebGL|WASM|JS fallback|Checking/)).toBeVisible({ timeout: 30000 });
});
test("collapses and expands control panel via header", async ({ page }) => {

View file

@ -160,6 +160,7 @@ describe("NetworkVisualiser.vue", () => {
"visualiser.nodes": "Nodes",
"visualiser.links": "Links",
"visualiser.engine_wasm": "WASM",
"visualiser.engine_webgl": "WebGL",
"visualiser.engine_fallback": "JS fallback",
"visualiser.engine_checking": "Checking",
};

View file

@ -542,6 +542,14 @@ describe("SettingsPage — visualiser display prefs (localStorage + emitter)", (
expect(GlobalEmitter.emit).toHaveBeenCalledWith("visualiser-display-prefs-changed");
});
it("onVisualiserRendererChange persists renderer and emits", async () => {
const w = await mountSettingsPage(api);
w.vm.visualiserRenderer = "webgl";
w.vm.onVisualiserRendererChange();
expect(localStorage.getItem("meshchatx.visualiser.renderer")).toBe("webgl");
expect(GlobalEmitter.emit).toHaveBeenCalledWith("visualiser-display-prefs-changed");
});
it("onDetailedOutboundSendStatusChange updates GlobalState and localStorage", async () => {
localStorage.removeItem("meshchatx_detailed_outbound_send_status");
const w = await mountSettingsPage(api);

View file

@ -260,4 +260,21 @@ describe("behavior contracts: network visualiser performance", () => {
expect(perf).toContain("dedupeIconQueueEntries");
expect(perf).toContain("pickAdaptiveFetchConcurrency");
});
it("prefers WebGL with WASM scene and keeps vis-network fallback", () => {
const src = readSource("meshchatx/src/frontend/components/network-visualiser/NetworkVisualiser.vue");
expect(src).toContain("tryStartWebGL");
expect(src).toContain("initVisNetwork");
expect(src).toContain("preferredRenderer");
expect(src).toContain('name: "nomadnetwork"');
expect(src).toContain("openAnnounceDestination");
const engine = readSource("meshchatx/src/frontend/js/networkVisualiserWebGLEngine.js");
expect(engine).toContain("meshchatxVisualiserSceneSet");
expect(engine).toContain("createVisualiserWebGLEngine");
const prefs = readSource("meshchatx/src/frontend/js/settings/settingsVisualiserPrefs.js");
expect(prefs).toContain("persistVisualiserRenderer");
expect(prefs).toContain('"auto"');
expect(prefs).toContain('"webgl"');
expect(prefs).toContain('"vis"');
});
});

View file

@ -5,6 +5,8 @@ import {
persistVisualiserAutoReload,
persistVisualiserShowDisabled,
persistVisualiserShowDiscovered,
persistVisualiserRenderer,
normalizeVisualiserRenderer,
} from "@/js/settings/settingsVisualiserPrefs.js";
describe("settingsVisualiserPrefs", () => {
@ -12,12 +14,13 @@ describe("settingsVisualiserPrefs", () => {
localStorage.clear();
});
it("defaults live layout on and auto-reload off", () => {
it("defaults live layout on, auto-reload off, renderer auto", () => {
expect(loadVisualiserDisplayPrefs()).toEqual({
showDisabledInterfaces: false,
showDiscoveredInterfaces: false,
enablePhysics: true,
autoReload: false,
renderer: "auto",
});
});
@ -31,6 +34,7 @@ describe("settingsVisualiserPrefs", () => {
showDiscoveredInterfaces: true,
enablePhysics: false,
autoReload: true,
renderer: "auto",
});
persistVisualiserLiveLayout(true);
expect(loadVisualiserDisplayPrefs().enablePhysics).toBe(true);
@ -42,4 +46,12 @@ describe("settingsVisualiserPrefs", () => {
persistVisualiserAutoReload(true, { emit: false });
expect(loadVisualiserDisplayPrefs().autoReload).toBe(true);
});
it("normalizes and persists renderer preference", () => {
expect(normalizeVisualiserRenderer("nope")).toBe("auto");
persistVisualiserRenderer("webgl");
expect(loadVisualiserDisplayPrefs().renderer).toBe("webgl");
persistVisualiserRenderer("vis", { emit: false });
expect(loadVisualiserDisplayPrefs().renderer).toBe("vis");
});
});