mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-11-15 12:40:12 +00:00
Call the three latest category observations 'Diagnostic summary' and retain 'Open log' for their event history. Update the primary English message, status card, and release note together. Validated with the refactored Commonlib candidate: full checks, 2,063 unit tests with one skipped, production build, iOS 15 compatibility, and five Chromium cases. Dependency declarations remain on the published version until the immutable Commonlib update is available.
365 lines
12 KiB
Svelte
365 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from "svelte";
|
|
import { eventHub } from "@/common/events";
|
|
import { delay, fireAndForget } from "octagonal-wheels/promises";
|
|
import type { P2PServerInfo } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
|
|
import {
|
|
EVENT_SERVER_STATUS,
|
|
EVENT_REQUEST_STATUS,
|
|
EVENT_P2P_REPLICATOR_STATUS,
|
|
} from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicatorP2PServer";
|
|
import { EVENT_SETTING_SAVED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents";
|
|
import type { P2PServiceViews } from "@vrtmrz/livesync-commonlib/p2p";
|
|
import type { P2PReplicatorStatus } from "@vrtmrz/livesync-commonlib/compat/replication/trystero/TrysteroReplicator";
|
|
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;
|
|
showBroadcastToggle?: boolean;
|
|
core?: LiveSyncBaseCore;
|
|
}
|
|
|
|
let { p2p, showBroadcastToggle = true, core }: Props = $props();
|
|
let serverInfo = $state<P2PServerInfo | undefined>(undefined);
|
|
let replicatorStatus = $state<P2PReplicatorStatus | undefined>(undefined);
|
|
// Later setting changes arrive through EVENT_SETTING_SAVED; these values only seed local state at mount time.
|
|
const readCurrentSettings = () => core?.services.setting.currentSettings();
|
|
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();
|
|
eventHub.emitEvent(EVENT_REQUEST_STATUS);
|
|
}
|
|
|
|
async function onOpenConnection() {
|
|
await p2p.transportLifecycle.connect();
|
|
await requestServerStatus();
|
|
}
|
|
|
|
async function onDisconnect() {
|
|
await p2p.transportLifecycle.disconnect();
|
|
await requestServerStatus();
|
|
}
|
|
|
|
function toggleBroadcast() {
|
|
if (replicatorStatus?.isBroadcasting) {
|
|
p2p.changeRelay.disableBroadcastChanges();
|
|
} else {
|
|
p2p.changeRelay.enableBroadcastChanges();
|
|
}
|
|
}
|
|
|
|
async function toggleDiagRTC() {
|
|
if (!core) {
|
|
return;
|
|
}
|
|
const next = !useDiagRTC;
|
|
await core.services.setting.updateSettings((settings) => {
|
|
settings.P2P_useDiagRTC = next;
|
|
return settings;
|
|
}, true);
|
|
useDiagRTC = next;
|
|
}
|
|
|
|
onMount(() => {
|
|
const unsubscribeDiagnostics = observeP2PDiagnosticRows(p2p.diagnostics, (rows) => {
|
|
diagnosticRows = rows;
|
|
});
|
|
const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => {
|
|
serverInfo = status;
|
|
roomSuffix = extractP2PRoomSuffix(status?.roomId ?? "");
|
|
});
|
|
const unsubscribeStatus = eventHub.onEvent(EVENT_P2P_REPLICATOR_STATUS, (status) => {
|
|
replicatorStatus = status;
|
|
});
|
|
const unsubscribeSettings = eventHub.onEvent(EVENT_SETTING_SAVED, (settings) => {
|
|
roomSuffix = extractP2PRoomSuffix(settings?.P2P_roomID ?? "");
|
|
useDiagRTC = settings?.P2P_useDiagRTC ?? false;
|
|
});
|
|
|
|
fireAndForget(async () => {
|
|
await delay(100);
|
|
await requestServerStatus();
|
|
});
|
|
|
|
return () => {
|
|
unsubscribeDiagnostics();
|
|
unsubscribe();
|
|
unsubscribeStatus();
|
|
unsubscribeSettings();
|
|
};
|
|
});
|
|
|
|
const isConnected = $derived.by(() => serverInfo?.isConnected);
|
|
const isBroadcasting = $derived.by(() => replicatorStatus?.isBroadcasting ?? false);
|
|
</script>
|
|
|
|
<div class="server-status">
|
|
<h3>{translateMessage("Signalling Status")}</h3>
|
|
|
|
<div class="status-item">
|
|
<span>{translateMessage("Connection:")}</span>
|
|
<span class="status-value {isConnected ? 'connected' : 'disconnected'}">
|
|
{isConnected ? translateMessage("🟢 Connected") : translateMessage("🔴 Disconnected")}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="status-item status-action">
|
|
{#if !isConnected}
|
|
<button onclick={onOpenConnection}>{translateMessage("Open connection")}</button>
|
|
{:else}
|
|
<button onclick={onDisconnect}>{translateMessage("Disconnect")}</button>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if serverInfo}
|
|
<div class="status-item">
|
|
<span>{translateMessage("Room ID suffix:")}</span>
|
|
<span class="room-suffix-display" title={roomSuffix || translateMessage("Not configured")}>
|
|
{roomSuffix || "-"}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="status-item">
|
|
<span>{translateMessage("Peer ID:")}</span>
|
|
<span class="peer-id-display" title={serverInfo.serverPeerId}>
|
|
{serverInfo.serverPeerId.slice(0, 12)}...
|
|
</span>
|
|
</div>
|
|
|
|
<div class="status-item">
|
|
<span>{translateMessage("Devices:")}</span>
|
|
<span>{serverInfo.knownAdvertisements.length}</span>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if showBroadcastToggle}
|
|
<div class="status-item status-action broadcast-row">
|
|
<label class="broadcast-label" for="broadcast-toggle">
|
|
{translateMessage("Announce changes")}
|
|
</label>
|
|
<button
|
|
id="broadcast-toggle"
|
|
class="broadcast-button {isBroadcasting ? 'is-on' : 'is-off'}"
|
|
onclick={toggleBroadcast}
|
|
title={isBroadcasting
|
|
? translateMessage("Stop announcing changes")
|
|
: translateMessage("Start announcing changes")}
|
|
>
|
|
{isBroadcasting ? translateMessage("📡 On") : translateMessage("📡 Off")}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if core}
|
|
<div class="status-item status-action diag-toggle-row">
|
|
<label class="broadcast-label" for="diag-toggle">
|
|
{translateMessage("🕵️ Diag")}
|
|
</label>
|
|
<button
|
|
id="diag-toggle"
|
|
class="broadcast-button {useDiagRTC ? 'is-on' : 'is-off'}"
|
|
onclick={toggleDiagRTC}
|
|
title={useDiagRTC
|
|
? 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>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if serverInfo}
|
|
<div class="diag-section">
|
|
<h4>{translateMessage("Stats")}</h4>
|
|
<div class="diag-grid">
|
|
<div class="diag-item">
|
|
<span>{translateMessage("Incoming:")}</span>
|
|
<span>{serverInfo.diag.totalNewConnections}</span>
|
|
</div>
|
|
<div class="diag-item">
|
|
<span>{translateMessage("Connected:")}</span>
|
|
<span>{serverInfo.diag.totalSuccessfulConnections}</span>
|
|
</div>
|
|
<div class="diag-item">
|
|
<span>{translateMessage("Failed:")}</span>
|
|
<span>{serverInfo.diag.totalFailedConnections}</span>
|
|
</div>
|
|
<div class="diag-item">
|
|
<span>{translateMessage("Closed:")}</span>
|
|
<span>{serverInfo.diag.totalClosedConnections}</span>
|
|
</div>
|
|
</div>
|
|
<details class="diag-details">
|
|
<summary>{translateMessage("Diagnostic summary")}</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;
|
|
}
|
|
|
|
h3 {
|
|
margin: 0 0 0.75rem 0;
|
|
font-weight: 600;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.status-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
overflow-wrap: anywhere;
|
|
font-size: 0.9rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.status-action {
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.status-value {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.status-value.connected {
|
|
color: var(--text-success);
|
|
}
|
|
|
|
.status-value.disconnected {
|
|
color: var(--text-error);
|
|
}
|
|
|
|
.peer-id-display {
|
|
font-family: monospace;
|
|
font-size: 0.85rem;
|
|
max-width: 150px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.room-suffix-display {
|
|
font-family: monospace;
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.broadcast-row {
|
|
align-items: center;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.diag-toggle-row {
|
|
align-items: center;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
.broadcast-label {
|
|
font-size: 0.9rem;
|
|
color: var(--text-normal);
|
|
cursor: pointer;
|
|
}
|
|
|
|
.broadcast-button {
|
|
font-size: 0.8rem;
|
|
padding: 0.2rem 0.6rem;
|
|
border: 1px solid var(--divider-color);
|
|
border-radius: 0.4rem;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: background-color 0.15s;
|
|
}
|
|
|
|
.broadcast-button.is-on {
|
|
background-color: var(--interactive-accent);
|
|
color: var(--text-on-accent);
|
|
border-color: var(--interactive-accent);
|
|
}
|
|
|
|
.broadcast-button.is-off {
|
|
background-color: var(--interactive-normal);
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.broadcast-button.is-off:hover {
|
|
background-color: var(--interactive-hover);
|
|
color: var(--text-normal);
|
|
}
|
|
|
|
.diag-section {
|
|
border-top: 1px solid var(--divider-color);
|
|
margin-top: 0.75rem;
|
|
padding-top: 0.75rem;
|
|
}
|
|
|
|
.diag-section h4 {
|
|
margin: 0 0 0.5rem 0;
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.diag-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
|
gap: 0.35rem 0.75rem;
|
|
}
|
|
|
|
.diag-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 0.85rem;
|
|
gap: 0.5rem;
|
|
}
|
|
</style>
|