+deno run --minimum-dependency-age=0 --allow-env https://raw.githubusercontent.com/vrtmrz/obsidian-livesync/main/utils/setup/generate_setup_uri.ts
+```
+
+The generated Setup URI contains the encrypted room, relay, and Vault settings. It deliberately omits the device-specific name. Store the URI and its passphrase separately. After importing it on the first device, continue from the initialisation step above, then generate a fresh Setup URI for an additional device from that working device.
diff --git a/docs/terms.md b/docs/terms.md
index c8809c85..721e2c44 100644
--- a/docs/terms.md
+++ b/docs/terms.md
@@ -74,8 +74,8 @@ All guidelines and conventions listed below are disclosed and maintained solely
- A privacy option that encrypts file paths and folder names on the remote server.
- plug-in
- We use the hyphenated form `plug-in` in user-facing messages and general documentation, while `plugin` may appear in codebase files, configuration settings, or technical contexts.
-- Relay Server (P2P relays)
- - A WebSocket-based coordination server used to establish direct WebRTC peer-to-peer connections. The default relay is provided by the plug-in author.
+- Signalling relay (P2P)
+ - A Nostr-compatible WebSocket relay used for peer discovery and WebRTC connection negotiation. It does not store or transfer Vault contents. The project author operates a public relay as a best-effort convenience, and users can provide another compatible relay.
- Remediation (maxMTimeForReflectEvents)
- A recovery setting that restricts the propagation of changes from the database to local storage, ignoring any file events (such as accidental mass deletions) that occurred after a specified date and time.
- Reset Synchronisation on This Device
@@ -95,7 +95,7 @@ All guidelines and conventions listed below are disclosed and maintained solely
- Sync Mode
- The replication trigger mechanism. Users can select from `On Events` (synchronising on local file changes), `Periodic and Events` (synchronising at fixed intervals as well as on events), or `LiveSync` (continuous, real-time synchronisation).
- TURN Server (WebRTC P2P)
- - A server type (Traversal Using Relays around NAT) used as a fallback to relay traffic when direct WebRTC peer-to-peer connection is blocked by strict NAT or firewalls.
+ - A Traversal Using Relays around NAT server used as an optional fallback to relay encrypted WebRTC traffic when strict NAT or firewall rules block a direct peer connection. It is distinct from the signalling relay.
- Update Thinning (Batch database update)
- An optimisation that groups multiple local file edits together over a short delay before committing them to the local database, reducing the number of database write operations.
- WebRTC P2P (Peer-to-Peer)
diff --git a/docs/tips/p2p-sync-tips.md b/docs/tips/p2p-sync-tips.md
index 6d8075d6..6ce0c9a2 100644
--- a/docs/tips/p2p-sync-tips.md
+++ b/docs/tips/p2p-sync-tips.md
@@ -10,22 +10,55 @@ authors:
# Peer-to-Peer Synchronisation Tips
-For the complete first-device, Setup URI, second-device, and two-way verification procedure, see [Set up peer-to-peer synchronisation](../setup_p2p.md).
+For the first device, Setup URI, additional device, and two-way verification procedure, see [Set up peer-to-peer synchronisation](../setup_p2p.md). For the communication and privacy model, see [How peer-to-peer synchronisation works](../p2p.md).
> [!IMPORTANT]
-> Peer-to-peer synchronisation is a supported opt-in feature. WebRTC connectivity still depends on the networks, relays, and optional TURN servers available to every device, so a working connection cannot be guaranteed in every environment.
+> P2P is a supported opt-in feature, but WebRTC connectivity still depends on the networks available to every device. A direct connection cannot be guaranteed in every environment.
-## Difficulties with Peer-to-Peer Synchronisation
+## A peer does not appear
-It is often the case that peer-to-peer connections do not function correctly, for instance, when using mobile data services.
-In such circumstances, we recommend connecting all devices to a single Virtual Private Network (VPN). It is advisable to select a service, such as Tailscale, which facilitates direct communication between peers wherever possible.
-Should one be in an environment where even Tailscale is unable to connect, or where it cannot be lawfully installed, please continue reading.
+Check discovery before changing any Vault settings:
-## A More Detailed Explanation
+1. Confirm that both devices use the same **Signalling relay URLs**, Group ID, and P2P passphrase.
+2. Confirm that each device has a distinct device name.
+3. Open `P2P Status` on both devices and confirm that each shows `Connected`.
+4. Select `Refresh` after the other device joins.
+5. If the peer remains absent, select `Disconnect`, then `Open connection` on the device which should be advertised again.
-The failure of a Peer-to-Peer connection via WebRTC can be attributed to several factors. These may include an unsuccessful UDP hole-punching attempt, or an intermediary gateway intentionally terminating the connection. Troubleshooting this matter is not a simple undertaking. Furthermore, and rather unfortunately, gateway administrators are typically aware of this type of network behaviour. Whilst a legitimate purpose for such traffic can be cited, such as for web conferencing, this is often insufficient to prevent it from being blocked.
+The signalling relay discovers peers; it does not prove that the networks can carry a WebRTC data connection.
-This situation, however, is the primary reason that our project does not provide a TURN server. Although it is said that a TURN server within WebRTC does not decrypt communications, the project holds the view that the risk of a malicious party impersonating a TURN server must be avoided. Consequently, configuring a TURN server for relay communication is not currently possible through the user interface. Furthermore, there is no official project TURN server, which is to say, one that could be monitored by a third party.
+## A peer appears but synchronisation cannot connect
-We request that you provide your own server, using your own Fully Qualified Domain Name (FQDN), and subsequently enter its details into the advanced settings.
-For testing purposes, Cloudflare's Real-Time TURN Service is exceedingly convenient and offers a generous amount of free data. However, it must be noted that because it is a well-known destination, such traffic is highly conspicuous. There is also a significant possibility that it may be blocked by default. We advise proceeding with caution.
+WebRTC may fail when UDP hole punching is blocked by carrier-grade NAT, a firewall, a VPN policy, or an intermediary gateway.
+
+Try these in order:
+
+1. Put both devices on the same ordinary network and retry.
+2. Remove a VPN temporarily if it blocks peer traffic, or use a trusted VPN such as Tailscale when it provides a reachable path between the devices.
+3. In `P2P Configuration` -> `Advanced Settings`, configure a trusted TURN service.
+
+TURN is a fallback for encrypted WebRTC traffic. It is different from the required signalling relay. The project does not operate an official TURN service. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume.
+
+## A connected peer does not receive later edits
+
+An open signalling connection does not automatically move every change.
+
+- Use `Replicate now` to prove an explicit bidirectional round trip.
+- Enable `Announce changes` on the source device before it dispatches notifications.
+- Enable `Follow changes` for that source on the receiving device before it fetches in response.
+- Use the peer's `More actions` menu only after the manual round trip works.
+
+If the device was asleep, Obsidian was in the background, or the peer disconnected, run an explicit synchronisation after both devices are visible and connected.
+
+## Mobile limitations
+
+Keep Obsidian visible and the device awake during initial transfer, rebuild, or a large finite synchronisation. Wake Lock support is best effort and cannot prevent the operating system from suspending or terminating a background application.
+
+## Collect evidence
+
+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`;
+- each device appeared in `Detected Peers`;
+- a connection request appeared; and
+- a TURN server or VPN was in use.
diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md
index 6946533a..4acae22d 100644
--- a/docs/troubleshooting.md
+++ b/docs/troubleshooting.md
@@ -1,454 +1,156 @@
-# Tips and Troubleshooting
-- [Tips and Troubleshooting](#tips-and-troubleshooting)
- - [Tips](#tips)
- - [CORS avoidance](#cors-avoidance)
- - [CORS configuration with reverse proxy](#cors-configuration-with-reverse-proxy)
- - [Nginx](#nginx)
- - [Nginx and subdirectory](#nginx-and-subdirectory)
- - [Caddy](#caddy)
- - [Caddy and subdirectory](#caddy-and-subdirectory)
- - [Apache](#apache)
- - [Show all setting panes](#show-all-setting-panes)
- - [How to resolve `Tweaks Mismatched of Changed`](#how-to-resolve-tweaks-mismatched-of-changed)
- - [Notable bugs and fixes](#notable-bugs-and-fixes)
- - [Binary files get bigger on iOS](#binary-files-get-bigger-on-ios)
- - [Some setting name has been changed](#some-setting-name-has-been-changed)
- - [Questions and Answers](#questions-and-answers)
- - [How should I share the settings between multiple devices?](#how-should-i-share-the-settings-between-multiple-devices)
- - [What should I enter for the passphrase of Setup-URI?](#what-should-i-enter-for-the-passphrase-of-setup-uri)
- - [Why the settings of Self-hosted LiveSync itself is disabled in default?](#why-the-settings-of-self-hosted-livesync-itself-is-disabled-in-default)
- - [The plug-in says `something went wrong`.](#the-plug-in-says-something-went-wrong)
- - [A large number of files were deleted, and were synchronised!](#a-large-number-of-files-were-deleted-and-were-synchronised)
- - [Why `Use an old adapter for compatibility` is somehow enabled in my vault?](#why-use-an-old-adapter-for-compatibility-is-somehow-enabled-in-my-vault)
- - [ZIP (or any extensions) files were not synchronised. Why?](#zip-or-any-extensions-files-were-not-synchronised-why)
- - [I hope to report the issue, but you said you needs `Report`. How to make it?](#i-hope-to-report-the-issue-but-you-said-you-needs-report-how-to-make-it)
- - [Where can I check the log?](#where-can-i-check-the-log)
- - [Why are the logs volatile and ephemeral?](#why-are-the-logs-volatile-and-ephemeral)
- - [Some network logs are not written into the file.](#some-network-logs-are-not-written-into-the-file)
- - [If a file were deleted or trimmed, the capacity of the database should be reduced, right?](#if-a-file-were-deleted-or-trimmed-the-capacity-of-the-database-should-be-reduced-right)
- - [How to launch the DevTools](#how-to-launch-the-devtools)
- - [On Desktop Devices](#on-desktop-devices)
- - [On Android](#on-android)
- - [On iOS, iPadOS devices](#on-ios-ipados-devices)
- - [How can I use the DevTools?](#how-can-i-use-the-devtools)
- - [Checking the network log](#checking-the-network-log)
- - [Troubleshooting](#troubleshooting)
- - [While using Cloudflare Tunnels, often Obsidian API fallback and `524` error occurs.](#while-using-cloudflare-tunnels-often-obsidian-api-fallback-and-524-error-occurs)
- - [On the mobile device, cannot synchronise on the local network!](#on-the-mobile-device-cannot-synchronise-on-the-local-network)
- - [I think that something bad happening on the vault...](#i-think-that-something-bad-happening-on-the-vault)
- - [Flag Files](#flag-files)
- - [Old tips](#old-tips)
+# Troubleshooting
-
-
-## Tips
-
-### CORS avoidance
-
-If we are unable to configure CORS properly for any reason (for example, if we cannot configure non-administered network devices), we may choose to ignore CORS.
-To use the Obsidian API (also known as the Non-Native API) to bypass CORS, we can enable the toggle ``Use Request API to avoid `inevitable` CORS problem``.
-
-
-
-### CORS configuration with reverse proxy
-
-- IMPORTANT: CouchDB handles CORS by itself. Do not process CORS on the reverse
- proxy.
- - Do not process `Option` requests on the reverse proxy!
- - Make sure `host` and `X-Forwarded-For` headers are forwarded to the CouchDB.
- - If you are using a subdirectory, make sure to handle it properly. More
- detailed information is in the
- [CouchDB documentation](https://docs.couchdb.org/en/stable/best-practices/reverse-proxies.html).
-
-Minimal configurations are as follows:
-
-#### Nginx
-
-```nginx
-location / {
- proxy_pass http://localhost:5984;
- proxy_redirect off;
- proxy_buffering off;
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-}
-```
-
-#### Nginx and subdirectory
-
-```nginx
-location /couchdb {
- rewrite ^ $request_uri;
- rewrite ^/couchdb/(.*) /$1 break;
- proxy_pass http://localhost:5984$uri;
- proxy_redirect off;
- proxy_buffering off;
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-}
-
-location /_session {
- proxy_pass http://localhost:5984/_session;
- proxy_redirect off;
- proxy_buffering off;
- proxy_set_header Host $host;
- proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-}
-```
-
-#### Caddy
-
-```caddyfile
-domain.com {
- reverse_proxy localhost:5984
-}
-```
-
-#### Caddy and subdirectory
-
-```caddyfile
-domain.com {
- reverse_proxy /couchdb/* localhost:5984
- reverse_proxy /_session/* localhost:5984/_session
-}
-```
-
-#### Apache
-
-Sorry, Apache is not recommended for CouchDB. Omit the configuration from here.
-Please refer to the
-[Official documentation](https://docs.couchdb.org/en/stable/best-practices/reverse-proxies.html#reverse-proxying-with-apache-http-server).
-
-### Show all setting panes
-
-Full pane is not shown by default. To show all panes, please toggle all in
-`🧙♂️ Wizard` -> `Enable extra and advanced features`.
-
-For your information, the all panes are as follows:
-
-
-### How to resolve `Tweaks Mismatched of Changed`
-
-(Since v0.23.17)
-
-If you have changed some configurations or tweaks which should be unified
-between the devices, you will be asked how to reflect (or not) other devices at
-the next synchronisation. It also occurs on the device itself, where changes are
-made, to prevent unexpected configuration changes from unwanted propagation.\
-(We may thank this behaviour if we have synchronised or backed up and restored
-Self-hosted LiveSync. At least, for me so).
-
-Following dialogue will be shown: 
-
-- If we want to propagate the setting of the device, we should choose
- `Update with mine`.
-- On other devices, we should choose `Use configured` to accept and use the
- configured configuration.
-- `Dismiss` can postpone a decision. However, we cannot synchronise until we
- have decided.
-
-Rest assured that in most cases we can choose `Use configured`. (Unless you are
-certain that you have not changed the configuration).
-
-If we see it for the first time, it reflects the settings of the device that has
-been synchronised with the remote for the first time since the upgrade.
-Probably, we can accept that.
-
-
-
-## Notable bugs and fixes
-
-### Binary files get bigger on iOS
-
-- Reported at: v0.20.x
-- Fixed at: v0.21.2 (Fixed but not reviewed)
-- Required action: larger files will not be fixed automatically, please perform
- `Verify and repair all files`. If our local database and storage are not
- matched, we will be asked to apply which one.
-
-### Some setting name has been changed
-
-- Fixed at: v0.22.6
-
-| Previous name | New name |
-| ---------------------------- | ---------------------------------------- |
-| Open setup URI | Use the copied setup URI |
-| Copy setup URI | Copy current settings as a new setup URI |
-| Setup Wizard | Minimal Setup |
-| Check database configuration | Check and Fix database configuration |
-
-## Questions and Answers
-
-### How should I share the settings between multiple devices?
-
-- Device setup:
- - Using `Setup URI` is the most straightforward way.
-- Setting changes during use:
- - Use `Sync settings via Markdown files` on the `🔄️ Sync settings` pane.
-
-### What should I enter for the passphrase of Setup-URI?
-
-- Anything you like is OK. However, the recommendation is as follows:
- - Include the vault (group) information.
- - Include the date of operation.
- - Anything random for your security.
- - For example, `MyVault-20240901-r4nd0mStr1ng`.
-- Why?
- - The Setup-URI is encoded; that means it cannot indicate the actual settings. Hence, if you use the same passphrase for multiple vaults, you may accidentally mix up vaults.
-
-### Why the settings of Self-hosted LiveSync itself is disabled in default?
-
-Basically, if we configure all `additionalSuffixOfDatabaseName` the same, we can synchronise this file between multiple devices.
-(`additionalSuffixOfDatabaseName` should be unique in each device, not in the synchronised vaults).
-However, if we synchronise the settings of Self-hosted LiveSync itself, we may encounter some unexpected behaviours.
-For example, if a setting that 'let Self-hosted LiveSync setting be excluded' is synced, it is very unlikely that things will recover automatically after this, and there is little chance we will even notice this. Even if we change our minds and change the settings back on other devices. It could get even worse if incompatible changes are automatically reflected; everything will break.
-
-### The plug-in says `something went wrong`.
-
-There are many cases where this is really unclear. One possibility is that the chunk fetch did not go well.
-
-1. Restarting Obsidian sometimes helps (fetch-order problem).
-2. If actually there are no chunks, please perform `Recreate missing chunks for all files` on the `🧰 Hatch` pane at the other devices. And synchronise again. (also restart Obsidian may effect).
-3. If the problem persists, please perform `Verify and repair all files` on the `🧰 Hatch` pane. If our local database and storage are not matched, we will be asked to apply which one.
-
-### A large number of files were deleted, and were synchronised!
-
-1. Backup everything important.
- - Your local vault.
- - Your CouchDB database (this can be done by replicating to another database).
-2. Prepare the empty vault
-3. Place `redflag.md` at the top of the vault.
-4. Apply the settings **BUT DO NOT PROCEED TO RESTORE YET**.
- - You can use `Setup URI`, QR Code, or manually apply the settings.
-5. Set `Maximum file modification time for reflected file events` in `Remediation` on the `🩹 Patches` pane.
- - If you know when the files were deleted, set the time a bit before that.
- - If not, bisecting may help us.
-6. Delete `redflag.md`.
-7. Perform `Reset Synchronisation on This Device` on the `🎛️ Maintenance` pane.
-
-This mode is very fragile. Please be careful.
-
-### Why `Use an old adapter for compatibility` is somehow enabled in my vault?
-
-Because you are a compassionate and experienced user. Before v0.17.16, we used
-an old adapter for the local database. At that time, current default adapter has
-not been stable. The new adapter has better performance and has a new feature
-like purging. Therefore, we should use new adapters and current default is so.
-
-However, when switching from an old adapter to a new adapter, some converting or
-local database rebuilding is required, and it takes some time. It was a long
-time ago now, but we once inconvenienced everyone in a hurry when we changed the
-format of our database. For these reasons, this toggle is automatically on if we
-have upgraded from vault which using an old adapter.
-
-When you overwrite server data with this device's files or reset synchronisation on this device again, you will be asked to
-switch this.
-
-Therefore, experienced users (especially those stable enough not to have to
-overwrite server data) may have this toggle enabled in their Vault. Please
-disable it when you have enough time.
-
-### ZIP (or any extensions) files were not synchronised. Why?
-
-It depends on Obsidian detects. May toggling `Detect all extensions` of
-`File and links` (setting of Obsidian) will help us.
-
-### I hope to report the issue, but you said you needs `Report`. How to make it?
-
-We can copy the report to the clipboard, by performing
-`Generate full report for opening the issue with debug info` command!
-
-### Where can I check the log?
-
-We can launch the log pane by `Show log` on the command palette. And if you have
-troubled something, please enable the `Verbose Log` on the `General Setting`
-pane.
-`Generate full report for opening the issue with debug info` command also contains
-the recent 1000 log lines, which is very helpful for debugging. Full-report is
-already set to the verbose level, so it contains all the logs without enabling the
-`Verbose Log` toggle.
-
-Let me note that please be sure to remove any sensitive information before sharing the report.
-
-However, the logs would not be kept so long and cleared when restarted. If you
-want to check the logs, please enable `Write logs into the file` temporarily.
-
-
+Start with the symptom which is visible now. Do not reset a database, change transport, or enable P2P merely to see whether the problem disappears.
> [!IMPORTANT]
->
-> - Writing logs into the file will impact the performance.
-> - Please make sure that you have erased all your confidential information
-> before reporting issue.
+> If Obsidian will not start, do not give up. Close it, create `redflag.md` at the Vault root with the operating system's file manager, then follow [Recovery and flag files](recovery.md). This is the supported route for intervening before ordinary LiveSync start-up work.
-### Why are the logs volatile and ephemeral?
+Before changing settings:
-To avoid unexpected exposure to our confidential things.
+1. Back up the affected Vaults and, where possible, the remote database or bucket.
+2. Stop editing on other devices.
+3. Confirm that every participating device uses the intended plug-in version.
+4. Identify whether the active main remote is CouchDB, Object Storage, or P2P.
+5. Open `Show log` and note the first error, rather than only the final summary.
-### Some network logs are not written into the file.
+For a report, run `Generate full report for opening the issue with debug info`, remove credentials and private server details, and include the steps which caused the symptom.
-Especially the CORS error will be reported as a general error to the plug-in for
-security reasons. So we cannot detect and log it. We are only able to
-investigate them by [Checking the network log](#checking-the-network-log).
+## CouchDB does not connect
-### If a file were deleted or trimmed, the capacity of the database should be reduced, right?
+Check the connection in this order:
-No, even though if files were deleted, chunks were not deleted. Self-hosted
-LiveSync splits the files into multiple chunks and transfers only newly created.
-This behaviour enables us to less traffic. And, the chunks will be shared
-between the files to reduce the total usage of the database.
+1. Confirm that the URL is complete and points to the intended server.
+2. On mobile, use HTTPS with a certificate trusted by the operating system. Plain HTTP and self-signed certificates are not supported.
+3. Confirm the username, password, database name, and any custom headers.
+4. Confirm that the server responds outside the plug-in and that the database exists on additional devices.
+5. Use the setup dialogue's connection test.
+6. If basic access works, run **Check server requirements**. Its initial check is read-only. Each offered server change requires separate confirmation.
-And one more thing, we can handle the conflicts on any device even though it has
-happened on other devices. This means that conflicts will happen in the past,
-after the time we have synchronised. Hence we cannot collect and delete the
-unused chunks even though if we are not currently referenced.
+Configure CouchDB CORS first. Reverse-proxy examples belong in [Set up your own CouchDB server](setup_own_server.md), alongside the rest of the server configuration.
-To shrink the database size, `Overwrite Server Data with This Device's Files` is the only reliable and effective way.
-But do not worry, if we have synchronised well. We have the actual and real
-files. Only it takes a bit of time and traffic.
+`Use Internal API` is a compatibility workaround for a trusted server. It sends the configured credentials through Obsidian's internal request API. Enable it only after checking the destination, and do not treat a fallback through that API as proof that the server or proxy is correctly configured.
-### How to launch the DevTools
+A Cloudflare `524` response means that Cloudflare timed out while waiting for the origin. The response may also lack the CORS headers which would have been present on an ordinary CouchDB response. Correct the long-running server or proxy request first. The advanced CouchDB option `Use timeouts instead of heartbeats` may help only when the underlying operation is otherwise healthy.
-#### On Desktop Devices
+For JWT-specific setup and key-format errors, see [JWT Authentication on CouchDB](tips/jwt-on-couchdb.md).
-We can launch the DevTools by pressing `ctrl`+`shift`+`i` (`Command`+`shift`+`i` on Mac).
+## CouchDB was working but synchronisation stopped
-#### On Android
+Do not switch to P2P or reset the database as the first response. Check:
-Please refer to [Remote debug Android devices](https://developer.chrome.com/docs/devtools/remote-debugging/).
-Once the DevTools have been launched, everything operates the same as on a PC.
+1. the active remote profile and connection state;
+2. the plug-in version on every device;
+3. the CouchDB response and server logs;
+4. pending LiveSync progress indicators;
+5. `Check server requirements`; and
+6. the LiveSync log and full report.
-#### On iOS, iPadOS devices
+If the remote is healthy but one device's local database is not, use [Reset Synchronisation on This Device](recovery.md#reset-synchronisation-on-this-device) only after backing up unsynchronised local files.
-If we have a Mac, we can inspect from Safari on the Mac. Please refer to [Inspecting iOS and iPadOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios).
+## Files are missing or excluded
-### How can I use the DevTools?
+Check Obsidian's `Detect all file extensions`, LiveSync selectors, ignore files, file-size limits, modification-time limits, and Hidden File Sync rules. A filtered file is different from a file which reached the database but could not be reconstructed from its chunks.
-#### Checking the network log
+If the log reports missing chunks or a size mismatch:
+
+1. restart Obsidian once to rule out an interrupted fetch;
+2. on a device which has the correct file, run `Recreate missing chunks for all files`, then synchronise; and
+3. if the mismatch remains, run `Verify and repair all files` from `Hatch` and review which copy is authoritative.
+
+## A configuration-mismatch dialogue blocks synchronisation
+
+Some settings must match across devices. LiveSync pauses synchronisation when the local and remote values differ rather than propagating an unexpected change silently.
+
+- Choose `Update with mine` only when this device's setting is the intended shared value.
+- Choose `Use configured` to accept the value already stored for the synchronisation group.
+- `Dismiss` postpones the decision, but synchronisation remains paused until it is resolved.
+
+
+
+Historic defect notices and renamed controls are retained in the [0.25 release history](releases/0.25.md) and [legacy release history](releases/legacy.md), rather than in the current troubleshooting path.
+
+## Setup and settings questions
+
+### Share a configuration with another device
+
+Generate an encrypted Setup URI from a working device. This preserves the intended remote profiles and selections while allowing the additional device to keep its own device-specific name. Store the URI and its passphrase separately.
+
+For deliberate setting changes during normal use, use `Sync settings via Markdown files` under `Sync settings`.
+
+### Choose a Setup URI passphrase
+
+Use a strong passphrase which is distinct from the Vault encryption passphrase. Record enough context outside the encrypted URI to identify the intended Vault and date, but do not rely on a reused human-readable pattern alone.
+
+### Why synchronising LiveSync's own settings is disabled by default
+
+An automatically propagated transport, database, or exclusion setting can disable the mechanism needed to reverse it. LiveSync therefore keeps its own settings out of Customisation Sync by default. Enable that advanced behaviour only with an independent recovery path and device-specific database suffixes.
+
+### The plug-in reports that something went wrong
+
+Use the first specific error in `Show log` to choose the relevant section. When it names chunks or a size mismatch, follow [Files are missing or excluded](#files-are-missing-or-excluded). Do not rebuild solely from the generic final message.
+
+### A large deletion propagated
+
+Stop every device, preserve the available copies, and follow [Recovery and flag files](recovery.md). If the deletion time is known, `Maximum file modification time for reflected file events` under `Remediation` can limit which remote events are applied while recovering into a separate, backed-up Vault. Treat that as a forensic recovery constraint, not as an ordinary synchronisation setting.
+
+### An old database adapter is still selected
+
+Very old Vaults may retain the compatibility adapter until a deliberate local database migration or reset. Do not toggle it merely to troubleshoot an unrelated current failure. The history and migration notes are in the [legacy release history](releases/legacy.md).
+
+### ZIP or another extension is not synchronised
+
+Enable Obsidian's `Detect all file extensions`, then check LiveSync selectors, ignore rules, and size limits as described in [Files are missing or excluded](#files-are-missing-or-excluded).
+
+## Collect a report
+
+Run `Generate full report for opening the issue with debug info` to copy the current settings summary and recent verbose log lines. Remove credentials, remote URLs, Vault names, file contents, and other private information before sharing it.
+
+Use `Show log` for live inspection. Logs are intentionally kept in memory for a limited time to reduce accidental disclosure. Enable `Write logs into the file` only while reproducing a problem, then disable it and remove the file after review because persistent logging affects performance and may contain private data.
+
+
+
+Browser security errors, particularly CORS failures, may reach the plug-in only as a general network error. Use the network inspector when the ordinary log cannot show the rejected response.
+
+## The database remains large after files are deleted
+
+LiveSync stores file metadata, chunks, revision history, conflicts, deletions, and tombstones. Deleting or shortening a file therefore does not immediately remove every object which once represented it.
+
+Garbage Collection can remove unreferenced chunks, but it is appropriate only when the Vault and local database are healthy and all relevant devices have synchronised. Tombstones and retained revisions are not free, so Garbage Collection does not guarantee a minimal database.
+
+`Overwrite Server Data with This Device's Files` is a separate rebuild operation and is the more certain way to reconstruct a central remote from a chosen authoritative Vault. It is also destructive and may discard changes which exist only on another device. Review [Recovery and flag files](recovery.md#garbage-collection-is-not-rebuild) before choosing between them.
+
+## Inspect a network failure
+
+### Desktop
+
+Open Developer Tools with `Ctrl`+`Shift`+`I`, or `Command`+`Option`+`I` on macOS.
+
+### Android
+
+Follow Chrome's [Remote debug Android devices](https://developer.chrome.com/docs/devtools/remote-debugging/) guide.
+
+### iOS and iPadOS
+
+Use Safari on a Mac and follow Apple's [Inspecting iOS and iPadOS](https://developer.apple.com/documentation/safari-developer-tools/inspecting-ios) guide.
+
+### Network evidence
1. Open the network pane.
-2. Find the requests marked in red.\
+2. Reproduce the failure and select the request marked in red.

-3. Capture the `Headers`, `Payload`, and, `Response`. **Please be sure to keep
- important information confidential**. If the `Response` contains secrets, you
- can omitted that. Note: Headers contains a some credentials. **The path of
- the request URL, Remote Address, authority, and authorization must be
- concealed.**\
+3. Record the status, timing, and a sanitised version of the headers, payload, and response.
+4. Remove the request path, remote address, authority, authorisation, cookies, credentials, and response secrets before sharing.
+

-## Troubleshooting
+## 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.
-### While using Cloudflare Tunnels, often Obsidian API fallback and `524` error occurs.
+## Obsidian or LiveSync remains suspended
-A `524` error occurs when the request to the server is not completed within a
-`specified time`. This is a timeout error from Cloudflare. From the reported
-issue, it seems to be 100 seconds. (#627).
+Follow [Recovery and flag files](recovery.md). A `redflag.md` emergency stop remains active until it is removed outside Obsidian. Fetch and rebuild flags have different, potentially destructive meanings; do not create them merely to clear a warning.
-Therefore, this error returns from Cloudflare, not from the server. Hence, the
-result contains no CORS field. It means that this response makes the Obsidian
-API fallback.
+## Further technical context
-However, even if the Obsidian API fallback occurs, the request is still not
-completed within the `specified time`, 100 seconds.
-
-To solve this issue, we need to configure the timeout settings.
-
-Please enable the toggle in `💪 Power users` -> `CouchDB Connection Tweak` ->
-`Use timeouts instead of heartbeats`.
-
-### On the mobile device, cannot synchronise on the local network!
-
-Obsidian mobile is not able to connect to the non-secure end-point, such as
-starting with `http://`. Make sure your URI of CouchDB. Also not able to use a
-self-signed certificate.
-
-### I think that something bad happening on the vault...
-
-Place the [flag file](#flag-files) on top of the vault, and restart Obsidian. The most simple
-way is to create a new note and rename it to `redflag`. Of course, we can put it
-without Obsidian.
-
-For example, if there is `redflag.md`, Self-hosted LiveSync suspends all database and storage
-processes.
-
-### Scram State and Flag Files (SCRAM Warning Loop)
-
-The plug-in uses a **Scram state** (emergency suspension of all synchronisation processes) to prevent database corruption when severe errors or conflicts are detected. This state is often triggered or persisted by **flag files** placed at the root of the vault.
-
-If you encounter a warning saying **"Scram detected, all sync operations are suspended per SCRAM"** or get caught in an infinite loop where the warning persists even after clicking "Resume", it is likely due to a flag file in your vault.
-
-#### Flag Files
-
-A flag file is a simple Markdown file located at the root of your vault. Its very existence is significant; it may be left blank or contain any text. These files are used so they can be easily placed or deleted from outside Obsidian (e.g., when Obsidian fails to launch).
-
-| Filename | Human-Friendly Name | Description |
-| ------------- | ------------------- | --------------------------------------------------------------------------------------- |
-| `redflag.md` | - | Suspends all processes (activates Scram). |
-| `redflag2.md` | `flag_rebuild.md` | Suspends all processes, and overwrites server data with this device's files. |
-| `redflag3.md` | `flag_fetch.md` | Suspends all processes, discards the local database, and resets synchronisation on this device. |
-
-When resetting synchronisation on this device or overwriting server data, restarting Obsidian is performed once for safety reasons. At that time, Self-hosted LiveSync uses these files to determine whether the process should be carried out. (This mechanism is especially useful on mobile devices to force cancellation if the database rebuilding fails). These files are not subject to synchronisation.
-
-Flag-file recovery is handled before the compatibility-review dialogue. `redflag.md` keeps start-up stopped, while a cancelled recovery or one which schedules a restart also prevents a second dialogue from competing with it in that process. When fetch-all or rebuild-all completes and start-up continues, Self-hosted LiveSync can still ask for compatibility review before ordinary synchronisation resumes. Completing a recovery does not automatically acknowledge a database or settings version; use the explicit resume action after reviewing the explanation.
-
-#### How to Resolve the Scram Loop
-
-If you cannot disable Scram, please follow these steps:
-1. Close Obsidian completely.
-2. Open your system's file manager and check the root directory of your vault.
-3. Locate and delete any flag files (such as `redflag.md`, `redflag2.md`, or `redflag3.md`).
-4. Launch Obsidian.
-5. Go to the settings dialogue -> **Hatch** -> **Scram Switches**, and manually toggle **Suspend file watching** and **Suspend database reflecting** to `false` (disabled) if they have not been reset automatically.
-
-> [!TIP]
-> This is the reason why flag files are standard `.md` files: it allows you to manage them externally. On mobile devices, you can use system file manager applications (such as the native **Files** app on iOS/iPadOS or **Files by Google** on Android) to find and delete these files to resolve a lock, or conversely, create/place a new `redflag.md` file (or directory) at the root of your vault to force-suspend synchronisation and stop Obsidian's boot-up sequence if you need to fix a database issue.
-
-### JWT Authentication Errors
-
-#### DataError when configuring JWT authentication
-
-If you encounter a `DataError:` with no additional information in the logs when configuring JWT authentication, this usually indicates a private key formatting issue.
-
-Self-hosted LiveSync requires the private key (for ES256/ES512 algorithms) to be in the **PKCS#8 PEM** format. Standard SEC1 EC private keys (which begin with `-----BEGIN EC PRIVATE KEY-----`) will trigger this error.
-
-To resolve this, convert your private key to PKCS#8 format using the following `openssl` command:
-```bash
-openssl pkcs8 -topk8 -inform PEM -nocrypt -in private.key -out pkcs8.key
-```
-Then paste the contents of `pkcs8.key` (which begins with `-----BEGIN PRIVATE KEY-----`) into the JWT Key field.
-
-### Old tips
-
-- Rarely, a file in the database could be corrupted. The plug-in will not write
- to local storage when a file looks corrupted. If a local version of the file
- is on your device, the corruption could be fixed by editing the local file and
- synchronising it. But if the file does not exist on any of your devices, then
- it can not be rescued. In this case, you can delete these items from the
- settings dialogue.
-- To stop the boot-up sequence (eg. for fixing problems on databases), you can
- put a `redflag.md` file (or directory) at the root of your vault. Tip for iOS:
- a redflag directory can be created at the root of the vault using the File
- application.
-- Also, with `redflag2.md` placed, we can automatically overwrite server data with this device's files during the boot-up sequence. With `redflag3.md`, we
- can discard only the local database and reset synchronisation on this device.
-- Q: The database is growing, how can I shrink it down? A: each of the docs is
- saved with their past 100 revisions for detecting and resolving conflicts.
- Picturing that one device has been offline for a while, and comes online
- again. The device has to compare its notes with the remotely saved ones. If
- there exists a historic revision in which the note used to be identical, it
- could be updated safely (like git fast-forward). Even if that is not in
- revision histories, we only have to check the differences after the revision
- that both devices commonly have. This is like git's conflict-resolving method.
- So, We have to make the database again like an enlarged git repo if you want
- to solve the root of the problem.
-- And more technical Information is in the [Technical Information](tech_info.md)
-- If you want to synchronise files without obsidian, you can use
- [filesystem-livesync](https://github.com/vrtmrz/filesystem-livesync).
-- WebClipper is also available on Chrome Web
- Store:[obsidian-livesync-webclip](https://chrome.google.com/webstore/detail/obsidian-livesync-webclip/jfpaflmpckblieefkegjncjoceapakdf)
-
-Repo is here:
-[obsidian-livesync-webclip](https://github.com/vrtmrz/obsidian-livesync-webclip).
-(Docs are a work in progress.)
+See [Technical Information](tech_info.md) for database and synchronisation internals. Current behaviour belongs in this guide; older defect-specific instructions remain in the release histories.
diff --git a/images/p2p-setup/guide-p2p-setup-connection-request-1.png b/images/p2p-setup/guide-p2p-setup-connection-request-1.png
index 5144d7c9..26c638f7 100644
Binary files a/images/p2p-setup/guide-p2p-setup-connection-request-1.png and b/images/p2p-setup/guide-p2p-setup-connection-request-1.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-connection-request-2.png b/images/p2p-setup/guide-p2p-setup-connection-request-2.png
index ce96df18..a5da0e9f 100644
Binary files a/images/p2p-setup/guide-p2p-setup-connection-request-2.png and b/images/p2p-setup/guide-p2p-setup-connection-request-2.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png b/images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png
index 4798828f..57acd4e3 100644
Binary files a/images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png and b/images/p2p-setup/guide-p2p-setup-copy-setup-uri-result.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-first-device-connected.png b/images/p2p-setup/guide-p2p-setup-first-device-connected.png
index 19bdca4f..72dea764 100644
Binary files a/images/p2p-setup/guide-p2p-setup-first-device-connected.png and b/images/p2p-setup/guide-p2p-setup-first-device-connected.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-first-setup-uri.png b/images/p2p-setup/guide-p2p-setup-first-setup-uri.png
index 31bc7d23..c57cddf9 100644
Binary files a/images/p2p-setup/guide-p2p-setup-first-setup-uri.png and b/images/p2p-setup/guide-p2p-setup-first-setup-uri.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-peer-actions-menu.png b/images/p2p-setup/guide-p2p-setup-peer-actions-menu.png
new file mode 100644
index 00000000..ffc3392f
Binary files /dev/null and b/images/p2p-setup/guide-p2p-setup-peer-actions-menu.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-retrieval-method.png b/images/p2p-setup/guide-p2p-setup-retrieval-method.png
index e3d331eb..b3b8f520 100644
Binary files a/images/p2p-setup/guide-p2p-setup-retrieval-method.png and b/images/p2p-setup/guide-p2p-setup-retrieval-method.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-second-fetch.png b/images/p2p-setup/guide-p2p-setup-second-fetch.png
index 84c7d411..2c65ca95 100644
Binary files a/images/p2p-setup/guide-p2p-setup-second-fetch.png and b/images/p2p-setup/guide-p2p-setup-second-fetch.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-second-setup-uri.png b/images/p2p-setup/guide-p2p-setup-second-setup-uri.png
index f37250c9..c4c37951 100644
Binary files a/images/p2p-setup/guide-p2p-setup-second-setup-uri.png and b/images/p2p-setup/guide-p2p-setup-second-setup-uri.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-second-to-first.png b/images/p2p-setup/guide-p2p-setup-second-to-first.png
index 3a3b64c9..3baab2b7 100644
Binary files a/images/p2p-setup/guide-p2p-setup-second-to-first.png and b/images/p2p-setup/guide-p2p-setup-second-to-first.png differ
diff --git a/images/p2p-setup/guide-p2p-setup-select-first-device.png b/images/p2p-setup/guide-p2p-setup-select-first-device.png
index 0165f9de..c7f39d42 100644
Binary files a/images/p2p-setup/guide-p2p-setup-select-first-device.png and b/images/p2p-setup/guide-p2p-setup-select-first-device.png differ
diff --git a/images/p2p-setup/p2p-status-pane-mobile.png b/images/p2p-setup/p2p-status-pane-mobile.png
new file mode 100644
index 00000000..961b8485
Binary files /dev/null and b/images/p2p-setup/p2p-status-pane-mobile.png differ
diff --git a/images/p2p-setup/p2p-status-pane.png b/images/p2p-setup/p2p-status-pane.png
new file mode 100644
index 00000000..b0cfac88
Binary files /dev/null and b/images/p2p-setup/p2p-status-pane.png differ
diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts
index 2ec64b67..5d031336 100644
--- a/src/common/messages/LiveSyncProvisionalMessages.ts
+++ b/src/common/messages/LiveSyncProvisionalMessages.ts
@@ -7,6 +7,60 @@
* remove it from this map in the same change.
*/
export const liveSyncProvisionalEnglishMessages = {
+ "This first setup has several short steps because it confirms encryption, the connection method, and which device provides the initial data. Once it is complete, additional devices can reuse a Setup URI.":
+ "This first setup has several short steps because it confirms encryption, the connection method, and which device provides the initial data. Once it is complete, additional devices can reuse a Setup URI.",
+ "Setup Complete: Preparing to Fetch from Another Device":
+ "Setup Complete: Preparing to Fetch from Another Device",
+ "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device.":
+ "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device.",
+ "After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data.":
+ "After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data.",
+ "Restart this device, then choose the source device when P2P Rebuild opens.":
+ "Restart this device, then choose the source device when P2P Rebuild opens.",
+ "Restart and Select Source Device": "Restart and Select Source Device",
+ "P2P Status pane": "P2P Status pane",
+ "No central data-storage server is required, but a signalling relay is required for peer discovery. Both devices must be online at the same time. Vault data travels through the encrypted P2P connection, not through the signalling relay. Some features may be limited.":
+ "No central data-storage server is required, but a signalling relay is required for peer discovery. Both devices must be online at the same time. Vault data travels through the encrypted P2P connection, not through the signalling relay. Some features may be limited.",
+ "P2P requires no central data-storage server, but it still uses a signalling relay for peer discovery.":
+ "P2P requires no central data-storage server, but it still uses a signalling relay for peer discovery.",
+ "Signalling relay URLs": "Signalling relay URLs",
+ "Peer discovery uses Nostr-compatible signalling relays.":
+ "Peer discovery uses Nostr-compatible signalling relays.",
+ "Use the project's public signalling relay": "Use the project's public signalling relay",
+ "The project's public signalling relay is a best-effort convenience operated by the project author. It does not store Vault contents, but signalling metadata may be visible to the relay. Availability and log retention are not guaranteed. You can replace it with your own Nostr-compatible relay.":
+ "The project's public signalling relay is a best-effort convenience operated by the project author. It does not store Vault contents, but signalling metadata may be visible to the relay. Availability and log retention are not guaranteed. You can replace it with your own Nostr-compatible relay.",
+ "Learn more about P2P connections": "Learn more about P2P connections",
+ "Learn more about signalling and TURN": "Learn more about signalling and TURN",
+ "TURN relays the encrypted WebRTC connection only when a direct path cannot be established. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume. Use a provider you trust.":
+ "TURN relays the encrypted WebRTC connection only when a direct path cannot be established. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume. Use a provider you trust.",
+ "Announce changes": "Announce changes",
+ "Announce changes automatically after connecting": "Announce changes automatically after connecting",
+ "When enabled, this device notifies connected peers after a local change. The notification contains no Vault data; a peer which follows this device then fetches the change through the encrypted P2P connection.":
+ "When enabled, this device notifies connected peers after a local change. The notification contains no Vault data; a peer which follows this device then fetches the change through the encrypted P2P connection.",
+ "Stop announcing changes": "Stop announcing changes",
+ "Start announcing changes": "Start announcing changes",
+ "Follow changes": "Follow changes",
+ "Stop following changes from this device": "Stop following changes from this device",
+ "Follow changes from this device": "Follow changes from this device",
+ "Synchronise when this device connects": "Synchronise when this device connects",
+ "Follow whenever this device connects": "Follow whenever this device connects",
+ "Include in the P2P synchronisation command": "Include in the P2P synchronisation command",
+ "More actions for ${DEVICE}": "More actions for ${DEVICE}",
+ "Create or connect to database and continue": "Create or connect to database and continue",
+ "Connect to existing database and continue": "Connect to existing database and continue",
+ "Test connection and save": "Test connection and save",
+ "Save without connecting": "Save without connecting",
+ "Enter a complete HTTP or HTTPS URL.": "Enter a complete HTTP or HTTPS URL.",
+ "CouchDB validates the database name when you connect. The name must not be empty.":
+ "CouchDB validates the database name when you connect. The name must not be empty.",
+ "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected.":
+ "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected.",
+ "This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required.":
+ "This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required.",
+ "Check server requirements": "Check server requirements",
+ "Change CouchDB server setting": "Change CouchDB server setting",
+ "Change CouchDB server setting '${SETTING}' to '${VALUE}'?":
+ "Change CouchDB server setting '${SETTING}' to '${VALUE}'?",
"This file has unresolved conflicts.": "This file has unresolved conflicts.",
"This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.":
"This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.",
diff --git a/src/common/translation.unit.spec.ts b/src/common/translation.unit.spec.ts
index da736044..45fb4719 100644
--- a/src/common/translation.unit.spec.ts
+++ b/src/common/translation.unit.spec.ts
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it } from "vitest";
import { englishMessageTranslator } from "@vrtmrz/livesync-commonlib/context";
import { $msg, setLang, translateLiveSyncMessage } from "@/common/translation";
import { SUPPORTED_I18N_LANGS } from "@/common/rosetta";
+import { liveSyncProvisionalEnglishMessages } from "@/common/messages/LiveSyncProvisionalMessages";
describe("LiveSync-owned translation catalogue", () => {
afterEach(() => setLang("def"));
@@ -28,6 +29,7 @@ describe("LiveSync-owned translation catalogue", () => {
it("uses LiveSync-owned provisional English without extending Commonlib's message contract", () => {
expect($msg("This file has unresolved conflicts.")).toBe("This file has unresolved conflicts.");
+ expect($msg("More actions for ${DEVICE}", { DEVICE: "phone" })).toBe("More actions for phone");
expect(
$msg("This file has ${COUNT} unresolved versions. They will be reviewed one pair at a time.", {
COUNT: "3",
@@ -37,4 +39,19 @@ describe("LiveSync-owned translation catalogue", () => {
"This file has unresolved conflicts."
);
});
+
+ it("keeps the additional-device P2P Fetch explanation in the LiveSync-owned provisional catalogue", () => {
+ expect(liveSyncProvisionalEnglishMessages).toMatchObject({
+ "Setup Complete: Preparing to Fetch from Another Device":
+ "Setup Complete: Preparing to Fetch from Another Device",
+ "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device.":
+ "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device.",
+ "After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data.":
+ "After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data.",
+ "Restart this device, then choose the source device when P2P Rebuild opens.":
+ "Restart this device, then choose the source device when P2P Rebuild opens.",
+ "Restart and Select Source Device": "Restart and Select Source Device",
+ "P2P Status pane": "P2P Status pane",
+ });
+ });
});
diff --git a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts
index 13d51343..86ad37e0 100644
--- a/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts
+++ b/src/features/P2PSync/P2PReplicator/P2PReplicationUI.unit.spec.ts
@@ -163,4 +163,16 @@ describe("createOpenRebuildUI", () => {
expect(replicator.replicateFrom).toHaveBeenCalledWith("peer-a", true, true);
expect(replicator.clearOnSetup).toHaveBeenCalledOnce();
});
+
+ it("does not complete Fetch when the rebuild dialogue closes without selecting a peer", async () => {
+ const replicator = createReplicator();
+ const session = createOpenRebuildUI({} as any)(replicator)(true);
+ const modal = modalState.instances[0];
+
+ modal.onClosed?.();
+
+ await expect(session).resolves.toBe(false);
+ expect(replicator.replicateFrom).not.toHaveBeenCalled();
+ expect(replicator.setOnSetup).not.toHaveBeenCalled();
+ });
});
diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte
index 71e21584..5f500f70 100644
--- a/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte
+++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusCard.svelte
@@ -13,6 +13,7 @@
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";
interface Props {
getLiveSyncReplicator: () => LiveSyncTrysteroReplicator;
@@ -134,15 +135,16 @@
{#if showBroadcastToggle}
-
diff --git a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte
index 50f53afb..02644515 100644
--- a/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte
+++ b/src/features/P2PSync/P2PReplicator/P2PServerStatusPane.svelte
@@ -24,6 +24,13 @@
import { extractP2PRoomSuffix } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import { SetupManager } from "@/modules/features/SetupManager";
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
+ import { Menu } from "@/deps";
+ import { $msg as translateMessage } from "@/common/translation";
+ import {
+ hasExactP2PPeer,
+ togglePersistedP2PPeer,
+ type PersistedP2PPeerSetting,
+ } from "./p2pPeerSettings";
interface Props {
getLiveSyncReplicator: () => LiveSyncTrysteroReplicator;
@@ -40,7 +47,6 @@
// 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 syncOnReplicationSetting = $state(initialSettings?.P2P_SyncOnReplication ?? "");
type P2PRemoteOption = {
id: string;
name: string;
@@ -50,21 +56,7 @@
let selectedP2PRemoteConfigurationId = $state(initialSettings?.P2P_ActiveRemoteConfigurationId ?? "");
let selectingP2PRemote = $state(false);
- function addToList(item: string, list: string): string {
- const items = list
- .split(",")
- .map((e) => e.trim())
- .filter((e) => e);
- if (!items.includes(item)) items.push(item);
- return items.join(",");
- }
- function removeFromList(item: string, list: string): string {
- return list
- .split(",")
- .map((e) => e.trim())
- .filter((e) => e && e !== item)
- .join(",");
- }
+ let peerMenu: Menu | undefined;
function markCommunicating(peerId: string) {
const expiry = Date.now() + COMMUNICATION_HOLD_MS;
@@ -154,7 +146,6 @@
});
const unsubscribeSettings = eventHub.onEvent(EVENT_SETTING_SAVED, (settings) => {
- syncOnReplicationSetting = settings?.P2P_SyncOnReplication ?? "";
refreshP2PRemoteOptions();
});
const unsubscribeLayoutReady = eventHub.onEvent(EVENT_LAYOUT_READY, () => {
@@ -174,6 +165,7 @@
unsubscribeReplicatorProgress();
unsubscribeSettings();
unsubscribeLayoutReady();
+ peerMenu?.hide();
};
});
@@ -206,8 +198,6 @@
const activated = activateP2PRemoteConfiguration(settings, id);
return activated || settings;
}, true);
- const latest = core.services.setting.currentSettings();
- syncOnReplicationSetting = latest.P2P_SyncOnReplication ?? "";
refreshP2PRemoteOptions();
} finally {
selectingP2PRemote = false;
@@ -252,8 +242,6 @@
const activated = activateP2PRemoteConfiguration(settings, id);
return activated || settings;
}, true);
- const latest = core.services.setting.currentSettings();
- syncOnReplicationSetting = latest.P2P_SyncOnReplication ?? "";
refreshP2PRemoteOptions();
}
@@ -299,7 +287,6 @@
const activated = activateP2PRemoteConfiguration(settings, selectedId);
return activated || settings;
}, true);
- syncOnReplicationSetting = core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? "";
}
async function makeDecision(
@@ -374,23 +361,49 @@
return isLiveCommunicating || isHeldCommunicating;
}
- function isSyncTarget(peerName: string) {
- return syncOnReplicationSetting
- .split(",")
- .map((e) => e.trim())
- .filter((e) => e)
- .includes(peerName);
+ function isPersistedPeerSettingEnabled(setting: PersistedP2PPeerSetting, peerName: string) {
+ const settings = core.services.setting.currentSettings();
+ return hasExactP2PPeer(settings[setting] ?? "", peerName);
}
- async function toggleSyncTarget(peer: P2PServerInfo["knownAdvertisements"][number]) {
+ async function togglePersistedPeerSetting(
+ peer: P2PServerInfo["knownAdvertisements"][number],
+ setting: PersistedP2PPeerSetting
+ ) {
if (!canEditP2PSettings()) {
return;
}
- const currentValue = core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? "";
- const newValue = isSyncTarget(peer.name)
- ? removeFromList(peer.name, currentValue)
- : addToList(peer.name, currentValue);
- await updateSelectedP2PRemote({ P2P_SyncOnReplication: newValue });
+ const currentSettings = core.services.setting.currentSettings();
+ await updateSelectedP2PRemote(togglePersistedP2PPeer(currentSettings, setting, peer.name));
+ }
+
+ function openPeerMenu(event: MouseEvent, peer: P2PServerInfo["knownAdvertisements"][number]) {
+ peerMenu?.hide();
+ peerMenu = new Menu()
+ .addItem((item) => {
+ item.setTitle(translateMessage("Synchronise when this device connects"))
+ .setChecked(isPersistedPeerSettingEnabled("P2P_AutoSyncPeers", peer.name))
+ .onClick(() => {
+ void togglePersistedPeerSetting(peer, "P2P_AutoSyncPeers");
+ });
+ })
+ .addItem((item) => {
+ item.setTitle(translateMessage("Follow whenever this device connects"))
+ .setChecked(isPersistedPeerSettingEnabled("P2P_AutoWatchPeers", peer.name))
+ .onClick(() => {
+ void togglePersistedPeerSetting(peer, "P2P_AutoWatchPeers");
+ });
+ })
+ .addItem((item) => {
+ item.setTitle(translateMessage("Include in the P2P synchronisation command"))
+ .setChecked(isPersistedPeerSettingEnabled("P2P_SyncOnReplication", peer.name))
+ .onClick(() => {
+ void togglePersistedPeerSetting(peer, "P2P_SyncOnReplication");
+ });
+ });
+ const target = event.currentTarget as HTMLElement;
+ const rect = target.getBoundingClientRect();
+ peerMenu.showAtPosition({ x: rect.left, y: rect.bottom });
}
@@ -486,35 +499,34 @@
>
Revoke
+
- WATCH
+ {translateMessage("Follow changes")}
-
- SYNC
-
-
{:else}
@@ -805,7 +817,7 @@
}
.accepted-row {
- grid-template-columns: 1fr auto auto;
+ grid-template-columns: 1fr auto auto auto;
}
.decision-label {
diff --git a/src/features/P2PSync/P2PReplicator/p2pPeerSettings.ts b/src/features/P2PSync/P2PReplicator/p2pPeerSettings.ts
new file mode 100644
index 00000000..fc494cfd
--- /dev/null
+++ b/src/features/P2PSync/P2PReplicator/p2pPeerSettings.ts
@@ -0,0 +1,38 @@
+import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
+
+export type PersistedP2PPeerSetting =
+ | "P2P_AutoSyncPeers"
+ | "P2P_AutoWatchPeers"
+ | "P2P_SyncOnReplication";
+
+function splitPeerSetting(value: string): string[] {
+ return value
+ .split(",")
+ .map((item) => item.trim())
+ .filter((item) => item !== "");
+}
+
+export function hasExactP2PPeer(value: string, peerName: string): boolean {
+ return splitPeerSetting(value).includes(peerName);
+}
+
+export function toggleExactP2PPeer(value: string, peerName: string): string {
+ const items = splitPeerSetting(value);
+ const existingIndex = items.indexOf(peerName);
+ if (existingIndex >= 0) {
+ items.splice(existingIndex, 1);
+ } else {
+ items.push(peerName);
+ }
+ return [...new Set(items)].join(",");
+}
+
+export function togglePersistedP2PPeer(
+ settings: ObsidianLiveSyncSettings,
+ setting: PersistedP2PPeerSetting,
+ peerName: string
+): Partial {
+ return {
+ [setting]: toggleExactP2PPeer(settings[setting] ?? "", peerName),
+ };
+}
diff --git a/src/features/P2PSync/P2PReplicator/p2pPeerSettings.unit.spec.ts b/src/features/P2PSync/P2PReplicator/p2pPeerSettings.unit.spec.ts
new file mode 100644
index 00000000..1021c499
--- /dev/null
+++ b/src/features/P2PSync/P2PReplicator/p2pPeerSettings.unit.spec.ts
@@ -0,0 +1,36 @@
+import { describe, expect, it } from "vitest";
+import type { ObsidianLiveSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/models/setting.type";
+import {
+ hasExactP2PPeer,
+ toggleExactP2PPeer,
+ togglePersistedP2PPeer,
+} from "./p2pPeerSettings";
+
+describe("persisted P2P peer controls", () => {
+ it("adds and removes only the selected peer without disturbing advanced patterns", () => {
+ const original = "~^phone-,desktop";
+
+ const added = toggleExactP2PPeer(original, "phone-main");
+ expect(added).toBe("~^phone-,desktop,phone-main");
+ expect(hasExactP2PPeer(added, "phone-main")).toBe(true);
+
+ const removed = toggleExactP2PPeer(added, "phone-main");
+ expect(removed).toBe(original);
+ });
+
+ it.each([
+ "P2P_AutoSyncPeers",
+ "P2P_AutoWatchPeers",
+ "P2P_SyncOnReplication",
+ ] as const)("updates %s through the same profile-backed setting boundary", (setting) => {
+ const settings = {
+ P2P_AutoSyncPeers: "",
+ P2P_AutoWatchPeers: "",
+ P2P_SyncOnReplication: "",
+ } as ObsidianLiveSyncSettings;
+
+ expect(togglePersistedP2PPeer(settings, setting, "peer-a")).toEqual({
+ [setting]: "peer-a",
+ });
+ });
+});
diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts
index 11f08416..4b1e9504 100644
--- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts
+++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts
@@ -29,6 +29,10 @@ import SetupRemote from "@/modules/features/SetupWizard/dialogs/SetupRemote.svel
import SetupRemoteCouchDB from "@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte";
import SetupRemoteBucket from "@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte";
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
+import type {
+ SetupRemoteCouchDBInitialData,
+ SetupRemoteCouchDBResultType,
+} from "@/modules/features/SetupWizard/dialogs/setupDialogTypes.ts";
import { syncActivatedRemoteSettings } from "./remoteConfigBuffer.ts";
function getSettingsFromEditingSettings(editingSettings: AllSettings): ObsidianLiveSyncSettings {
@@ -216,7 +220,13 @@ export function paneRemoteConfig(
return { ...baseSettings, ...p2pConf, remoteType: REMOTE_P2P };
}
- const couchConf = await dialogManager.openWithExplicitCancel(SetupRemoteCouchDB, baseSettings);
+ const couchConf = await dialogManager.openWithExplicitCancel<
+ SetupRemoteCouchDBResultType,
+ SetupRemoteCouchDBInitialData
+ >(SetupRemoteCouchDB, {
+ settings: baseSettings,
+ mode: "settings",
+ });
if (couchConf === "cancelled" || typeof couchConf !== "object") {
return false;
}
diff --git a/src/modules/features/SetupManager.ts b/src/modules/features/SetupManager.ts
index e461745a..b37f3ec0 100644
--- a/src/modules/features/SetupManager.ts
+++ b/src/modules/features/SetupManager.ts
@@ -1,6 +1,5 @@
import {
type BucketSyncSetting,
- type CouchDBConnection,
type EncryptionSettings,
type ObsidianLiveSyncSettings,
type P2PSyncSetting,
@@ -34,6 +33,7 @@ import type {
ScanQRCodeResultType,
SetupRemoteBucketResultType,
SetupRemoteCouchDBResultType,
+ SetupRemoteCouchDBInitialData,
SetupRemoteE2EEResultType,
SetupRemoteP2PResultType,
SetupRemoteResultType,
@@ -171,8 +171,16 @@ export class SetupManager extends AbstractModule {
): Promise {
const couchConf = await this.dialogManager.openWithExplicitCancel<
SetupRemoteCouchDBResultType,
- CouchDBConnection
- >(SetupRemoteCouchDB, currentSetting);
+ SetupRemoteCouchDBInitialData
+ >(SetupRemoteCouchDB, {
+ settings: currentSetting,
+ mode:
+ userMode === UserMode.NewUser
+ ? "create-or-connect"
+ : userMode === UserMode.ExistingUser
+ ? "connect-existing"
+ : "settings",
+ });
if (couchConf === "cancelled") {
this._log("Manual configuration cancelled.", LOG_LEVEL_NOTICE);
return await this.onOnboard(userMode);
diff --git a/src/modules/features/SetupManager.unit.spec.ts b/src/modules/features/SetupManager.unit.spec.ts
index 75c52909..fdbc8f57 100644
--- a/src/modules/features/SetupManager.unit.spec.ts
+++ b/src/modules/features/SetupManager.unit.spec.ts
@@ -348,6 +348,39 @@ describe("SetupManager", () => {
expect(activeProfile?.uri).toContain("sls+https://alice:secret@couch.example");
});
+ it.each([
+ [UserMode.NewUser, "create-or-connect"],
+ [UserMode.ExistingUser, "connect-existing"],
+ [UserMode.Update, "settings"],
+ ] as const)(
+ "passes the %s CouchDB database policy to the manual setup dialogue",
+ async (userMode, expectedMode) => {
+ const { manager, setting, dialogManager } = createSetupManager();
+ const couchConf = {
+ couchDB_URI: "https://couch.example",
+ couchDB_USER: "alice",
+ couchDB_PASSWORD: "secret",
+ couchDB_DBNAME: "notes",
+ couchDB_CustomHeaders: "",
+ useJWT: false,
+ jwtAlgorithm: "",
+ jwtKey: "",
+ jwtKid: "",
+ jwtSub: "",
+ jwtExpDuration: 5,
+ useRequestAPI: false,
+ };
+ dialogManager.openWithExplicitCancel.mockResolvedValueOnce(couchConf).mockResolvedValueOnce("cancelled");
+
+ await manager.onCouchDBManualSetup(userMode, setting.currentSettings());
+
+ expect(dialogManager.openWithExplicitCancel).toHaveBeenNthCalledWith(1, expect.anything(), {
+ settings: setting.currentSettings(),
+ mode: expectedMode,
+ });
+ }
+ );
+
it("adds and activates a manually configured Object Storage profile without replacing existing profiles", async () => {
const { manager, setting, dialogManager } = createSetupManager();
setting.settings = {
diff --git a/src/modules/features/SetupWizard/dialogs/Intro.svelte b/src/modules/features/SetupWizard/dialogs/Intro.svelte
index 54eabb12..e2cde71a 100644
--- a/src/modules/features/SetupWizard/dialogs/Intro.svelte
+++ b/src/modules/features/SetupWizard/dialogs/Intro.svelte
@@ -1,6 +1,7 @@
-
-
-
- The connection to the server has been configured successfully. As the next step, the latest synchronisation data will be downloaded from the server to this device.
-
-
- PLEASE NOTE
-
- After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised
- files in this vault, conflicts may occur with the server data.
-
-
-
- Please select the button below to restart and proceed to the data fetching confirmation.
-
-
- setResult(TYPE_APPLY)} />
- setResult(TYPE_CANCELLED)} />
-
+{#if isP2P}
+
+
+
+ {translateMessage(
+ "The P2P connection has been configured successfully. The initial synchronisation data must now be fetched from an online source device."
+ )}
+
+
+ PLEASE NOTE
+
+ {translateMessage(
+ "After restarting, select an online source device for the initial Fetch. The local LiveSync database on this device will be rebuilt from that source. Unsynchronised files in this Vault may conflict with the fetched data."
+ )}
+
+
+
+
+ {translateMessage("Restart this device, then choose the source device when P2P Rebuild opens.")}
+
+
+
+ setResult(TYPE_APPLY)}
+ />
+ setResult(TYPE_CANCELLED)} />
+
+{:else}
+
+
+
+ The connection to the server has been configured successfully. As the next step, the latest synchronisation data will be downloaded from the server to this device.
+
+
+ PLEASE NOTE
+
+ After restarting, the database on this device will be rebuilt using data from the server. If there are any unsynchronised
+ files in this vault, conflicts may occur with the server data.
+
+
+
+ Please select the button below to restart and proceed to the data fetching confirmation.
+
+
+ setResult(TYPE_APPLY)} />
+ setResult(TYPE_CANCELLED)} />
+
+{/if}
diff --git a/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte b/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte
index e3f768b7..39d825f6 100644
--- a/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte
+++ b/src/modules/features/SetupWizard/dialogs/PanelCouchDBCheck.svelte
@@ -7,10 +7,14 @@
import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte";
import { checkConfig, type ConfigCheckResult, type ResultError, type ResultErrorMessage } from "./utilCheckCouchDB";
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
+ import { $msg as translateMessage } from "@/common/translation";
+ import { getDialogContext } from "@/modules/services/LiveSyncUI/svelteDialog";
+ import { getCouchDBServerFixConfirmation } from "./couchDBServerFixConfirmation";
type Props = {
trialRemoteSetting: ObsidianLiveSyncSettings;
};
const { trialRemoteSetting }: Props = $props();
+ const context = getDialogContext();
let detectedIssues = $state([]);
async function testAndFixSettings() {
detectedIssues = [];
@@ -33,14 +37,23 @@
}
let processing = $state(false);
async function fixIssue(issue: ResultError) {
+ const confirmation = getCouchDBServerFixConfirmation(issue.settingKey, issue.expectedValue);
+ const confirmed = await context.services.confirm.askYesNoDialog(confirmation.message, {
+ title: confirmation.title,
+ defaultOption: "No",
+ });
+ if (confirmed !== "yes") {
+ return;
+ }
try {
processing = true;
await issue.fix();
} catch (e) {
Logger(e, LOG_LEVEL_VERBOSE, "setup-couchdb-fix");
+ } finally {
+ await testAndFixSettings();
+ processing = false;
}
- await testAndFixSettings();
- processing = false;
}
const errorIssueCount = $derived.by(() => {
return detectedIssues.filter((issue) => isErrorResult(issue)).length;
@@ -64,7 +77,7 @@
{/snippet}
-
+
diff --git a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte
index aaba8915..d479cb8f 100644
--- a/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte
+++ b/src/modules/features/SetupWizard/dialogs/SelectMethodNewUser.svelte
@@ -7,6 +7,7 @@
import Options from "@/modules/services/LiveSyncUI/components/Options.svelte";
import Instruction from "@/modules/services/LiveSyncUI/components/Instruction.svelte";
import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte";
+ import { $msg as translateMessage } from "@/common/translation";
import {
TYPE_USE_SETUP_URI,
TYPE_CONFIGURE_MANUALLY,
@@ -49,7 +50,10 @@
>
This is an advanced option for users who do not have a URI or who wish to configure detailed settings.
You can also select this option if you intend to use P2P (Peer-to-Peer) synchronisation
- instead of a CouchDB/S3 server — P2P requires no server setup at all.
+ instead of a CouchDB/S3 server.
+ {translateMessage(
+ "P2P requires no central data-storage server, but it still uses a signalling relay for peer discovery."
+ )}
diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte
index 133b78c7..702b389b 100644
--- a/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte
+++ b/src/modules/features/SetupWizard/dialogs/SetupRemote.svelte
@@ -6,6 +6,7 @@
import Options from "@/modules/services/LiveSyncUI/components/Options.svelte";
import Instruction from "@/modules/services/LiveSyncUI/components/Instruction.svelte";
import UserDecisions from "@/modules/services/LiveSyncUI/components/UserDecisions.svelte";
+ import { $msg as translateMessage } from "@/common/translation";
import {
TYPE_COUCHDB,
TYPE_BUCKET,
@@ -47,9 +48,9 @@
Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.
diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte
index 9ceda90b..a77fdf63 100644
--- a/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte
+++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte
@@ -21,18 +21,27 @@
import { getDialogContext, type GuestDialogProps } from "@/modules/services/LiveSyncUI/svelteDialog";
import { copyTo, pickCouchDBSyncSettings } from "@vrtmrz/livesync-commonlib/compat/common/utils";
import PanelCouchDBCheck from "./PanelCouchDBCheck.svelte";
- import { TYPE_CANCELLED, type SetupRemoteCouchDBResultType } from "./setupDialogTypes";
+ import {
+ TYPE_CANCELLED,
+ type CouchDBSetupMode,
+ type SetupRemoteCouchDBInitialData,
+ type SetupRemoteCouchDBResultType,
+ } from "./setupDialogTypes";
+ import { isValidCouchDBServerURL, probeCouchDBConnection } from "./couchDBConnectionProbe";
+ import { $msg as translateMessage } from "@/common/translation";
const default_setting = pickCouchDBSyncSettings(DEFAULT_SETTINGS);
let syncSetting = $state({ ...default_setting });
- type Props = GuestDialogProps;
+ let setupMode = $state("settings");
+ type Props = GuestDialogProps;
const { setResult, getInitialData }: Props = $props();
onMount(() => {
if (getInitialData) {
const initialData = getInitialData();
if (initialData) {
- copyTo(initialData, syncSetting);
+ setupMode = initialData.mode;
+ copyTo(initialData.settings, syncSetting);
}
}
});
@@ -69,11 +78,15 @@
return "Failed to create replicator instance.";
}
try {
- const result = await replicator.tryConnectRemote(trialRemoteSetting, false);
- if (result) {
+ const result = await probeCouchDBConnection(
+ replicator,
+ trialRemoteSetting,
+ setupMode === "create-or-connect"
+ );
+ if (result.ok) {
return "";
} else {
- return "Failed to connect to the server. Please check your settings.";
+ return `Failed to connect to the server: ${result.reason}`;
}
} catch (e) {
return `Failed to connect to the server: ${e}`;
@@ -122,7 +135,7 @@
});
const canProceed = $derived.by(() => {
return (
- syncSetting.couchDB_URI.trim().length > 0 &&
+ isValidCouchDBServerURL(syncSetting.couchDB_URI.trim()) &&
syncSetting.couchDB_USER.trim().length > 0 &&
syncSetting.couchDB_PASSWORD.trim().length > 0 &&
syncSetting.couchDB_DBNAME.trim().length > 0 &&
@@ -132,6 +145,18 @@
const testSettings = $derived.by(() => {
return generateSetting();
});
+ const isURLInvalid = $derived.by(
+ () => syncSetting.couchDB_URI.trim() !== "" && !isValidCouchDBServerURL(syncSetting.couchDB_URI.trim())
+ );
+ const primaryActionTitle = $derived.by(() => {
+ if (setupMode === "create-or-connect") {
+ return translateMessage("Create or connect to database and continue");
+ }
+ if (setupMode === "connect-existing") {
+ return translateMessage("Connect to existing database and continue");
+ }
+ return translateMessage("Test connection and save");
+ });
@@ -150,6 +175,7 @@
/>
We can use only Secure (HTTPS) connections on Obsidian Mobile.
+{translateMessage("Enter a complete HTTP or HTTPS URL.")}
- You cannot use capital letters, spaces, or special characters in the database name. And not allowed to start with an
- underscore (_).
+ {translateMessage("CouchDB validates the database name when you connect. The name must not be empty.")}
@@ -270,6 +294,11 @@
+
+ {translateMessage(
+ "This optional check uses Obsidian's internal request API and sends the credentials above to the CouchDB server. Use it only with a server you trust; administrator access may be required."
+ )}
+
@@ -281,8 +310,19 @@
Checking connection... Please wait.
{:else}
- checkAndCommit()} />
- commit()} />
+ checkAndCommit()} />
+ {#if setupMode === "settings"}
+
+ {translateMessage(
+ "Saving without a successful connection test keeps this profile, but automatic synchronisation may fail until the connection is corrected."
+ )}
+
+ commit()}
+ />
+ {/if}
cancel()} />
{/if}
diff --git a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte
index 1970c991..ea807da5 100644
--- a/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte
+++ b/src/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte
@@ -33,6 +33,8 @@
import ExtraItems from "@/modules/services/LiveSyncUI/components/ExtraItems.svelte";
import { TYPE_CANCELLED, type SetupRemoteP2PResultType } from "./setupDialogTypes";
import { LOG_LEVEL_VERBOSE, Logger } from "octagonal-wheels/common/logger";
+ import { $msg as translateMessage } from "@/common/translation";
+ import { probeP2PSetupConnection } from "./p2pSetupConnectionProbe";
const default_setting = pickP2PSyncSettings(DEFAULT_SETTINGS);
let syncSetting = $state({ ...default_setting });
@@ -119,29 +121,15 @@
};
const replicator = new TrysteroReplicator(env);
try {
- await replicator.setOnSetup();
- await replicator.allowReconnection();
- await replicator.open();
- for (let i = 0; i < 10; i++) {
- // await delay(1000);
- await new Promise((resolve) => window.setTimeout(resolve, 1000));
- // Logger(`Checking known advertisements... (${i})`, LOG_LEVEL_INFO);
- if (replicator.knownAdvertisements.length > 0) {
- break;
- }
- }
- // context.holdingSettings = trialRemoteSetting;
-
- if (replicator.knownAdvertisements.length === 0) {
- return "Your settings seem correct, but no other peers were found.";
+ const result = await probeP2PSetupConnection(replicator);
+ if (!result.ok) {
+ return `Failed to connect to the signalling relay: ${result.reason}`;
}
return "";
- } catch (e) {
- return `Failed to connect to other peers: ${e}`;
} finally {
try {
- replicator.close();
- dummyPouch.destroy();
+ await replicator.close();
+ await dummyPouch.destroy();
} catch (e) {
Logger(e, LOG_LEVEL_VERBOSE, "setup-p2p-cleanup");
}
@@ -195,18 +183,31 @@
-
+
-
+
+
+ {translateMessage("Peer discovery uses Nostr-compatible signalling relays.")}
+ {translateMessage(
+ "The project's public signalling relay is a best-effort convenience operated by the project author. It does not store Vault contents, but signalling metadata may be visible to the relay. Availability and log retention are not guaranteed. You can replace it with your own Nostr-compatible relay."
+ )}
+ {translateMessage("Learn more about P2P connections")}.
+
-
+
- If "Auto Broadcast Changes" is enabled, changes will be automatically broadcasted to connected peers without
- requiring manual intervention. This requests peers to fetch this device's changes.
+ {translateMessage(
+ "When enabled, this device notifies connected peers after a local change. The notification contains no Vault data; a peer which follows this device then fetches the change through the encrypted P2P connection."
+ )}
@@ -258,10 +260,14 @@
connections. In most cases, you can leave these fields blank.
- Using public TURN servers may have privacy implications, as your data will be relayed through third-party
- servers. Even if your data are encrypted, your existence may be known to them. Please ensure you trust the TURN
- server provider before using their services. Also your `network administrator` too. You should consider setting
- up your own TURN server for your FQDN, if possible.
+ {translateMessage(
+ "TURN relays the encrypted WebRTC connection only when a direct path cannot be established. A TURN provider cannot read encrypted Vault contents, but it can observe connection metadata and traffic volume. Use a provider you trust."
+ )}
+ {translateMessage("Learn more about signalling and TURN")}.