feat: show P2P diagnostics in the status pane and log

Show expandable relay, peer, and sync observations below Stats and route Diag
events to the existing Log Pane without notification or editor-status churn.
Retain departed-peer evidence and replay logs recorded before the pane opens.

Keep narrow panes usable with wrapping peer controls, readable device names,
and larger buttons. Add browser regressions for disclosure, control bounds,
button targets, and existing actions.
This commit is contained in:
vorotamoroz committed 2026-11-06 12:17:31 +00:00
1 parent fc5f02ca3b
commit cf527fbf86
22 files changed
+978 -88

No files matched your search

+32
View File
@@ -0,0 +1,32 @@
# P2P diagnostic UI checks
These checks use Node.js 24 or newer, the existing Playwright dependency, and a
Chromium installation.
The Log Pane fixture starts with a buffered diagnostic message and suppresses
ordinary logger output, modelling a report opened while ordinary INFO messages
are hidden. It checks initial display, later events, and unmounting. The status-card fixture
also checks three summary rows at a narrow viewport and the existing Log Pane
link. The full status-pane fixture exercises accepted, session-accepted, new,
and denied peers at 320, 390, and 768 px, including long names. It checks bounds,
complete action labels, and existing sync/admission callbacks. The card check
also verifies initially collapsed diagnostic details, keyboard toggling, and
updates received while collapsed. Host view, Menu, and SetupManager capabilities
are stubbed; the Svelte components and shared log store are real, while P2P and
Obsidian capabilities are fixtures. Add `?preview` to the status-pane URL for a
shorter illustrative peer list.
From the repository root, start the fixture server:
```sh
NODE_OPTIONS=--max-old-space-size=3072 node node_modules/vite/bin/vite.js --config test/p2p-diagnostics/vite.config.mjs
```
In another terminal, run the focused browser check:
```sh
NODE_OPTIONS=--max-old-space-size=3072 node --test --test-concurrency=1 test/p2p-diagnostics/log-pane.test.ts test/p2p-diagnostics/status-pane.test.ts
```
The default endpoint is `http://127.0.0.1:5157`. Set `DIAG_UI_BASE_URL` when using
another endpoint. Stop the fixture server after the check. This browser fixture
does not replace verification in Obsidian.
+3
View File
@@ -0,0 +1,3 @@
// Host-only controls are not exercised by the browser layout fixture.
export class Menu {}
export class SetupManager {}
+1
View File
@@ -0,0 +1 @@
export const VIEW_TYPE_LOG = "log-log";
+84
View File
@@ -0,0 +1,84 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { chromium } from "playwright";
test("Log Pane shows diagnostics recorded before it opens and follows subsequent events", async () => {
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] });
try {
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
const base = process.env.DIAG_UI_BASE_URL ?? "http://127.0.0.1:5157";
await page.goto(`${base}/test/p2p-diagnostics/log-pane/index.html`);
await page.waitForSelector(".logpane");
assert.deepEqual(errors, [], "Fixture must load without browser errors");
await page.waitForFunction(
() => document.querySelector(".log")?.textContent?.includes("Earlier connection failure"),
undefined,
{ timeout: 3_000 }
);
await page.click("#append");
await page.waitForFunction(
() => document.querySelector(".log")?.textContent?.includes("Later connection recovery"),
undefined,
{ timeout: 3_000 }
);
assert.match((await page.locator(".log").textContent()) ?? "", /Earlier connection failure/);
await page.click("#unmount");
await page.waitForSelector(".logpane", { state: "detached" });
await page.click("#append");
assert.deepEqual(errors, []);
} finally {
await browser.close();
}
});
test("P2P Stats has three diagnostic rows and opens the existing Log Pane", async () => {
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] });
try {
const page = await browser.newPage({ viewport: { width: 390, height: 844 } });
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
const base = process.env.DIAG_UI_BASE_URL ?? "http://127.0.0.1:5157";
await page.goto(`${base}/test/p2p-diagnostics/status-card/index.html`);
await page.waitForSelector(".diag-details", { state: "attached", timeout: 3_000 });
assert.equal(await page.locator(".diag-grid").isVisible(), true, "Stats remains visible");
assert.equal(await page.getByRole("button", { name: "Open log", exact: true }).isVisible(), false);
await page.click("#complete");
assert.equal(await page.locator(".diag-summary").isVisible(), false, "New events do not expand the log");
await page.locator(".diag-details > summary").focus();
await page.keyboard.press("Enter");
await page.waitForSelector(".diag-summary");
assert.deepEqual(errors, [], "Fixture must load without browser errors");
const rows = page.locator(".diag-summary > div");
assert.equal(await rows.count(), 3);
assert.match((await rows.nth(0).textContent()) ?? "", /Room opened; relay replies not observed/);
assert.match((await rows.nth(1).textContent()) ?? "", /Peer left/);
assert.match((await rows.nth(2).textContent()) ?? "", /Pull completed/);
assert.equal(
await page.locator(".diag-summary").evaluate((element) => getComputedStyle(element).fontSize),
"12px"
);
if (process.env.DIAG_UI_SCREENSHOT) {
await page.screenshot({ path: process.env.DIAG_UI_SCREENSHOT, fullPage: true });
}
await page.click("#complete");
await page.waitForFunction(
() => document.querySelector(".diag-summary")?.textContent?.includes("Pull completed"),
undefined,
{ timeout: 3_000 }
);
await page.locator(".diag-details > summary").click();
assert.equal(await page.locator(".diag-grid").isVisible(), true);
assert.equal(await page.locator(".diag-summary").isVisible(), false);
await page.locator(".diag-details > summary").focus();
await page.keyboard.press("Space");
await page.getByRole("button", { name: "Open log", exact: true }).click();
await page.waitForSelector(".logpane");
assert.equal(await page.locator("#opened").textContent(), "log-log");
assert.match((await page.locator(".log").textContent()) ?? "", /Earlier connection failure/);
assert.deepEqual(errors, []);
} finally {
await browser.close();
}
});
+3
View File
@@ -0,0 +1,3 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>Log Pane diagnostic fixture</title></head>
<body><button id="append">Append event</button><button id="unmount">Unmount</button><div id="app"></div><script type="module" src="./main.ts"></script></body></html>
+15
View File
@@ -0,0 +1,15 @@
import { mount, unmount } from "svelte";
import LogPane from "../../../src/modules/features/Log/LogPane.svelte";
import { logMessages } from "@vrtmrz/livesync-commonlib/compat/mock_and_interop/stores";
import { setGlobalLogFunction } from "octagonal-wheels/common/logger";
// Simulate ordinary INFO messages being hidden while a prior diagnostic is visible.
setGlobalLogFunction(() => undefined);
logMessages.value = ["[P2P Diag] Earlier connection failure"];
const component = mount(LogPane, { target: document.querySelector("#app")!, props: { close: () => undefined } });
document.querySelector<HTMLButtonElement>("#append")!.onclick = () => {
logMessages.value = [...logMessages.value, "[P2P Diag] Later connection recovery"];
};
document.querySelector<HTMLButtonElement>("#unmount")!.onclick = () => {
void unmount(component);
};
@@ -0,0 +1,4 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>P2P diagnostic summary fixture</title>
<style>:root { --font-ui-small: 12px; --text-muted: #666; --size-4-1: 4px; --size-4-2: 8px; } body { margin: 8px; font: 14px sans-serif; }</style></head>
<body><button id="complete">Complete pull</button><div id="opened"></div><div id="app"></div><div id="logapp"></div><script type="module" src="./main.ts"></script></body></html>
+58
View File
@@ -0,0 +1,58 @@
import { mount } from "svelte";
import Card from "../../../src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte";
import LogPane from "../../../src/modules/features/Log/LogPane.svelte";
import { eventHub } from "../../../src/common/events";
import { EVENT_SERVER_STATUS } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
import { logMessages } from "@vrtmrz/livesync-commonlib/compat/mock_and_interop/stores";
import { setGlobalLogFunction } from "octagonal-wheels/common/logger";
import type { P2PDiagnosticEvent } from "@vrtmrz/livesync-commonlib/p2p";
setGlobalLogFunction(() => undefined);
const timestamp = 1_700_000_000_000;
const listeners = new Set<(event: P2PDiagnosticEvent) => void>();
const settings = { P2P_useDiagRTC: true, P2P_roomID: "fixture-room" };
const p2p = {
transportLifecycle: { connect: async () => undefined, disconnect: async () => undefined },
changeRelay: { enableBroadcastChanges() {}, disableBroadcastChanges() {} },
diagnostics: {
getDiagnosticSnapshot: () => ({
relay: { category: "relay", code: "room-opened", reason: "relay-details-unobserved", timestamp },
peer: { category: "peer", code: "peer-left", peerId: "departed-peer", timestamp },
}),
onDiagnosticEvent: (listener: (event: P2PDiagnosticEvent) => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
requestStatus: () =>
eventHub.emitEvent(EVENT_SERVER_STATUS, {
isConnected: true,
knownAdvertisements: [],
serverPeerId: "fixture-self",
roomId: "fixture-room",
diag: {
totalNewConnections: 0,
totalSuccessfulConnections: 0,
totalFailedConnections: 0,
totalClosedConnections: 0,
details: {},
},
}),
},
};
const core = {
services: {
setting: { currentSettings: () => settings },
API: {
showWindow: async (view: string) => {
document.querySelector("#opened")!.textContent = view;
logMessages.value = ["[P2P Diag] Earlier connection failure"];
mount(LogPane, { target: document.querySelector("#logapp")!, props: { close() {} } });
},
},
},
};
mount(Card, { target: document.querySelector("#app")!, props: { p2p: p2p as never, core: core as never } });
document.querySelector<HTMLButtonElement>("#complete")!.onclick = () => {
for (const listener of listeners)
listener({ category: "sync", code: "pull-completed", timestamp: timestamp + 1_000 });
};
+101
View File
@@ -0,0 +1,101 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { chromium } from "playwright";
for (const width of [320, 390, 768]) {
test(`P2P pane controls fit a ${width}px viewport`, async () => {
const browser = await chromium.launch({ headless: true, args: ["--no-sandbox"] });
try {
const page = await browser.newPage({
viewport: { width, height: 844 },
colorScheme: width === 320 ? "dark" : "light",
});
const errors: string[] = [];
page.on("pageerror", (error) => errors.push(error.message));
const base = process.env.DIAG_UI_BASE_URL ?? "http://127.0.0.1:5157";
await page.goto(`${base}/test/p2p-diagnostics/status-pane/index.html`);
await page.waitForSelector(".peer-item");
assert.deepEqual(errors, [], "Fixture must load without browser errors");
const undersized = await page.locator(".peers-section button").evaluateAll((buttons) =>
buttons
.filter((button) => {
const rect = button.getBoundingClientRect();
return rect.width < 43.5 || rect.height < 43.5;
})
.map((button) => ({
label: button.textContent?.trim(),
width: button.getBoundingClientRect().width,
height: button.getBoundingClientRect().height,
}))
);
assert.deepEqual(undersized, [], "Peer buttons must have at least a 44px target at the default font size");
const overflow = await page.locator(".peer-item").evaluateAll((items) =>
items.flatMap((item, index) => {
const rect = item.getBoundingClientRect();
const style = getComputedStyle(item);
const left = rect.left + parseFloat(style.paddingLeft) + parseFloat(style.borderLeftWidth);
const right = rect.right - parseFloat(style.paddingRight) - parseFloat(style.borderRightWidth);
return Array.from(item.querySelectorAll("button")).flatMap((button) => {
const bounds = button.getBoundingClientRect();
return bounds.left < left - 1 || bounds.right > right + 1
? [
{
peer: index,
button: button.textContent?.trim(),
left: bounds.left,
right: bounds.right,
container: [left, right],
},
]
: [];
});
})
);
assert.deepEqual(overflow, [], "Every peer action must stay inside its card content");
const clipped = await page
.locator(".peer-actions .action-button")
.evaluateAll((buttons) =>
buttons
.filter((button) => button.scrollWidth > button.clientWidth + 1)
.map((button) => button.textContent?.trim())
);
assert.deepEqual(clipped, [], "Peer action labels must remain readable in full");
assert.equal(
await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth),
true,
"Long names must not cause horizontal page overflow"
);
assert.equal(await page.locator(".diag-grid").isVisible(), true);
assert.deepEqual(await page.locator(".diag-item > span:last-child").allTextContents(), [
"4",
"2",
"1",
"1",
]);
if (process.env.DIAG_LAYOUT_SCREENSHOT_PREFIX) {
await page.screenshot({
path: `${process.env.DIAG_LAYOUT_SCREENSHOT_PREFIX}-${width}.png`,
fullPage: true,
});
}
await page.locator(".peer-item").nth(0).getByRole("button", { name: "Replicate now", exact: true }).click();
assert.deepEqual(JSON.parse((await page.locator("#action").textContent()) ?? "null"), {
sync: "permanent-peer",
});
await page
.locator(".peer-item")
.nth(2)
.getByRole("button", { name: "Allow in session", exact: true })
.click();
assert.deepEqual(JSON.parse((await page.locator("#action").textContent()) ?? "null"), {
peerId: "new-peer",
name: "New device",
decision: true,
isTemporary: true,
});
assert.deepEqual(errors, []);
} finally {
await browser.close();
}
});
}
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><title>P2P status pane layout fixture</title>
<style>
:root { --font-ui-small: 12px; --text-muted: #666; --text-normal: #222; --divider-color: #bbb; --size-4-1: 4px; --size-4-2: 8px; --background-secondary: #f4f4f4; --background-tertiary: #ddd; --interactive-normal: #eee; --interactive-hover: #ddd; --background-modifier-success: #bdb; --background-modifier-border: #ddd; --background-modifier-error: #ecc; }
@media (prefers-color-scheme: dark) { :root { color-scheme: dark; --text-muted: #aaa; --text-normal: #ddd; --divider-color: #555; --background-secondary: #262626; --background-tertiary: #444; --interactive-normal: #333; --interactive-hover: #444; --background-modifier-success: #253f25; --background-modifier-border: #444; --background-modifier-error: #522; } }
body { margin: 8px; font: 16px sans-serif; color: var(--text-normal); }
#app { margin: 16px; }
button, select { font: inherit; color: var(--text-normal); background: var(--interactive-normal); }
button { padding: 4px 12px; white-space: nowrap; }
</style></head>
<body><div id="app"></div><output id="action"></output><script type="module" src="./main.ts"></script></body></html>
+67
View File
@@ -0,0 +1,67 @@
import { mount } from "svelte";
import Pane from "../../../src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte";
import { eventHub } from "../../../src/common/events";
import {
EVENT_SERVER_STATUS,
EVENT_P2P_REPLICATOR_STATUS,
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
import { setGlobalLogFunction } from "octagonal-wheels/common/logger";
setGlobalLogFunction(() => undefined);
const settings = { P2P_useDiagRTC: true, P2P_roomID: "fixture-room", remoteConfigurations: {} };
const knownAdvertisements = [
{ peerId: "permanent-peer", name: "Phone", platform: "mobile", isAccepted: true },
{
peerId: "session-peer",
name: "LongDeviceNameWithoutWordBreaks".repeat(4),
platform: "desktop",
isTemporaryAccepted: true,
},
{ peerId: "new-peer", name: "New device", platform: "mobile" },
{ peerId: "denied-peer", name: "Denied device", platform: "mobile", isTemporaryAccepted: false },
];
if (new URLSearchParams(location.search).has("preview")) {
knownAdvertisements.splice(1, 1);
knownAdvertisements.splice(2, 1);
knownAdvertisements[0].name = "Android phone";
knownAdvertisements[1].name = "MacBook Pro (Living room)";
}
const record = (action: unknown) => {
document.querySelector("#action")!.textContent = JSON.stringify(action);
};
const p2p = {
transportLifecycle: { connect: async () => undefined, disconnect: async () => undefined },
changeRelay: { enableBroadcastChanges() {}, disableBroadcastChanges() {} },
peerAdmission: {
makeDecision: async (decision: unknown) => record(decision),
revokeDecision: async (decision: unknown) => record(decision),
},
targetedTransfer: { synchroniseWithPeer: async (peerId: string) => record({ sync: peerId }) },
diagnostics: {
getDiagnosticSnapshot: () => ({ peer: { category: "peer", code: "peer-left", timestamp: 1_700_000_000_000 } }),
onDiagnosticEvent: () => () => undefined,
requestStatus: () => {
eventHub.emitEvent(EVENT_SERVER_STATUS, {
isConnected: true,
knownAdvertisements,
serverPeerId: "fixture-self",
roomId: "fixture-room",
diag: {
totalNewConnections: 4,
totalSuccessfulConnections: 2,
totalFailedConnections: 1,
totalClosedConnections: 1,
details: {},
},
} as never);
eventHub.emitEvent(EVENT_P2P_REPLICATOR_STATUS, {
replicatingFrom: [],
replicatingTo: [],
watchingPeers: [],
isBroadcasting: false,
} as never);
},
},
};
const core = { services: { setting: { currentSettings: () => settings }, API: { showWindow: async () => undefined } } };
mount(Pane, { target: document.querySelector("#app")!, props: { p2p: p2p as never, core: core as never } });
+21
View File
@@ -0,0 +1,21 @@
import { mergeConfig } from "vite";
import { fileURLToPath } from "node:url";
import common from "../../vitest.config.common.ts";
const config = mergeConfig(common, {
optimizeDeps: {
entries: [
"test/p2p-diagnostics/log-pane/index.html",
"test/p2p-diagnostics/status-card/index.html",
"test/p2p-diagnostics/status-pane/index.html",
],
},
server: { host: "127.0.0.1", port: 5157, strictPort: true },
});
config.resolve.alias = {
"@/modules/features/Log/LogPaneView": fileURLToPath(new URL("./log-pane-view.ts", import.meta.url)),
"@/deps": fileURLToPath(new URL("./host-ui.ts", import.meta.url)),
"@/modules/features/SetupManager": fileURLToPath(new URL("./host-ui.ts", import.meta.url)),
...common.resolve.alias,
};
export default config;