diff --git a/docs/README.md b/docs/README.md index 6699b1a9..3d2856cf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,7 +14,7 @@ A document's original date and reviewed-version fields describe its baseline. Re | Find hosting services proposed by the community | [Third-party hosting listings](../README.md#hosting-services-proposed-by-the-community) | | Set up S3-compatible storage | [Object Storage Setup](setup_object_storage.md) and [transfers and service costs](setup_object_storage_costs.md) | | Set up peer-to-peer synchronisation | [P2P communication model](p2p.md), then [Peer-to-Peer Setup](setup_p2p.md) | -| Diagnose P2P connectivity | [Browser-assisted check](../src/apps/webpeer/README.md#p2p-connection-check), [network tips](tips/p2p-sync-tips.md), and [self-hosted TURN](../docker/coturn/README.md) | +| Diagnose P2P connectivity or transfer activity | [Diagnostic summary and logs](tips/p2p-diagnostics.md), [browser-assisted check](../src/apps/webpeer/README.md#p2p-connection-check), [network tips](tips/p2p-sync-tips.md), and [self-hosted TURN](../docker/coturn/README.md) | | Understand a setting or command | [Settings reference](settings.md) | | Read or customise synchronisation status | [Status indicators and CSS snippets](tips/status-display.md) | | Investigate or recover from a problem | [Troubleshooting](troubleshooting.md), then the relevant [recovery procedure](recovery.md) | diff --git a/docs/p2p.md b/docs/p2p.md index 75160f48..64fe6339 100644 --- a/docs/p2p.md +++ b/docs/p2p.md @@ -2,7 +2,15 @@ Peer-to-peer (P2P) synchronisation transfers Vault data between LiveSync devices through WebRTC. It does not require a central database containing a copy of the Vault. It does require a signalling relay so that devices can discover one another and establish a connection. -For the procedure for the first and additional devices, see [Set up peer-to-peer synchronisation](setup_p2p.md). For connection problems, see [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md). +For the procedure for the first and additional devices, see [Set up peer-to-peer synchronisation](setup_p2p.md). For connection problems, see [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md). Use the [P2P diagnostic guide](tips/p2p-diagnostics.md) to investigate discovery and transfer problems with the summary, logs, and progress indicators. + +## Updating a P2P group + +The Trystero 0.26.0 transport uses a data format which is +incompatible with the previous transport. Update every device in the P2P +group, including CLI and WebPeer clients, before resuming P2P synchronisation. +Keeping the same Group ID, passphrase, and relay settings does not make mixed +transport versions compatible. ## Connection model diff --git a/docs/recovery.md b/docs/recovery.md index 06b09edd..587238fa 100644 --- a/docs/recovery.md +++ b/docs/recovery.md @@ -98,6 +98,7 @@ The `Hatch` recovery controls are ordered by escalation. Running **Recreate chun - **Compare with Vault** opens a read-only comparison for readable text. - **Apply this revision to Vault** replaces the Vault file with that readable database revision. - **Mark this revision as the Vault version** records an exact byte-for-byte match without creating a child revision. + - **Correct recorded file size** is available when a readable, conflict-free database winner has the same bytes as the Vault but its recorded size differs. It creates a child Metadata revision with the decoded byte size while preserving the body, Chunks, path, and timestamps. The action rechecks the selected revision; if the database has advanced, inspect the file again. - **Store Vault file as a child of this revision** preserves the current Vault bytes on that selected branch. - **Retry reading revision** retries configured chunk retrieval without changing the revision tree. - **Apply logical deletion to Vault**, **Discard this branch**, and **Discard unreadable revision** are destructive decisions. Use them only after preserving every version which may still be needed. diff --git a/docs/settings.md b/docs/settings.md index 77493609..abf32307 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -963,7 +963,7 @@ Recreate chunks from files currently present in the Vault. This can repair missi Compare each Vault file with every current leaf revision in the local database. Each winner and conflict revision is shown separately with its exact revision identifier, local chunk availability, and relationship to the current Vault file. Unavailable shared ancestors are reported separately because they prevent conservative three-way merging but are not current leaves which can be discarded. -Select **Begin inspection** to run the inspection. Each reported file and current leaf revision has a wrench menu for read-only comparison, applying an exact database revision to the Vault, recording an exact byte match, preserving the Vault file as a child of a selected branch, retrying chunk retrieval, or explicitly discarding a branch. Destructive actions require confirmation. Follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file) before changing revision history. +Select **Begin inspection** to run the inspection. Each reported file and current leaf revision has a wrench menu for read-only comparison, applying an exact database revision to the Vault, recording an exact byte match, correcting an incorrect recorded file size, preserving the Vault file as a child of a selected branch, retrying chunk retrieval, or explicitly discarding a branch. Size correction is available when a readable revision exactly matches the Vault with no conflicts; it preserves the content and file timestamps and rechecks the selected revision before writing. Destructive actions require confirmation. Follow [Recover a conflicted or mismatched file](recovery.md#recover-a-conflicted-or-mismatched-file) before changing revision history. The same inspection also reports local Metadata whose stored document ID does not agree with its recorded path. A stale entry does not suppress ordinary inspection when consistently addressed Metadata can still be resolved for that logical path; otherwise, the unresolved path is excluded from ordinary file-repair actions. When the current winner has no conflict leaves and has an unambiguous target, its wrench menu offers a separately confirmed, one-entry repair. The target is derived from the current local file-name case and path obfuscation settings, then written and verified before the obsolete ID is removed. Ambiguous, conflicted, deleted, excluded, or otherwise unsafe entries remain read-only. This action does not rename Vault files or folders. Follow [Repair a Metadata document ID mismatch](recovery.md#repair-a-metadata-document-id-mismatch) for the complete backup, repair, propagation, and verification procedure. For widespread naming differences across devices, use that guide to choose an authoritative Vault, correct its storage names while Obsidian is closed, rebuild the central remote, and reset the other devices. diff --git a/docs/specs_conflict_resolution.md b/docs/specs_conflict_resolution.md index 6f29fedb..36678cb4 100644 --- a/docs/specs_conflict_resolution.md +++ b/docs/specs_conflict_resolution.md @@ -89,6 +89,8 @@ Each reported current leaf revision has a compact wrench menu. The available act Every mutating action rechecks that the selected revision is still a current leaf. If another operation resolved or replaced it, the action fails and the card is refreshed instead of extending an obsolete branch. +The inspection also offers **Correct recorded file size** when the conflict-free current winner is readable, its decoded bytes match the Vault exactly, and its recorded size differs from the decoded byte length. This creates a child Metadata revision which changes only the recorded size while retaining the content references and file timestamps. The operation rechecks the current revision before writing and records the resulting byte match as Vault provenance. Size and timestamps diagnose this condition; exact bytes establish that the Vault and database contain the same file. + The card uses compact, mobile-friendly diagnostic rows with an emoji and a text label. `🧩 Missing chunks: N` identifies an unreadable revision. In the database row, `Ξ”size` is decoded size minus recorded size; in the Vault row, `Ξ”size vs DB` is Vault size minus decoded database size. `Ξ”time` is Vault modification time minus database modification time. The ordinary two-second comparison window still labels which side is newer. These values help diagnose a mismatch; path, size, and modification time do not prove revision identity or decide which content should win. A shared ancestor is informational. An ancestor which is no longer a current leaf cannot be discarded independently through this workflow. If its body is unavailable, conservative three-way merge remains disabled, although readable current leaf revisions can still be selected manually. diff --git a/docs/tips/p2p-diagnostics.md b/docs/tips/p2p-diagnostics.md new file mode 100644 index 00000000..d660ea8c --- /dev/null +++ b/docs/tips/p2p-diagnostics.md @@ -0,0 +1,138 @@ +# Diagnose P2P synchronisation + +Use **P2P Status** to distinguish peer discovery, authentication, and data +transfer before changing connection or database settings. + +Participating devices must use +[compatible P2P transport versions](../p2p.md#updating-a-p2p-group). +If your version does not show **Diagnostic summary**, use +[Peer-to-Peer Synchronisation Tips](p2p-sync-tips.md) and include the installed +versions in your report. + +## Capture one attempt + +1. Keep both devices online, with Obsidian visible and the devices awake. + Record which device originates the change and which should receive it. +2. Open **P2P Status** on both devices, using + **Self-hosted LiveSync: P2P Sync : Open P2P Status** in the command palette. +3. In the **P2P Status** pane, expand **Diagnostic summary** below **Stats**. + It starts collapsed. Record all three rows and their observation times before + changing connection or database settings. +4. Turn the **Diag** toggle on in the **P2P Status** card, then select + **Open log** inside the expanded summary. In the opened Log Pane, ensure + **Pause** is cleared before reproducing the problem. New diagnostic lines + contain `[P2P Diag]`; **Verbose Log** is not required for these lines. +5. Select **Replicate now** for the intended peer to test a manual round trip. + Check **P2P Connection Request** prompts on both devices. + For a problem with later automatic changes, first establish that the manual + operation works, then make a small, non-private test edit on the source. + Observe the receiving device as well as the source. +6. Record the last relevant messages and progress on both devices, then + [collect a report](#collect-a-report) before restarting or retrying. + +The summary continues updating with **Diag** off. **Diag** controls new detailed +output in the Log Pane; enabling it does not replay earlier hidden lines. +Diagnostic messages also enter the existing recent report history while +**Diag** is off. Opening the Log Pane displays messages already in its display +buffer. Repeated unchanged non-failure observations may be omitted, so the log +is not a record of every network message. **Pause** affects only the log display; +replication and the summary continue updating. + +## Read the summary + +Each row holds the latest recorded observation in one category on this device. +A new event from another peer can replace that row. Evidence can remain after +a peer leaves or a connection closes; use the peer identifier and timestamp +rather than reading the rows as a current connection-health checklist. The +summary is held in memory and does not preserve history across a restart. + +| Row | Examples | What it establishes | +| --------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Relay** | `Room opening`, `Room opened`, `Room could not open`, `Room closed` | The local room lifecycle. 'Room opened; relay replies not observed' records local room opening. Relay acceptance remains unobserved; use later peer and sync observations to investigate discovery and data transfer. | +| **Peer** | `Peer advertised`, `Peer left`, `Authentication accepted`, `Authentication refused`, `Authentication timed out` | Discovery or the observed authentication outcome for the identified peer. A timeout indicates that the request exceeded its time limit, rather than an explicit rejection. | +| **Sync** | `Pull started`, `Pull completed`, `Watching peer changes`, `Announcing changes`, `Change notification received` | The latest observed pull, watch, announcement, or notification activity. Note that notifications signal a peer to fetch changes, rather than carrying Vault data directly. | + +- **Not observed** indicates that no event has been recorded yet for that + category, rather than confirming success, failure, or a zero count. +- **Unavailable** means the diagnostic capability is unavailable in the + current implementation. Include the installed version in the report. +- `Authentication failed; authentication response not observed` and + `Automatic watch skipped; peer announcement state not observed` identify an + unavailable response, rather than claiming refusal or disabled announcements. +- Summary times are local observation times, while `[P2P Diag]` lines include an + `observed` UTC timestamp. Consider any device-clock differences when comparing + reports across devices. Timestamps record when this device observed an event; + they are not an ongoing heartbeat or live health check. + +Relay rejection details are not provided by this diagnostic summary. +Some existing RTC diagnostics also lack peer or room attribution and are not +assigned to individual peers in these rows. **Stats** describes connection activity, +whereas transferred document totals appear in transfer progress. If the summary +cannot fully identify a failure, preserve the log output and note any unobserved +categories in your report. + +## Understand transfer progress + +During synchronisation, transfer counters appear next to each peer in the +P2P Status pane, in the editor status bar, and in the main status bar when +P2P is the active synchronisation method. + +| Indicator | Meaning | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **↑ / ↓** | Successfully sent or received database document revisions for the observed peer and direction, including both Metadata and Chunks. These are not counts of Vault files written. A new replication attempt starts new counts. | +| **⏩↑ / ⏩↓** | Changes-feed entries compared in that direction, including entries already held by the receiver. The count can advance while no revisions need transferring. | +| **seq** | A compatibility display of database sequence progress when document statistics are unavailable. Interpret it as a sequence value, not a document total or percentage. | + +Each directional comparison label disappears three seconds after its count +last changes. A hidden label alone does not establish whether comparison or +replication has finished. A zero transfer count can be normal when the receiver +already has the data. + +`Pull completed` confirms that a single database pull has finished. To verify +bidirectional synchronisation and full reflection, ensure the reverse direction +completes and check the updated notes on the receiving device. For missing files, +follow +[the corresponding troubleshooting procedure](../troubleshooting.md#files-are-missing-or-excluded). + +## Choose the next check + +| Symptom or observation | Next check | +| --------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| The peer does not appear | Capture the **Relay** and **Peer** rows, then verify both devices' compatible versions, relay set, Group ID, and passphrase using [discovery checks](p2p-sync-tips.md#a-peer-does-not-appear). A `Connected` status indicates that the local room is open; relay replies remain unobserved. Use peer and sync observations and both devices' logs to investigate discovery and transfer. | +| Authentication is refused | Confirm the peer identity on both devices and check for pending approval prompts. The summary displays the observed outcome; please ensure the connection request is explicitly accepted on the receiving device. | +| Authentication times out, or a peer appears but cannot transfer | Keep both devices active and check for an unanswered **P2P Connection Request** before troubleshooting network connectivity. Compare logs between devices, then follow existing [WebRTC and TURN checks](p2p-sync-tips.md#a-peer-appears-but-synchronisation-cannot-connect) to inspect the data path. | +| Manual replication works, but later edits do not arrive | Confirm **Announce changes** on the source and **Follow changes** for that source on the receiver. Compare `Announcing changes`, `Watching peer changes`, and notification events on both devices. `Change notification ignored; peer is not watched` identifies a missing follow on the receiver. | +| A pull remains the last observation | Compare its timestamp, ↑/↓ and ⏩ changes, and both devices' logs. A subsequent operation may have replaced the summary row; verify with the surrounding logs and transfer counters to confirm whether a transfer is truly stalled. For a repeatable network-specific stall, use [message-size checks](p2p-sync-tips.md#a-connection-opens-but-a-transfer-stalls). | + +After capturing the initial symptom, selecting **Disconnect** followed by +**Open connection** and **Replicate now** can test reconnection. Because +disconnecting cancels active operations, record whether a cancellation occurred. +Testing reconnection, adjusting the message-size preset, or trying an +alternative network can help isolate environmental differences. + +## Collect a report + +Collect evidence from both the source and receiving devices around the same +attempt. Include: + +- the plug-in or client version on each device, and which device originated the change; +- the **Diagnostic summary** rows and times, plus the surrounding `[P2P Diag]` + lines and any other relevant errors; +- whether the peer appeared, whether manual **Replicate now** worked, and the + observed ↑/↓, ⏩, or `seq` values; +- whether **Announce changes** and **Follow changes** were enabled, and whether + either application was in the background or the device had been asleep; and +- the network types and whether TURN or a VPN was used, especially when the + same devices work on one network but not another. + +Run **Generate full report for opening the issue with debug info** before the +recent history is overwritten. It includes up to 1,000 recent log lines, +including verbose diagnostic messages even when **Verbose Log** is off. +For a problem which crosses a restart, follow the existing +[persistent logging procedure](../to_issue_reporting.md#a-problem-which-crosses-a-restart). + +Review the complete report using the +[issue-reporting redaction guidance](../to_issue_reporting.md#full-livesync-report). +New `[P2P Diag]` messages use fixed descriptions and shortened peer identifiers, +but other log messages can still contain private information. Keep consistent +device aliases across the reports when replacing private identifiers. diff --git a/docs/tips/p2p-sync-tips.md b/docs/tips/p2p-sync-tips.md index 74a28370..c9e5a59f 100644 --- a/docs/tips/p2p-sync-tips.md +++ b/docs/tips/p2p-sync-tips.md @@ -67,6 +67,8 @@ Keep Obsidian visible and the device awake during initial transfer, rebuild, or ## Collect evidence +Use the [P2P diagnostic guide](p2p-diagnostics.md) to capture the latest observations, enable **Diag**, and compare transfer progress and logs on both devices before changing settings. + If the same room works on one network but not another, include both network types in the report. Run `Generate full report for opening the issue with debug info`, remove credentials and private relay details, and state whether: - both devices reached `Connected`; diff --git a/docs/to_issue_reporting.md b/docs/to_issue_reporting.md index dc64edc1..0ffb7ae1 100644 --- a/docs/to_issue_reporting.md +++ b/docs/to_issue_reporting.md @@ -11,6 +11,7 @@ If a maintainer requests additional evidence, follow the relevant procedure belo | Database integrity, current conflicts, or differences between sources | [Integrity report](#integrity-report) | | Local revision and Chunk usage | [Usage report](#usage-report) | | More information about a path pseudonym in an existing report | [File follow-up](#file-follow-up) | +| P2P discovery, authentication, or transfer activity | [P2P summary and logs](tips/p2p-diagnostics.md#collect-a-report) | Maintainers can link directly to each procedure and adapt the [example requests](#requesting-diagnostics). diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 8da3a3d5..ac8bbbab 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -270,7 +270,7 @@ Use Safari on a Mac and follow Apple's [Inspecting iOS and iPadOS](https://devel ## P2P does not connect or transfer changes -Use [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md). Check signalling discovery separately from the WebRTC data path, and confirm which devices announce and follow changes. P2P is not a repair step for another transport. +Start with [P2P diagnostic observations, logs, and transfer progress](tips/p2p-diagnostics.md). Use [Peer-to-Peer Synchronisation Tips](tips/p2p-sync-tips.md) for network and configuration checks. Check signalling discovery separately from the WebRTC data path, and confirm which devices announce and follow changes. P2P is not a repair step for another transport. ## Obsidian or LiveSync remains suspended diff --git a/package-lock.json b/package-lock.json index f3e355e7..6616e73e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.3.2", + "@vrtmrz/livesync-commonlib": "0.4.0", "@vrtmrz/obsidian-plugin-kit": "0.1.7", "@vrtmrz/ui-interactions": "0.1.2", "@vrtmrz/value-store": "0.1.0", @@ -1949,9 +1949,9 @@ } }, "node_modules/@noble/secp256k1": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.1.0.tgz", - "integrity": "sha512-+F7iS7tUMaNGXcc9X3PjmjvuQnXEuSjCRNzVVA2xAcKXgCaP0dHYz4SFyt4FKNHef7sOP//xihowcySSS7PK9g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-3.2.0.tgz", + "integrity": "sha512-Z3ZAWOTxJ0EuTuZTi7Y69iK7GgrLHh8sgm45lVDhg/b3Nk1TpAiKhick2KkZisHuupeepSkyIydN/J459SdX1w==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -3281,6 +3281,22 @@ "vite": "^8.0.0-beta.7 || ^8.0.0" } }, + "node_modules/@trystero-p2p/core": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@trystero-p2p/core/-/core-0.26.0.tgz", + "integrity": "sha512-CZpeXPgfbDdFKrGxfD98F9tn/fTjya4mQ2o/Ax6H0qpBaJXCBz5+jyPtjsYEPAAcKRK/btOatCetFiQ5qIMCOA==", + "license": "MIT" + }, + "node_modules/@trystero-p2p/nostr": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@trystero-p2p/nostr/-/nostr-0.26.0.tgz", + "integrity": "sha512-HArp9fonBkqFZgw3U2D823fCePQv7haKkvjtwbGRkWzy+Wo9G3cCDEnG/GENDWwzERHrhRcRacpFsFt4JpwRJQ==", + "license": "MIT", + "dependencies": { + "@noble/secp256k1": "^3.2.0", + "@trystero-p2p/core": "0.26.0" + } + }, "node_modules/@tsconfig/svelte": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", @@ -4065,9 +4081,9 @@ } }, "node_modules/@vrtmrz/livesync-commonlib": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.3.2.tgz", - "integrity": "sha512-PiKUdJUiBI4ot0AV1BjlkDoaY4Lbn9oTdEjL8FMX1qf2VCOwSnbsLoQMUr/45wmINpV31KWmqgr2zxqcYO+hpA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@vrtmrz/livesync-commonlib/-/livesync-commonlib-0.4.0.tgz", + "integrity": "sha512-2bhkrd0rRWSiy90rjOxpaO01Ei2V9VSwifzv4ePUttxrzt195zjWNZCzW9ZMvJvAtyPoifShilW0J4kig4oKFA==", "license": "MIT", "dependencies": { "@aws-crypto/sha256-js": "5.2.0", @@ -4077,7 +4093,7 @@ "@smithy/middleware-apply-body-checksum": "^4.3.9", "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", - "@trystero-p2p/nostr": "0.25.3", + "@trystero-p2p/nostr": "0.26.0", "@vrtmrz/value-store": "0.1.0", "diff-match-patch": "^1.0.5", "events": "^3.3.0", @@ -4115,22 +4131,6 @@ } } }, - "node_modules/@vrtmrz/livesync-commonlib/node_modules/@trystero-p2p/core": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@trystero-p2p/core/-/core-0.25.3.tgz", - "integrity": "sha512-lQKNq/ha+vF6kQZrpaXJGzlzxLF/Fhizoy5dwJUYQlkqRrbPup/Jov2EpPX/CPr2X+f79HxVzonkeni3MxmCuQ==", - "license": "MIT" - }, - "node_modules/@vrtmrz/livesync-commonlib/node_modules/@trystero-p2p/nostr": { - "version": "0.25.3", - "resolved": "https://registry.npmjs.org/@trystero-p2p/nostr/-/nostr-0.25.3.tgz", - "integrity": "sha512-nZV9Fl/GXuhIkJSQ+wIkdMoRLO1oSTUL/+vZorukaojeAdOpT/61e8b9Vzbt8L1ktMTaGPEJHw2BG/B5BCwBuw==", - "license": "MIT", - "dependencies": { - "@noble/secp256k1": "^3.1.0", - "@trystero-p2p/core": "0.25.3" - } - }, "node_modules/@vrtmrz/livesync-commonlib/node_modules/minimatch": { "version": "11.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-11.0.4.tgz", diff --git a/package.json b/package.json index 681ef357..0bcf3773 100644 --- a/package.json +++ b/package.json @@ -211,7 +211,7 @@ "@smithy/types": "^4.14.3", "@smithy/util-retry": "^4.4.5", "@vrtmrz/browser-ui-kit": "0.1.0", - "@vrtmrz/livesync-commonlib": "0.3.2", + "@vrtmrz/livesync-commonlib": "0.4.0", "@vrtmrz/obsidian-plugin-kit": "0.1.7", "@vrtmrz/ui-interactions": "0.1.2", "@vrtmrz/value-store": "0.1.0", diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index ae63c87c..209a3f80 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -9,6 +9,12 @@ * the same change. */ export const liveSyncProvisionalEnglishMessages = { + "Diagnostic summary": "Diagnostic summary", + "Open log": "Open log", + "Detailed P2P diagnostics and Log Pane output are enabled": + "Detailed P2P diagnostics and Log Pane output are enabled", + "Enable detailed P2P diagnostics and Log Pane output": + "Enable detailed P2P diagnostics and Log Pane output", "Enable SyncGuard": "Enable SyncGuard", "Enable device-local SyncGuard protection during synchronisation.": "Enable device-local SyncGuard protection during synchronisation.", @@ -280,6 +286,7 @@ export const liveSyncProvisionalEnglishMessages = { "Apply database revision to Vault": "Apply database revision to Vault", "Mark this revision as the Vault version": "Mark this revision as the Vault version", "Store Vault file as a child of this revision": "Store Vault file as a child of this revision", + "Correct recorded file size": "Correct recorded file size", "Apply logical deletion to Vault": "Apply logical deletion to Vault", "Apply logical deletion ${REVISION} to ${FILE}? The current Vault file will be removed.": "Apply logical deletion ${REVISION} to ${FILE}? The current Vault file will be removed.", diff --git a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte index 41e0e11b..9c4749d4 100644 --- a/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte +++ b/src/features/P2PSync/P2PReplicator/P2POpenReplicationPane.svelte @@ -13,6 +13,7 @@ import { delay, fireAndForget } from "octagonal-wheels/promises"; import P2PServerStatusCard from "./P2PServerStatusCard.svelte"; import { $msg as translateMessage } from "@/common/translation"; + import { observeP2PDiagnosticsProgress, type P2PStatusProjection } from "@/modules/features/P2PStatusLabel"; interface Props { p2p: P2PServiceViews; @@ -27,6 +28,7 @@ let serverInfo = $state(undefined); let syncingPeerId = $state(null); + let peerProgress = $state(new Map()); const logLevel = $derived(showResult ? LOG_LEVEL_NOTICE : LOG_LEVEL_INFO); async function requestServerStatus() { @@ -34,6 +36,9 @@ eventHub.emitEvent(EVENT_REQUEST_STATUS); } onMount(() => { + const unsubscribeProgress = observeP2PDiagnosticsProgress(p2p.diagnostics, (snapshot) => { + peerProgress = snapshot.peers; + }); // ServerStatus const unsubscribe = eventHub.onEvent(EVENT_SERVER_STATUS, (status) => { serverInfo = status; @@ -42,7 +47,10 @@ await delay(100); await requestServerStatus(); }); - return unsubscribe; + return () => { + unsubscribe(); + unsubscribeProgress(); + }; }); async function handleSync(peerId: string) { @@ -109,6 +117,14 @@
{peer.name}
+ {#if peerProgress.has(peer.peerId)} +
+ {peerProgress.get(peer.peerId)?.message} +
+ {/if}
{peer.platform} @@ -176,6 +192,12 @@
+
diff --git a/test/p2p-diagnostics/status-card/main.ts b/test/p2p-diagnostics/status-card/main.ts new file mode 100644 index 00000000..08fc854a --- /dev/null +++ b/test/p2p-diagnostics/status-card/main.ts @@ -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("#complete")!.onclick = () => { + for (const listener of listeners) + listener({ category: "sync", code: "pull-completed", timestamp: timestamp + 1_000 }); +}; diff --git a/test/p2p-diagnostics/status-pane.test.ts b/test/p2p-diagnostics/status-pane.test.ts new file mode 100644 index 00000000..a27ca664 --- /dev/null +++ b/test/p2p-diagnostics/status-pane.test.ts @@ -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(); + } + }); +} diff --git a/test/p2p-diagnostics/status-pane/index.html b/test/p2p-diagnostics/status-pane/index.html new file mode 100644 index 00000000..c363aacb --- /dev/null +++ b/test/p2p-diagnostics/status-pane/index.html @@ -0,0 +1,11 @@ + +P2P status pane layout fixture + +
diff --git a/test/p2p-diagnostics/status-pane/main.ts b/test/p2p-diagnostics/status-pane/main.ts new file mode 100644 index 00000000..de05e236 --- /dev/null +++ b/test/p2p-diagnostics/status-pane/main.ts @@ -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 } }); diff --git a/test/p2p-diagnostics/vite.config.mjs b/test/p2p-diagnostics/vite.config.mjs new file mode 100644 index 00000000..85d1b866 --- /dev/null +++ b/test/p2p-diagnostics/vite.config.mjs @@ -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; diff --git a/updates.md b/updates.md index 93c3d030..255c0bad 100644 --- a/updates.md +++ b/updates.md @@ -12,6 +12,50 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi ## Unreleased +### Fixed + +#### Recovery + +- Files saved while host size information is stale retain the correct content size, preventing an incorrect size record from blocking reflection to other devices. (#1324) +- **Inspect conflicts and file/database differences** reports files whose recorded size differs from their readable content, and offers a guarded **Correct recorded file size** action when that exact content matches the Vault without conflicts. (#1324) + +### Improved + +#### P2P synchronisation + +- P2P status shows Stats with diagnostic details collapsed by default. Expand + 'Diagnostic summary' to see the latest relay, peer, and synchronisation observations. + With Diag enabled, their events also appear in the Log Pane without updating + the editor status or producing notifications. + - Unobserved information is labelled explicitly. Relay rejection messages are + unavailable through the current transport API; an open room does not confirm + that a relay accepted its announcements. + +- Larger peer controls wrap onto additional lines in narrow panes, keeping action labels + readable alongside long device names. + +- P2P progress shows sent (↑) and received (↓) document revision counts, including + Metadata and Chunks. Directional ⏩ labels show comparison progress even when no + transfer is needed, and disappear three seconds after their last count change. + - Counts appear in the P2P panes and editor status. Only P2P also uses them in + the main status bar. Older peers retain a sequence display labelled `seq`. + +- P2P connections include upstream improvements to peer discovery after relay + reconnection, recovery from missed signalling messages, and stale connections + after rejoining or waiting. Connection offers are created on demand to reduce + idle resource use. + - Update every device in the P2P group, including CLI and WebPeer clients, + before resuming P2P synchronisation. The updated transport uses Trystero + 0.26.0 and cannot exchange data with the previous transport. + +### Fixed + +#### Synchronisation + +- New saves record the byte length of the stored content, preventing stale host + file-size information from causing a receiving device to reject the file. (#1324) + - Existing incorrect Metadata needs explicit repair. + ## 1.0.45 5th November, 2026