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

@@ -14,6 +14,8 @@
import { extractP2PRoomSuffix } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { $msg as translateMessage } from "@/common/translation";
import { observeP2PDiagnosticRows, type P2PDiagnosticRow } from "@/modules/features/P2PDiagnosticDisplay";
import { VIEW_TYPE_LOG } from "@/modules/features/Log/LogPaneView";
interface Props {
p2p: P2PServiceViews;
@@ -29,6 +31,12 @@
const initialSettings = readCurrentSettings();
let roomSuffix = $state<string>(extractP2PRoomSuffix(initialSettings?.P2P_roomID ?? ""));
let useDiagRTC = $state<boolean>(initialSettings?.P2P_useDiagRTC ?? false);
let diagnosticRows = $state<readonly P2PDiagnosticRow[]>([]);
function openLog() {
const currentCore = core;
if (currentCore) fireAndForget(() => currentCore.services.API.showWindow(VIEW_TYPE_LOG));
}
async function requestServerStatus() {
p2p.diagnostics.requestStatus();
@@ -66,6 +74,9 @@
}
onMount(() => {
const unsubscribeDiagnostics = observeP2PDiagnosticRows(p2p.diagnostics, (rows) => {
diagnosticRows = rows;
});
const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
serverInfo = status;
roomSuffix = extractP2PRoomSuffix(status?.roomId ?? "");
@@ -84,6 +95,7 @@
});
return () => {
unsubscribeDiagnostics();
unsubscribe();
unsubscribeStatus();
unsubscribeSettings();
@@ -161,8 +173,8 @@
class="broadcast-button {useDiagRTC ? 'is-on' : 'is-off'}"
onclick={toggleDiagRTC}
title={useDiagRTC
? translateMessage("Diagnostic RTCPeerConnection is enabled")
: translateMessage("Use Diagnostic RTCPeerConnection for statistics")}
? translateMessage("Detailed P2P diagnostics and Log Pane output are enabled")
: translateMessage("Enable detailed P2P diagnostics and Log Pane output")}
>
{useDiagRTC ? translateMessage("On") : translateMessage("Off")}
</button>
@@ -190,12 +202,48 @@
<span>{serverInfo.diag.totalClosedConnections}</span>
</div>
</div>
<details class="diag-details">
<summary>{translateMessage("Diagnostic log")}</summary>
<div class="diag-summary">
{#each diagnosticRows as row (row.category)}
<div title={row.title}>
<span>{row.label}: {row.text}</span>{#if row.time}<span> · {row.time}</span>{/if}
</div>
{/each}
{#if core}
<button class="diag-open-log" onclick={openLog}>{translateMessage("Open log")}</button>
{/if}
</div>
</details>
</div>
{/if}
</div>
<style>
.diag-details {
margin-top: var(--size-4-2);
}
.diag-details > summary {
color: var(--text-muted);
font-size: var(--font-ui-small);
cursor: pointer;
}
.diag-summary {
margin-top: var(--size-4-2);
color: var(--text-muted);
font-size: var(--font-ui-small);
overflow-wrap: anywhere;
}
.diag-open-log {
margin-top: var(--size-4-1);
font-size: inherit;
}
.server-status {
min-width: 0;
border: 1px solid var(--divider-color);
border-radius: 0.5rem;
padding: 1rem;
@@ -211,6 +259,7 @@
display: flex;
justify-content: space-between;
flex-wrap: wrap;
overflow-wrap: anywhere;
font-size: 0.9rem;
margin-bottom: 0.5rem;
}
@@ -498,37 +498,39 @@
<span class="badge status-chip {getAcceptanceStatusClass(peer)}">
{getAcceptanceStatus(peer)}
</span>
<button
class="emoji-button"
disabled={replicatingPeerId !== null}
title={replicatingPeerId === peer.peerId
? translateMessage("Replicating...")
: translateMessage("Replicate now")}
aria-label={replicatingPeerId === peer.peerId
? translateMessage("Replicating")
: translateMessage("Replicate now")}
onclick={() => startReplication(peer)}
>
{replicatingPeerId === peer.peerId ? "⏳" : "🔄"}
</button>
<button
class="action-button"
disabled={decidingPeerId !== null}
onclick={() => revokeDecision(peer)}
>
{translateMessage("Revoke")}
</button>
<button
class="emoji-button"
title={translateMessage("More actions for ${DEVICE}", { DEVICE: peer.name })}
aria-label={translateMessage("More actions for ${DEVICE}", {
DEVICE: peer.name,
})}
disabled={!canEditP2PSettings()}
onclick={(event) => openPeerMenu(event, peer)}
>
…
</button>
<div class="decision-buttons">
<button
class="emoji-button"
disabled={replicatingPeerId !== null}
title={replicatingPeerId === peer.peerId
? translateMessage("Replicating...")
: translateMessage("Replicate now")}
aria-label={replicatingPeerId === peer.peerId
? translateMessage("Replicating")
: translateMessage("Replicate now")}
onclick={() => startReplication(peer)}
>
{replicatingPeerId === peer.peerId ? "⏳" : "🔄"}
</button>
<button
class="action-button"
disabled={decidingPeerId !== null}
onclick={() => revokeDecision(peer)}
>
{translateMessage("Revoke")}
</button>
<button
class="emoji-button"
title={translateMessage("More actions for ${DEVICE}", { DEVICE: peer.name })}
aria-label={translateMessage("More actions for ${DEVICE}", {
DEVICE: peer.name,
})}
disabled={!canEditP2PSettings()}
onclick={(event) => openPeerMenu(event, peer)}
>
…
</button>
</div>
</div>
<div class="decision-row watch-row">
<span class="decision-label">{translateMessage("Follow changes")}</span>
@@ -554,45 +556,49 @@
</div>
<div class="decision-row">
<span class="decision-label">{translateMessage("PERMANENT")}</span>
<button
class="emoji-button"
title={translateMessage("Allow permanently")}
aria-label={translateMessage("Allow permanently")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, true, false)}
>
✅
</button>
<button
class="emoji-button mod-warning"
title={translateMessage("Deny permanently")}
aria-label={translateMessage("Deny permanently")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, false, false)}
>
🚫
</button>
<div class="decision-buttons">
<button
class="emoji-button"
title={translateMessage("Allow permanently")}
aria-label={translateMessage("Allow permanently")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, true, false)}
>
✅
</button>
<button
class="emoji-button mod-warning"
title={translateMessage("Deny permanently")}
aria-label={translateMessage("Deny permanently")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, false, false)}
>
🚫
</button>
</div>
</div>
<div class="decision-row">
<span class="decision-label">{translateMessage("SESSION")}</span>
<button
class="emoji-button"
title={translateMessage("Allow in session")}
aria-label={translateMessage("Allow in session")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, true, true)}
>
✅
</button>
<button
class="emoji-button mod-warning"
title={translateMessage("Deny in session")}
aria-label={translateMessage("Deny in session")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, false, true)}
>
🚫
</button>
<div class="decision-buttons">
<button
class="emoji-button"
title={translateMessage("Allow in session")}
aria-label={translateMessage("Allow in session")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, true, true)}
>
✅
</button>
<button
class="emoji-button mod-warning"
title={translateMessage("Deny in session")}
aria-label={translateMessage("Deny in session")}
disabled={decidingPeerId !== null}
onclick={() => makeDecision(peer, false, true)}
>
🚫
</button>
</div>
</div>
{/if}
{#if !isAccepted(peer) && (peer.isAccepted !== undefined || peer.isTemporaryAccepted !== undefined)}
@@ -626,11 +632,17 @@
overflow-wrap: anywhere;
}
.p2p-container {
min-width: 0;
display: flex;
flex-direction: column;
gap: 1rem;
}
.peers-section button {
min-width: 2.75rem;
min-height: 2.75rem;
}
.peers-section {
border: 1px solid var(--divider-color);
border-radius: 0.5rem;
@@ -647,6 +659,8 @@
.pane-header-actions {
display: flex;
flex-wrap: wrap;
max-width: 100%;
align-items: center;
gap: 0.4rem;
min-width: 0;
@@ -654,6 +668,8 @@
.remote-picker-wrap {
display: inline-flex;
flex: 1 1 10rem;
max-width: 100%;
gap: 0.3rem;
align-items: center;
min-width: 0;
@@ -661,8 +677,8 @@
.remote-picker {
max-width: 10rem;
min-width: 1em;
flex-shrink: 1;
min-width: 0;
flex: 1 1 0;
height: 1.9rem;
border: 1px solid var(--divider-color);
border-radius: 0.4rem;
@@ -681,10 +697,11 @@
margin: 0;
font-size: 1.1rem;
font-weight: 700;
white-space: nowrap;
overflow-wrap: anywhere;
}
.icon-button {
padding: 0;
width: 1.9rem;
height: 1.9rem;
display: inline-flex;
@@ -720,8 +737,12 @@
}
.refresh {
font-size: 0.8rem;
padding: 0.2rem 0.5rem;
max-width: 100%;
height: auto;
white-space: normal;
overflow-wrap: anywhere;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--divider-color);
border-radius: 0.3rem;
background-color: var(--interactive-normal);
@@ -756,6 +777,8 @@
}
.peer-name {
flex-wrap: wrap;
overflow-wrap: anywhere;
font-weight: 600;
margin-bottom: 0.25rem;
font-size: 0.9rem;
@@ -772,6 +795,9 @@
}
.badge {
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
background-color: var(--background-tertiary);
padding: 0.1rem 0.4rem;
border-radius: 0.25rem;
@@ -837,17 +863,25 @@
}
.decision-row {
display: grid;
grid-template-columns: 1fr auto auto;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 0.35rem;
}
.accepted-row {
grid-template-columns: 1fr auto auto auto;
.decision-buttons {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
min-width: 0;
max-width: 100%;
}
.decision-label {
min-width: 0;
max-width: 100%;
overflow-wrap: anywhere;
font-size: 0.7rem;
font-weight: 700;
color: var(--text-muted);
@@ -855,22 +889,25 @@
}
.action-button {
font-size: 0.75rem;
padding: 0.2rem 0.45rem;
font-size: 0.9rem;
padding: 0.5rem 0.75rem;
border: 1px solid var(--divider-color);
border-radius: 0.3rem;
background-color: var(--interactive-normal);
color: var(--text-normal);
cursor: pointer;
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
height: auto;
white-space: normal;
overflow-wrap: anywhere;
}
.emoji-button {
width: 2rem;
height: 1.7rem;
flex-shrink: 0;
width: 2.75rem;
height: 2.75rem;
font-size: 1.125rem;
display: inline-flex;
align-items: center;
justify-content: center;
@@ -923,7 +960,7 @@
}
.revoke-inline {
justify-self: start;
align-self: flex-start;
}
.no-peers {