mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-24 21:42:58 +00:00
Clarify remote setup and verify manual CouchDB onboarding
This commit is contained in:
+22
-2
@@ -100,6 +100,26 @@ Add optional features separately so that their ownership and initialisation dire
|
||||
|
||||
Do not enable both features for the same files.
|
||||
|
||||
## Manual configuration
|
||||
## Configure CouchDB manually on the first device
|
||||
|
||||
If a Setup URI is unavailable, choose `Enter the server information manually` during onboarding. Manual configuration is an advanced path: verify the connection, encryption, remote profile, and synchronisation preset before initialising either side. After the first device works, use `Copy settings as a new Setup URI` from the command palette to add later devices through the recommended path.
|
||||
Use this path when CouchDB is ready but a Setup URI is unavailable. It configures one first device through the visible onboarding dialogue; it does not provision or repair the CouchDB server. Add later devices with a Setup URI generated by this working first device instead of entering the credentials again.
|
||||
|
||||
1. Install and enable Self-hosted LiveSync in the intended Vault.
|
||||
2. Select the `Welcome to Self-hosted LiveSync` Notice, choose `I am setting this up for the first time`, then confirm that you want to set up a new synchronisation.
|
||||
3. On `Connection Method`, select `Configure a remote manually`, then select `Proceed with manual configuration`.
|
||||
4. On `End-to-End Encryption`, decide how the synchronised data will be protected.
|
||||
- For an ordinary new Vault, enable `End-to-End Encryption` and enter a strong Vault encryption passphrase.
|
||||
- Enable `Obfuscate Properties` if remote document properties should also be concealed.
|
||||
- Store the Vault encryption passphrase securely. It is separate from the passphrase used to protect a Setup URI.
|
||||
5. On `Choose a synchronisation remote`, select `CouchDB`, then select `Continue to CouchDB setup`.
|
||||
6. Enter the complete CouchDB URL, username, password, and database name.
|
||||
- Obsidian Mobile requires HTTPS. Plain HTTP is suitable only for a trusted local connection from a desktop device.
|
||||
- Use credentials which are allowed to connect to the selected database and, for this first-device path, create it when it does not exist.
|
||||
7. `Check server requirements` is optional. It sends the displayed credentials to the configured server through Obsidian's internal request API, and some checks require CouchDB administrator access. The initial check is read-only. If it offers a server change, review and confirm that individual change separately.
|
||||
8. Select `Create or connect to database and continue`. Onboarding requires this connection test to succeed.
|
||||
9. Review `Setup Complete: Preparing to Initialise Server`, then select `Restart and Initialise Server`.
|
||||
10. Read the final overwrite warning. Select `I Understand, Overwrite Server` only when this device is intentionally the source of truth and a current backup exists.
|
||||
11. A newly created database can show `Fetch Remote Configuration Failed` because it does not yet contain a saved preferred configuration. Select `Skip and proceed` only for this known new database.
|
||||
12. Acknowledge `All optional features are disabled`, then keep Obsidian open until the initialisation progress has cleared.
|
||||
|
||||
Create and synchronise an ordinary test note. Once it has reached CouchDB, follow [Create a Setup URI for another device](#create-a-setup-uri-for-another-device), then [Add another device](#add-another-device). This keeps the second device aligned with the remote profile and encryption settings which the first device actually applied.
|
||||
|
||||
@@ -167,7 +167,7 @@ Now `https://tiles-photograph-routine-groundwater.trycloudflare.com` is our serv
|
||||
|
||||
## 4. Client Setup
|
||||
> [!TIP]
|
||||
> Now manual configuration is not recommended for some reasons. However, if you want to do so, please use `Setup wizard`. The recommended extra configurations will be also set.
|
||||
> A generated Setup URI is the recommended path because it carries the current new-Vault defaults and remote profile. If a Setup URI cannot be generated, follow [Configure CouchDB manually on the first device](./quick_setup.md#configure-couchdb-manually-on-the-first-device), then generate a new Setup URI from that working device for every additional device.
|
||||
|
||||
### 1. Generate the setup URI on a desktop device or server
|
||||
```bash
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
"test:e2e:obsidian:p2p-pane": "tsx test/e2e-obsidian/scripts/p2p-pane.ts",
|
||||
"test:e2e:obsidian:vault-reflection": "tsx test/e2e-obsidian/scripts/vault-reflection.ts",
|
||||
"test:e2e:obsidian:couchdb-upload": "tsx test/e2e-obsidian/scripts/couchdb-upload.ts",
|
||||
"test:e2e:obsidian:couchdb-manual-setup-workflow": "tsx test/e2e-obsidian/scripts/couchdb-manual-setup-workflow.ts",
|
||||
"test:e2e:obsidian:cli-to-obsidian-sync": "tsx test/e2e-obsidian/scripts/cli-to-obsidian-sync.ts",
|
||||
"test:e2e:obsidian:minio-upload": "tsx test/e2e-obsidian/scripts/minio-upload.ts",
|
||||
"test:e2e:obsidian:object-storage-setup-uri-workflow": "tsx test/e2e-obsidian/scripts/object-storage-setup-uri-workflow.ts",
|
||||
|
||||
@@ -8810,15 +8810,15 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "你正在将此设备加入已有同步配置。",
|
||||
},
|
||||
"Ui.SetupWizard.SelectExisting.ManualOption": {
|
||||
def: "Enter the server information manually",
|
||||
def: "Configure a remote manually",
|
||||
zh: "手动输入服务器信息",
|
||||
},
|
||||
"Ui.SetupWizard.SelectExisting.ManualOptionDesc": {
|
||||
def: "Configure the same server information as your other devices again manually. This is intended only for advanced users.",
|
||||
def: "Configure the same remote as your other devices again manually. This is intended only for advanced users.",
|
||||
zh: "手动重新配置与你其他设备相同的服务器信息。此方式仅适用于高级用户。",
|
||||
},
|
||||
"Ui.SetupWizard.SelectExisting.ProceedManual": {
|
||||
def: "I know my server details, let me enter them",
|
||||
def: "Proceed with manual configuration",
|
||||
zh: "我知道服务器信息,让我手动输入",
|
||||
},
|
||||
"Ui.SetupWizard.SelectExisting.ProceedQr": {
|
||||
@@ -8854,19 +8854,19 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "设备设置方式",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.Guidance": {
|
||||
def: "We will now proceed with the server configuration.",
|
||||
def: "We will now configure the synchronisation connection.",
|
||||
zh: "接下来将继续配置服务器连接信息。",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.ManualOption": {
|
||||
def: "Enter the server information manually",
|
||||
def: "Configure a remote manually",
|
||||
zh: "手动输入服务器信息",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.ManualOptionDesc": {
|
||||
def: "This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings.",
|
||||
def: "This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings. You can also use it for P2P synchronisation instead of CouchDB or S3-compatible Object Storage.",
|
||||
zh: "如果你没有 Setup URI,或希望自行配置更详细的参数,可选择此高级选项。",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.ProceedManual": {
|
||||
def: "I know my server details, let me enter them",
|
||||
def: "Proceed with manual configuration",
|
||||
zh: "我知道服务器信息,让我手动输入",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.ProceedSetupUri": {
|
||||
@@ -8874,7 +8874,7 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "使用 Setup URI 继续",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.Question": {
|
||||
def: "How would you like to configure the connection to your server?",
|
||||
def: "How would you like to configure this synchronisation connection?",
|
||||
zh: "你希望如何配置服务器连接?",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.SetupUriOption": {
|
||||
@@ -8882,7 +8882,7 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "使用 Setup URI(推荐)",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.SetupUriOptionDesc": {
|
||||
def: "A Setup URI is a single string containing your server address and authentication details. If one was generated by your server installation script, it provides a simple and secure configuration method.",
|
||||
def: "A Setup URI is a single string containing connection and authentication details. When one is available from a setup script, it provides a simple and secure configuration method.",
|
||||
zh: "Setup URI 是一段包含服务器地址和认证信息的文本。如果你的服务器安装脚本已经生成了它,这是最简单且安全的配置方式。",
|
||||
},
|
||||
"Ui.SetupWizard.SelectNew.Title": {
|
||||
@@ -8890,11 +8890,11 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "连接方式",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.BucketOption": {
|
||||
def: "S3/MinIO/R2 Object Storage",
|
||||
def: "S3-compatible Object Storage",
|
||||
zh: "S3/MinIO/R2 对象存储",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.BucketOptionDesc": {
|
||||
def: "Synchronisation using journal files. You must already have an S3/MinIO/R2 compatible object storage service set up.",
|
||||
def: "Synchronisation using journal files. You must already have an S3-compatible Object Storage service set up, such as Amazon S3, MinIO, or Cloudflare R2.",
|
||||
zh: "使用日志文件进行同步。你需要先准备好兼容 S3/MinIO/R2 的对象存储服务。",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": {
|
||||
@@ -8902,11 +8902,11 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "这是当前设计下最适合的同步方式,所有功能都可用。你需要先准备好 CouchDB 实例。",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.Guidance": {
|
||||
def: "Please select the type of server you are connecting to.",
|
||||
def: "Select the remote type for this synchronisation setup.",
|
||||
zh: "请选择你要连接的服务器类型。",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.P2POption": {
|
||||
def: "Peer-to-Peer only",
|
||||
def: "Peer-to-Peer (P2P)",
|
||||
zh: "仅点对点",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.P2POptionDesc": {
|
||||
@@ -8914,7 +8914,7 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "启用设备之间的直接同步。无需服务器,但两台设备必须同时在线,且部分功能可能受限。互联网连接仅用于信令,不用于传输数据。",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.ProceedBucket": {
|
||||
def: "Continue to S3/MinIO/R2 setup",
|
||||
def: "Continue to Object Storage setup",
|
||||
zh: "继续配置 S3/MinIO/R2",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.ProceedCouchDb": {
|
||||
@@ -8922,11 +8922,11 @@ export const allMessages: Readonly<Record<string, Readonly<Record<string, string
|
||||
zh: "继续配置 CouchDB",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.ProceedP2P": {
|
||||
def: "Continue to Peer-to-Peer only setup",
|
||||
def: "Continue to P2P setup",
|
||||
zh: "继续配置仅点对点模式",
|
||||
},
|
||||
"Ui.SetupWizard.SetupRemote.Title": {
|
||||
def: "Enter Server Information",
|
||||
def: "Choose a synchronisation remote",
|
||||
zh: "输入服务器信息",
|
||||
},
|
||||
"Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": {
|
||||
|
||||
@@ -1074,9 +1074,9 @@
|
||||
"Ui.SetupWizard.RebuildEverythingP2P.Proceed": "I Understand, Prepare This Device",
|
||||
"Ui.SetupWizard.RebuildEverythingP2P.Title": "Final Confirmation: Prepare This Device for P2P",
|
||||
"Ui.SetupWizard.SelectExisting.Guidance": "You are adding this device to an existing synchronisation setup.",
|
||||
"Ui.SetupWizard.SelectExisting.ManualOption": "Enter the server information manually",
|
||||
"Ui.SetupWizard.SelectExisting.ManualOptionDesc": "Configure the same server information as your other devices again manually. This is intended only for advanced users.",
|
||||
"Ui.SetupWizard.SelectExisting.ProceedManual": "I know my server details, let me enter them",
|
||||
"Ui.SetupWizard.SelectExisting.ManualOption": "Configure a remote manually",
|
||||
"Ui.SetupWizard.SelectExisting.ManualOptionDesc": "Configure the same remote as your other devices again manually. This is intended only for advanced users.",
|
||||
"Ui.SetupWizard.SelectExisting.ProceedManual": "Proceed with manual configuration",
|
||||
"Ui.SetupWizard.SelectExisting.ProceedQr": "Scan the QR code displayed on an active device using this device's camera.",
|
||||
"Ui.SetupWizard.SelectExisting.ProceedSetupUri": "Proceed with Setup URI",
|
||||
"Ui.SetupWizard.SelectExisting.QrOption": "Scan a QR Code (Recommended for mobile)",
|
||||
@@ -1085,25 +1085,25 @@
|
||||
"Ui.SetupWizard.SelectExisting.SetupUriOption": "Use a Setup URI (Recommended)",
|
||||
"Ui.SetupWizard.SelectExisting.SetupUriOptionDesc": "Paste the Setup URI generated from one of your active devices.",
|
||||
"Ui.SetupWizard.SelectExisting.Title": "Device Setup Method",
|
||||
"Ui.SetupWizard.SelectNew.Guidance": "We will now proceed with the server configuration.",
|
||||
"Ui.SetupWizard.SelectNew.ManualOption": "Enter the server information manually",
|
||||
"Ui.SetupWizard.SelectNew.ManualOptionDesc": "This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings.",
|
||||
"Ui.SetupWizard.SelectNew.ProceedManual": "I know my server details, let me enter them",
|
||||
"Ui.SetupWizard.SelectNew.Guidance": "We will now configure the synchronisation connection.",
|
||||
"Ui.SetupWizard.SelectNew.ManualOption": "Configure a remote manually",
|
||||
"Ui.SetupWizard.SelectNew.ManualOptionDesc": "This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings. You can also use it for P2P synchronisation instead of CouchDB or S3-compatible Object Storage.",
|
||||
"Ui.SetupWizard.SelectNew.ProceedManual": "Proceed with manual configuration",
|
||||
"Ui.SetupWizard.SelectNew.ProceedSetupUri": "Proceed with Setup URI",
|
||||
"Ui.SetupWizard.SelectNew.Question": "How would you like to configure the connection to your server?",
|
||||
"Ui.SetupWizard.SelectNew.Question": "How would you like to configure this synchronisation connection?",
|
||||
"Ui.SetupWizard.SelectNew.SetupUriOption": "Use a Setup URI (Recommended)",
|
||||
"Ui.SetupWizard.SelectNew.SetupUriOptionDesc": "A Setup URI is a single string containing your server address and authentication details. If one was generated by your server installation script, it provides a simple and secure configuration method.",
|
||||
"Ui.SetupWizard.SelectNew.SetupUriOptionDesc": "A Setup URI is a single string containing connection and authentication details. When one is available from a setup script, it provides a simple and secure configuration method.",
|
||||
"Ui.SetupWizard.SelectNew.Title": "Connection Method",
|
||||
"Ui.SetupWizard.SetupRemote.BucketOption": "S3/MinIO/R2 Object Storage",
|
||||
"Ui.SetupWizard.SetupRemote.BucketOptionDesc": "Synchronisation using journal files. You must already have an S3/MinIO/R2 compatible object storage service set up.",
|
||||
"Ui.SetupWizard.SetupRemote.BucketOption": "S3-compatible Object Storage",
|
||||
"Ui.SetupWizard.SetupRemote.BucketOptionDesc": "Synchronisation using journal files. You must already have an S3-compatible Object Storage service set up, such as Amazon S3, MinIO, or Cloudflare R2.",
|
||||
"Ui.SetupWizard.SetupRemote.CouchDbOptionDesc": "This is the most suitable synchronisation method for the current design. All features are available. You must already have a CouchDB instance set up.",
|
||||
"Ui.SetupWizard.SetupRemote.Guidance": "Please select the type of server you are connecting to.",
|
||||
"Ui.SetupWizard.SetupRemote.P2POption": "Peer-to-Peer only",
|
||||
"Ui.SetupWizard.SetupRemote.Guidance": "Select the remote type for this synchronisation setup.",
|
||||
"Ui.SetupWizard.SetupRemote.P2POption": "Peer-to-Peer (P2P)",
|
||||
"Ui.SetupWizard.SetupRemote.P2POptionDesc": "This enables direct synchronisation between devices. No server is required, but both devices must be online at the same time and some features may be limited. Internet connectivity is required only for signalling, not for data transfer.",
|
||||
"Ui.SetupWizard.SetupRemote.ProceedBucket": "Continue to S3/MinIO/R2 setup",
|
||||
"Ui.SetupWizard.SetupRemote.ProceedBucket": "Continue to Object Storage setup",
|
||||
"Ui.SetupWizard.SetupRemote.ProceedCouchDb": "Continue to CouchDB setup",
|
||||
"Ui.SetupWizard.SetupRemote.ProceedP2P": "Continue to Peer-to-Peer only setup",
|
||||
"Ui.SetupWizard.SetupRemote.Title": "Enter Server Information",
|
||||
"Ui.SetupWizard.SetupRemote.ProceedP2P": "Continue to P2P setup",
|
||||
"Ui.SetupWizard.SetupRemote.Title": "Choose a synchronisation remote",
|
||||
"Unique name between all synchronized devices. To edit this setting, please disable customization sync once.": "Unique name between all synchronized devices. To edit this setting, please disable customization sync once.",
|
||||
"Use a custom passphrase": "Use a custom passphrase",
|
||||
"Use a Setup URI (Recommended)": "Use a Setup URI (Recommended)",
|
||||
|
||||
@@ -934,25 +934,25 @@ P2P:
|
||||
NoKnownPeers: No peers has been detected, waiting incoming other peers...
|
||||
Note:
|
||||
description: >2-
|
||||
This replicator allows us to synchronise our vault with other devices
|
||||
using a peer-to-peer connection. We can use this to synchronise our vault
|
||||
with our other devices without using a cloud service.
|
||||
This replicator allows us to synchronise our vault with other devices
|
||||
using a peer-to-peer connection. We can use this to synchronise our vault
|
||||
with our other devices without using a cloud service.
|
||||
|
||||
This replicator is based on Trystero. It also uses a signalling server to
|
||||
establish a connection between devices. The signalling server is used to
|
||||
exchange connection information between devices. It does (or,should) not
|
||||
know or store any of our data.
|
||||
This replicator is based on Trystero. It also uses a signalling server to
|
||||
establish a connection between devices. The signalling server is used to
|
||||
exchange connection information between devices. It does (or,should) not
|
||||
know or store any of our data.
|
||||
|
||||
|
||||
The signalling server can be hosted by anyone. This is just a Nostr relay.
|
||||
For the sake of simplicity and checking the behaviour of the replicator,
|
||||
an instance of the signalling server is hosted by vrtmrz. You can use the
|
||||
experimental server provided by vrtmrz, or you can use any other server.
|
||||
The signalling server can be hosted by anyone. This is just a Nostr relay.
|
||||
For the sake of simplicity and checking the behaviour of the replicator,
|
||||
an instance of the signalling server is hosted by vrtmrz. You can use the
|
||||
experimental server provided by vrtmrz, or you can use any other server.
|
||||
|
||||
|
||||
By the way, even if the signalling server does not store our data, it can
|
||||
see the connection information of some of our devices. Please be aware of
|
||||
this. Also, be cautious when using the server provided by someone else.
|
||||
By the way, even if the signalling server does not store our data, it can
|
||||
see the connection information of some of our devices. Please be aware of
|
||||
this. Also, be cautious when using the server provided by someone else.
|
||||
important_note: Peer-to-Peer Replicator.
|
||||
important_note_sub: This feature is still on the bleeding edge. Please be aware
|
||||
that ensure your data is backed up before using this feature. And, we
|
||||
@@ -1895,9 +1895,9 @@ Ui:
|
||||
Guidance: We will now guide you through a few questions to simplify the synchronisation setup.
|
||||
SelectExisting:
|
||||
Guidance: You are adding this device to an existing synchronisation setup.
|
||||
ManualOption: Enter the server information manually
|
||||
ManualOptionDesc: Configure the same server information as your other devices again manually. This is intended only for advanced users.
|
||||
ProceedManual: I know my server details, let me enter them
|
||||
ManualOption: Configure a remote manually
|
||||
ManualOptionDesc: Configure the same remote as your other devices again manually. This is intended only for advanced users.
|
||||
ProceedManual: Proceed with manual configuration
|
||||
ProceedQr: Scan the QR code displayed on an active device using this device's camera.
|
||||
ProceedSetupUri: Proceed with Setup URI
|
||||
Question: Please select a method to import the settings from another device.
|
||||
@@ -1907,14 +1907,14 @@ Ui:
|
||||
SetupUriOptionDesc: Paste the Setup URI generated from one of your active devices.
|
||||
Title: Device Setup Method
|
||||
SelectNew:
|
||||
Guidance: We will now proceed with the server configuration.
|
||||
ManualOption: Enter the server information manually
|
||||
ManualOptionDesc: This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings.
|
||||
ProceedManual: I know my server details, let me enter them
|
||||
Guidance: We will now configure the synchronisation connection.
|
||||
ManualOption: Configure a remote manually
|
||||
ManualOptionDesc: This is an advanced option for users who do not have a Setup URI or who want to configure detailed settings. You can also use it for P2P synchronisation instead of CouchDB or S3-compatible Object Storage.
|
||||
ProceedManual: Proceed with manual configuration
|
||||
ProceedSetupUri: Proceed with Setup URI
|
||||
Question: How would you like to configure the connection to your server?
|
||||
Question: How would you like to configure this synchronisation connection?
|
||||
SetupUriOption: Use a Setup URI (Recommended)
|
||||
SetupUriOptionDesc: A Setup URI is a single string containing your server address and authentication details. If one was generated by your server installation script, it provides a simple and secure configuration method.
|
||||
SetupUriOptionDesc: A Setup URI is a single string containing connection and authentication details. When one is available from a setup script, it provides a simple and secure configuration method.
|
||||
Title: Connection Method
|
||||
OutroAskUserMode:
|
||||
CompatibleOption: The remote is already set up, and the configuration is compatible (or became compatible through this operation).
|
||||
@@ -1951,13 +1951,13 @@ Ui:
|
||||
Proceed: I Understand, Prepare This Device
|
||||
Title: "Final Confirmation: Prepare This Device for P2P"
|
||||
SetupRemote:
|
||||
BucketOption: S3/MinIO/R2 Object Storage
|
||||
BucketOptionDesc: Synchronisation using journal files. You must already have an S3/MinIO/R2 compatible object storage service set up.
|
||||
BucketOption: S3-compatible Object Storage
|
||||
BucketOptionDesc: Synchronisation using journal files. You must already have an S3-compatible Object Storage service set up, such as Amazon S3, MinIO, or Cloudflare R2.
|
||||
CouchDbOptionDesc: This is the most suitable synchronisation method for the current design. All features are available. You must already have a CouchDB instance set up.
|
||||
Guidance: Please select the type of server you are connecting to.
|
||||
P2POption: Peer-to-Peer only
|
||||
Guidance: Select the remote type for this synchronisation setup.
|
||||
P2POption: Peer-to-Peer (P2P)
|
||||
P2POptionDesc: This enables direct synchronisation between devices. No server is required, but both devices must be online at the same time and some features may be limited. Internet connectivity is required only for signalling, not for data transfer.
|
||||
ProceedBucket: Continue to S3/MinIO/R2 setup
|
||||
ProceedBucket: Continue to Object Storage setup
|
||||
ProceedCouchDb: Continue to CouchDB setup
|
||||
ProceedP2P: Continue to Peer-to-Peer only setup
|
||||
Title: Enter Server Information
|
||||
ProceedP2P: Continue to P2P setup
|
||||
Title: Choose a synchronisation remote
|
||||
|
||||
@@ -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_SCAN_QR_CODE,
|
||||
@@ -24,7 +25,7 @@
|
||||
if (userType === TYPE_USE_SETUP_URI) {
|
||||
return "Proceed with Setup URI";
|
||||
} else if (userType === TYPE_CONFIGURE_MANUALLY) {
|
||||
return "I know my server details, let me enter them";
|
||||
return translateMessage("Ui.SetupWizard.SelectExisting.ProceedManual");
|
||||
} else if (userType === TYPE_SCAN_QR_CODE) {
|
||||
return "Scan the QR code displayed on an active device using this device's camera.";
|
||||
} else {
|
||||
@@ -49,10 +50,10 @@
|
||||
</Option>
|
||||
<Option
|
||||
selectedValue={TYPE_CONFIGURE_MANUALLY}
|
||||
title="Enter the server information manually"
|
||||
title={translateMessage("Ui.SetupWizard.SelectExisting.ManualOption")}
|
||||
bind:value={userType}
|
||||
>
|
||||
Configure the same server information as your other devices again, manually, very advanced users only.
|
||||
{translateMessage("Ui.SetupWizard.SelectExisting.ManualOptionDesc")}
|
||||
</Option>
|
||||
</Options>
|
||||
</Instruction>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
if (userType === TYPE_USE_SETUP_URI) {
|
||||
return "Proceed with Setup URI";
|
||||
} else if (userType === TYPE_CONFIGURE_MANUALLY) {
|
||||
return "I know my server details, let me enter them";
|
||||
return translateMessage("Ui.SetupWizard.SelectNew.ProceedManual");
|
||||
} else {
|
||||
return "Please select an option to proceed";
|
||||
}
|
||||
@@ -35,22 +35,19 @@
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Connection Method" />
|
||||
<Guidance>We will now proceed with the server configuration.</Guidance>
|
||||
<Guidance>{translateMessage("Ui.SetupWizard.SelectNew.Guidance")}</Guidance>
|
||||
<Instruction>
|
||||
<Question>How would you like to configure the connection to your server?</Question>
|
||||
<Question>{translateMessage("Ui.SetupWizard.SelectNew.Question")}</Question>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_USE_SETUP_URI} title="Use a Setup URI (Recommended)" bind:value={userType}>
|
||||
A Setup URI is a single string of text containing your server address and authentication details. Using a
|
||||
URI, if one was generated by your server installation script, provides a simple and secure configuration.
|
||||
{translateMessage("Ui.SetupWizard.SelectNew.SetupUriOptionDesc")}
|
||||
</Option>
|
||||
<Option
|
||||
selectedValue={TYPE_CONFIGURE_MANUALLY}
|
||||
title="Enter the server information manually"
|
||||
title={translateMessage("Ui.SetupWizard.SelectNew.ManualOption")}
|
||||
bind:value={userType}
|
||||
>
|
||||
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 <strong>P2P (Peer-to-Peer) synchronisation</strong>
|
||||
instead of a CouchDB/S3 server.
|
||||
{translateMessage("Ui.SetupWizard.SelectNew.ManualOptionDesc")}
|
||||
{translateMessage(
|
||||
"P2P requires no central data-storage server, but it still uses a signalling relay for peer discovery."
|
||||
)}
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
if (userType === TYPE_COUCHDB) {
|
||||
return "Continue to CouchDB setup";
|
||||
} else if (userType === TYPE_BUCKET) {
|
||||
return "Continue to S3/MinIO/R2 setup";
|
||||
return translateMessage("Ui.SetupWizard.SetupRemote.ProceedBucket");
|
||||
} else if (userType === TYPE_P2P) {
|
||||
return "Continue to Peer-to-Peer only setup";
|
||||
return translateMessage("Ui.SetupWizard.SetupRemote.ProceedP2P");
|
||||
} else {
|
||||
return "Please select an option to proceed";
|
||||
}
|
||||
@@ -36,18 +36,26 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<DialogHeader title="Enter Server Information" />
|
||||
<DialogHeader title={translateMessage("Ui.SetupWizard.SetupRemote.Title")} />
|
||||
<Instruction>
|
||||
<Question>Please select the type of server to which you are connecting.</Question>
|
||||
<Question>{translateMessage("Ui.SetupWizard.SetupRemote.Guidance")}</Question>
|
||||
<Options>
|
||||
<Option selectedValue={TYPE_COUCHDB} title="CouchDB" bind:value={userType}>
|
||||
This is the most suitable synchronisation method for the design. All functions are available. You must have
|
||||
set up a CouchDB instance.
|
||||
</Option>
|
||||
<Option selectedValue={TYPE_BUCKET} title="S3/MinIO/R2 Object Storage" bind:value={userType}>
|
||||
Synchronisation utilising journal files. You must have set up an S3/MinIO/R2 compatible object storage.
|
||||
<Option
|
||||
selectedValue={TYPE_BUCKET}
|
||||
title={translateMessage("Ui.SetupWizard.SetupRemote.BucketOption")}
|
||||
bind:value={userType}
|
||||
>
|
||||
{translateMessage("Ui.SetupWizard.SetupRemote.BucketOptionDesc")}
|
||||
</Option>
|
||||
<Option selectedValue={TYPE_P2P} title="Peer-to-Peer only" bind:value={userType}>
|
||||
<Option
|
||||
selectedValue={TYPE_P2P}
|
||||
title={translateMessage("Ui.SetupWizard.SetupRemote.P2POption")}
|
||||
bind:value={userType}
|
||||
>
|
||||
{translateMessage(
|
||||
"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."
|
||||
)}
|
||||
|
||||
@@ -93,12 +93,14 @@ The mobile pass uses Obsidian's `app.emulateMobile(true)`, a 390 by 844 CSS-pixe
|
||||
|
||||
`test:e2e:obsidian:p2p-pane` starts one unconfigured CouchDB-only session and one configured P2P session. It proves that the command remains registered while the retired command, automatic pane, and unconfigured ribbon entry are absent. For the configured profile, it verifies that the ribbon and current status command reach the pane without opening it at start-up, checks its connection control and horizontal layout, and captures unobstructed desktop and mobile screenshots. It deliberately uses no relay or peer: replacement of the active replicator is covered by focused unit tests, the Deno and Compose CLI P2P lifecycle suite covers the headless transport, and `p2p-setup-uri-workflow` owns the visible transfer path between two real Obsidian sessions.
|
||||
|
||||
`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run.
|
||||
`test:e2e:obsidian:local-suite` builds the plug-in and, unless `LIVESYNC_CLI_COMMAND` selects an external CLI, the local LiveSync CLI. It then runs discovery, smoke, the onboarding invitation, Svelte dialogue mounting, settings UI, the Review Harness, the P2P status pane, Vault reflection, CouchDB upload and manual setup, CLI-to-Obsidian synchronisation, Object Storage upload and Setup URI round-trip, P2P Setup URI round-trip, startup scan, provisioned CouchDB Setup URI, two-vault synchronisation, Hidden File Sync, Customisation Sync, and setting Markdown export in sequence. Start the local CouchDB, MinIO, and P2P relay fixtures before running it, or use `test:e2e:obsidian:local-suite:services` to let the wrapper stop leftover fixtures, start fresh fixtures, and stop them again after the run.
|
||||
|
||||
`test:e2e:obsidian:couchdb-upload` reuses the CouchDB variables from `.test.env` or the process environment. It expects a reachable CouchDB service, creates a unique database, starts from configured plug-in data without the device-local compatibility marker, and verifies the copied-or-restored Vault explanation in the actual compatibility dialogue. It captures the summary and details, resumes explicitly, confirms that the marker was recorded, creates a note in real Obsidian, commits the note into the local database, runs one-shot synchronisation, and verifies that the remote database contains both the metadata document and its chunk documents.
|
||||
|
||||
The same workflow checks the two remote-activity status boundaries. It first holds a real CouchDB request at the selected fetch implementation and confirms that `🌐N` is visible while `📲` is absent. It then holds the real one-shot replication immediately before its replicator call, confirms that `📲` is visible while no physical request is active, releases it, and requires the finite and bounded activity counts to return to zero, the request and response counts to balance, and both indicators to disappear. Finally, it creates a remote-only chunk, holds the real on-demand fetch immediately before its remote call, makes the same logical active and idle assertions, and verifies that the fetched chunk is written into the local database. These gates make the active states deterministic without replacing the remote request or operation.
|
||||
|
||||
`test:e2e:obsidian:couchdb-manual-setup-workflow` follows the visible first-device onboarding path without a bootstrap Setup URI. It enters end-to-end encryption and CouchDB details, runs the read-only server-requirements check, requires the prepared fixture to pass without applying a server fix, and lets the onboarding connection test create the named database. After first-device Rebuild, it creates an ordinary note, asks that working device to generate a Setup URI for a second device, completes Fetch there, and verifies a bidirectional note round-trip. The workflow captures each decision point and the expanded server-check result; password controls remain visually masked.
|
||||
|
||||
If this status workflow fails while Obsidian is running, it writes a full-page screenshot and a JSON snapshot of the status text and counters under `/tmp/obsidian-livesync-e2e`. The dialogue-mount workflow leaves desktop and mobile screenshots for both representative Svelte routes, and the Hidden File Sync workflow captures the successfully displayed JSON Resolve dialogue before selecting an option. The suite therefore records representative evidence without capturing every interaction. Set `E2E_OBSIDIAN_DIAGNOSTICS_DIR` to use another directory.
|
||||
|
||||
The two-Vault workflow performs the missing-marker review once for each isolated Vault. Later process launches reuse the same profile-backed acknowledgement, rather than seeding a replacement or repeatedly applying a first-device decision. The Hidden File Sync scenario is narrower: it starts from an explicitly acknowledged marker because it tests consumer-owned hidden-file behaviour, JSON resolution, target filtering, and grouped mobile Notices rather than duplicating the compatibility workflow. After `app.emulateMobile(true)`, its fixture operations use the active DevTools renderer because Obsidian can remove desktop-only CLI commands in mobile mode.
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { evalObsidianJson } from "../runner/cli.ts";
|
||||
import {
|
||||
assertCouchDbReachable,
|
||||
deleteCouchDbDatabase,
|
||||
loadCouchDbConfig,
|
||||
makeUniqueDatabaseName,
|
||||
waitForCouchDbDocs,
|
||||
type CouchDbConfig,
|
||||
} from "../runner/couchdb.ts";
|
||||
import { discoverObsidianCli, requireObsidianBinary } from "../runner/environment.ts";
|
||||
import { assertEqual, pushLocalChanges, waitForLocalDatabaseEntry } from "../runner/liveSyncWorkflow.ts";
|
||||
import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts";
|
||||
import {
|
||||
acknowledgeDisabledOptionalFeatures,
|
||||
captureAndStartInitialisation,
|
||||
captureGuideDialogue,
|
||||
confirmFastFetch,
|
||||
confirmRebuild,
|
||||
enterSetupURI,
|
||||
finishInitialisation,
|
||||
generateSetupURIFromDevice,
|
||||
modalByTitle,
|
||||
resumeCompatibilityReviewIfShown,
|
||||
selectRadioOption,
|
||||
skipMissingRemoteConfiguration,
|
||||
type SetupArtifact,
|
||||
} from "../runner/setupUri.ts";
|
||||
import { captureObsidianPage, withObsidianPage } from "../runner/ui.ts";
|
||||
import { createTemporaryVault, type TemporaryVault } from "../runner/vault.ts";
|
||||
|
||||
process.env.E2E_OBSIDIAN_CLI_TIMEOUT_MS ??= "90000";
|
||||
process.env.E2E_OBSIDIAN_COUCHDB_TIMEOUT_MS ??= "30000";
|
||||
|
||||
const uiTimeoutMs = Number(process.env.E2E_OBSIDIAN_SETUP_URI_TIMEOUT_MS ?? 30000);
|
||||
const notePath = "E2E/manual-couchdb/from-first-device.md";
|
||||
const noteContent = "# Manual CouchDB setup\n\nThis note was sent by the manually configured first device.\n";
|
||||
const returnNotePath = "E2E/manual-couchdb/from-second-device.md";
|
||||
const returnNoteContent =
|
||||
"# Manual CouchDB return journey\n\nThis note returned through a Setup URI generated by the first device.\n";
|
||||
const captures = {
|
||||
scenario: "couchdb-manual-setup-workflow",
|
||||
guide: "couchdb-manual",
|
||||
} as const;
|
||||
|
||||
type RunnerContext = {
|
||||
binary: string;
|
||||
cliBinary: string;
|
||||
couchDb: CouchDbConfig;
|
||||
dbName: string;
|
||||
activeSessions: Set<ObsidianLiveSyncSession>;
|
||||
};
|
||||
|
||||
async function startUnconfiguredSession(
|
||||
context: RunnerContext,
|
||||
vault: TemporaryVault
|
||||
): Promise<ObsidianLiveSyncSession> {
|
||||
const session = await startObsidianLiveSyncSession({
|
||||
binary: context.binary,
|
||||
cliBinary: context.cliBinary,
|
||||
vault,
|
||||
startupGraceMs: Number(process.env.E2E_OBSIDIAN_STARTUP_GRACE_MS ?? 1000),
|
||||
});
|
||||
context.activeSessions.add(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
async function stopTrackedSession(context: RunnerContext, session: ObsidianLiveSyncSession): Promise<void> {
|
||||
if (!context.activeSessions.has(session)) return;
|
||||
await session.app.stop();
|
||||
context.activeSessions.delete(session);
|
||||
}
|
||||
|
||||
async function stopTrackedSessions(context: RunnerContext): Promise<void> {
|
||||
for (const session of [...context.activeSessions]) {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
}
|
||||
|
||||
async function captureFailure(session: ObsidianLiveSyncSession, label: string): Promise<void> {
|
||||
const screenshot = await captureObsidianPage(
|
||||
session.remoteDebuggingPort,
|
||||
`couchdb-manual-${label}-failure.png`,
|
||||
async () => undefined
|
||||
).catch(() => undefined);
|
||||
if (screenshot) {
|
||||
console.error(`Manual CouchDB failure screenshot: ${screenshot}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function enterManualCouchDBSettings(port: number, couchDb: CouchDbConfig, dbName: string): Promise<string[]> {
|
||||
const screenshots: string[] = [];
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const invitation = page.locator(".notice").filter({ hasText: "Welcome to Self-hosted LiveSync" });
|
||||
await invitation.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await invitation.locator(".sls-onboarding-invitation-action").click({ timeout: uiTimeoutMs });
|
||||
|
||||
const intro = modalByTitle(page, "Welcome to Self-hosted LiveSync");
|
||||
await intro.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await selectRadioOption(intro, "I am setting this up for the first time");
|
||||
await intro
|
||||
.getByRole("button", { name: "Yes, I want to set up a new synchronisation" })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
});
|
||||
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-connection-method.png", "Connection Method")
|
||||
);
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const method = modalByTitle(page, "Connection Method");
|
||||
await selectRadioOption(method, "Configure a remote manually");
|
||||
await method
|
||||
.getByRole("button", { name: "Proceed with manual configuration" })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
|
||||
const encryption = modalByTitle(page, "End-to-End Encryption");
|
||||
await encryption.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await encryption
|
||||
.locator("label.row")
|
||||
.filter({ hasText: "End-to-End Encryption" })
|
||||
.locator('input[type="checkbox"]')
|
||||
.first()
|
||||
.check({ timeout: uiTimeoutMs });
|
||||
await encryption
|
||||
.locator("label.row")
|
||||
.filter({ hasText: "Obfuscate Properties" })
|
||||
.locator('input[type="checkbox"]')
|
||||
.first()
|
||||
.check({ timeout: uiTimeoutMs });
|
||||
await encryption.locator('input[name="e2ee-passphrase"]').fill(randomBytes(24).toString("base64url"));
|
||||
});
|
||||
screenshots.push(await captureGuideDialogue(port, "guide-couchdb-manual-encryption.png", "End-to-End Encryption"));
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const encryption = modalByTitle(page, "End-to-End Encryption");
|
||||
await encryption.getByRole("button", { name: "Proceed", exact: true }).click({ timeout: uiTimeoutMs });
|
||||
});
|
||||
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-remote-selection.png", "Choose a synchronisation remote")
|
||||
);
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const remoteSelection = modalByTitle(page, "Choose a synchronisation remote");
|
||||
await selectRadioOption(remoteSelection, "CouchDB");
|
||||
await remoteSelection
|
||||
.getByRole("button", { name: "Continue to CouchDB setup", exact: true })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
|
||||
const couchDB = modalByTitle(page, "CouchDB Configuration");
|
||||
await couchDB.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await couchDB.locator('input[name="couchdb-url"]').fill(couchDb.uri);
|
||||
await couchDB.locator('input[name="couchdb-username"]').fill(couchDb.username);
|
||||
await couchDB.locator('input[name="couchdb-password"]').fill(couchDb.password);
|
||||
await couchDB.locator('input[name="couchdb-database"]').fill(dbName);
|
||||
});
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-connection-details.png", "CouchDB Configuration")
|
||||
);
|
||||
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const couchDB = modalByTitle(page, "CouchDB Configuration");
|
||||
await couchDB
|
||||
.getByRole("button", { name: "Check server requirements", exact: true })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
const summary = couchDB.locator(".check-results summary");
|
||||
await summary.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
await summary
|
||||
.filter({ hasText: /All checks passed successfully!|issue\(s\) detected!/u })
|
||||
.waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
const errors = couchDB.locator(".check-result.error");
|
||||
if ((await errors.count()) > 0) {
|
||||
const messages = await errors.locator(".message").allTextContents();
|
||||
throw new Error(`The documented CouchDB fixture failed its server requirements: ${messages.join(" | ")}`);
|
||||
}
|
||||
const details = couchDB.locator(".check-results details");
|
||||
if (!(await details.evaluate((element) => (element as HTMLDetailsElement).open))) {
|
||||
await summary.click({ timeout: uiTimeoutMs });
|
||||
}
|
||||
});
|
||||
screenshots.push(
|
||||
await captureGuideDialogue(port, "guide-couchdb-manual-server-requirements.png", "CouchDB Configuration")
|
||||
);
|
||||
|
||||
await withObsidianPage(port, async (page) => {
|
||||
const couchDB = modalByTitle(page, "CouchDB Configuration");
|
||||
await couchDB
|
||||
.getByRole("button", { name: "Create or connect to database and continue", exact: true })
|
||||
.click({ timeout: uiTimeoutMs });
|
||||
await modalByTitle(page, "Setup Complete: Preparing to Initialise Server").waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
});
|
||||
return screenshots;
|
||||
}
|
||||
|
||||
async function writeNoteViaObsidian(
|
||||
cliBinary: string,
|
||||
environment: NodeJS.ProcessEnv,
|
||||
path: string,
|
||||
content: string
|
||||
): Promise<void> {
|
||||
await evalObsidianJson<unknown>(
|
||||
cliBinary,
|
||||
[
|
||||
"(async()=>{",
|
||||
`const path=${JSON.stringify(path)};`,
|
||||
`const content=${JSON.stringify(content)};`,
|
||||
"const folder=path.split('/').slice(0,-1).join('/');",
|
||||
"if(folder&&!(await app.vault.adapter.exists(folder))) await app.vault.createFolder(folder);",
|
||||
"const existing=app.vault.getAbstractFileByPath(path);",
|
||||
"if(existing) await app.vault.modify(existing,content);",
|
||||
"else await app.vault.create(path,content);",
|
||||
"return JSON.stringify({ok:true});",
|
||||
"})()",
|
||||
].join(""),
|
||||
environment
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForVaultFile(
|
||||
vault: TemporaryVault,
|
||||
path: string,
|
||||
expected: string,
|
||||
timeoutMs = Number(process.env.E2E_OBSIDIAN_FILE_TIMEOUT_MS ?? 30000)
|
||||
): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
let lastContent = "";
|
||||
while (Date.now() < deadline) {
|
||||
try {
|
||||
lastContent = await readFile(join(vault.path, path), "utf8");
|
||||
if (lastContent === expected) return;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${path}. Last content:\n${lastContent}`);
|
||||
}
|
||||
|
||||
async function waitForRemoteEntry(context: RunnerContext, entry: { id: string; children: string[] }): Promise<void> {
|
||||
await waitForCouchDbDocs(context.couchDb, context.dbName, (docs) => {
|
||||
const ids = new Set(docs.map((doc) => doc._id));
|
||||
return ids.has(entry.id) && entry.children.every((childId) => ids.has(childId));
|
||||
});
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const binary = requireObsidianBinary();
|
||||
const cli = discoverObsidianCli();
|
||||
if (!cli.binary) {
|
||||
throw new Error(`Could not find obsidian-cli. Checked paths: ${cli.checked.join(", ")}`);
|
||||
}
|
||||
const couchDb = await loadCouchDbConfig();
|
||||
const dbName = makeUniqueDatabaseName(couchDb.dbPrefix, "manual-setup");
|
||||
const vaultA = await createTemporaryVault();
|
||||
const vaultB = await createTemporaryVault();
|
||||
const context: RunnerContext = {
|
||||
binary,
|
||||
cliBinary: cli.binary,
|
||||
couchDb,
|
||||
dbName,
|
||||
activeSessions: new Set(),
|
||||
};
|
||||
const screenshots: string[] = [];
|
||||
let secondDeviceArtifact: SetupArtifact | undefined;
|
||||
|
||||
try {
|
||||
await assertCouchDbReachable(couchDb);
|
||||
console.log(`Using Obsidian executable: ${binary}`);
|
||||
console.log(`Temporary Vault A: ${vaultA.path}`);
|
||||
console.log(`Temporary Vault B: ${vaultB.path}`);
|
||||
console.log(`CouchDB database to be created by the onboarding dialogue: ${dbName}`);
|
||||
|
||||
let session = await startUnconfiguredSession(context, vaultA);
|
||||
try {
|
||||
screenshots.push(...(await enterManualCouchDBSettings(session.remoteDebuggingPort, couchDb, dbName)));
|
||||
screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "new", captures));
|
||||
screenshots.push(await confirmRebuild(session.remoteDebuggingPort, captures));
|
||||
screenshots.push(await skipMissingRemoteConfiguration(session.remoteDebuggingPort, captures));
|
||||
screenshots.push(await acknowledgeDisabledOptionalFeatures(session.remoteDebuggingPort, captures));
|
||||
const state = await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv);
|
||||
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
|
||||
assertEqual(state.activeConfigurationId !== "", true, "Manual CouchDB setup did not activate a profile.");
|
||||
assertEqual(
|
||||
state.remoteConfigurationCount,
|
||||
1,
|
||||
"Manual CouchDB setup did not persist exactly one remote profile."
|
||||
);
|
||||
|
||||
await writeNoteViaObsidian(context.cliBinary, session.cliEnv, notePath, noteContent);
|
||||
const entry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, notePath);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForRemoteEntry(context, entry);
|
||||
|
||||
const generated = await generateSetupURIFromDevice(
|
||||
session.remoteDebuggingPort,
|
||||
randomBytes(24).toString("base64url"),
|
||||
captures
|
||||
);
|
||||
secondDeviceArtifact = generated.artifact;
|
||||
screenshots.push(...generated.screenshots);
|
||||
} catch (error) {
|
||||
await captureFailure(session, "first-device");
|
||||
throw error;
|
||||
} finally {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
|
||||
session = await startUnconfiguredSession(context, vaultB);
|
||||
try {
|
||||
if (!secondDeviceArtifact) {
|
||||
throw new Error("The manually configured first device did not generate a Setup URI.");
|
||||
}
|
||||
screenshots.push(
|
||||
await enterSetupURI(session.remoteDebuggingPort, "existing", secondDeviceArtifact, captures)
|
||||
);
|
||||
screenshots.push(await captureAndStartInitialisation(session.remoteDebuggingPort, "existing", captures));
|
||||
screenshots.push(...(await confirmFastFetch(session.remoteDebuggingPort, captures)));
|
||||
await finishInitialisation(session.remoteDebuggingPort, context.cliBinary, session.cliEnv);
|
||||
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForVaultFile(vaultB, notePath, noteContent);
|
||||
|
||||
await writeNoteViaObsidian(context.cliBinary, session.cliEnv, returnNotePath, returnNoteContent);
|
||||
const returnEntry = await waitForLocalDatabaseEntry(context.cliBinary, session.cliEnv, returnNotePath);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForRemoteEntry(context, returnEntry);
|
||||
} catch (error) {
|
||||
await captureFailure(session, "second-device");
|
||||
throw error;
|
||||
} finally {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
|
||||
session = await startUnconfiguredSession(context, vaultA);
|
||||
try {
|
||||
await resumeCompatibilityReviewIfShown(session.remoteDebuggingPort);
|
||||
await pushLocalChanges(context.cliBinary, session.cliEnv);
|
||||
await waitForVaultFile(vaultA, returnNotePath, returnNoteContent);
|
||||
} catch (error) {
|
||||
await captureFailure(session, "return-journey");
|
||||
throw error;
|
||||
} finally {
|
||||
await stopTrackedSession(context, session);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Manual CouchDB onboarding created and tested its database, generated a second-device Setup URI, and completed a bidirectional note round-trip. Screenshots: ${screenshots.join(", ")}`
|
||||
);
|
||||
} finally {
|
||||
await stopTrackedSessions(context).catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : error);
|
||||
});
|
||||
await vaultA.dispose();
|
||||
await vaultB.dispose();
|
||||
if (process.env.E2E_OBSIDIAN_KEEP_COUCHDB !== "true") {
|
||||
await deleteCouchDbDatabase(couchDb, dbName).catch((error: unknown) => {
|
||||
console.warn(error instanceof Error ? error.message : error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error: unknown) => {
|
||||
console.error(error instanceof Error ? error.stack : error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -196,14 +196,14 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string
|
||||
`setup-remote-selection-dialogue${mode === "mobile" ? "-mobile" : ""}.png`,
|
||||
async (page) => {
|
||||
const modal = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Enter Server Information" }),
|
||||
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
||||
});
|
||||
const dialogue = modal.locator(".dialog-host");
|
||||
await modal.waitFor({
|
||||
state: "visible",
|
||||
timeout: uiTimeoutMs,
|
||||
});
|
||||
for (const label of ["CouchDB", "S3/MinIO/R2 Object Storage", "Peer-to-Peer only"]) {
|
||||
for (const label of ["CouchDB", "S3-compatible Object Storage", "Peer-to-Peer (P2P)"]) {
|
||||
await dialogue.getByText(label, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs });
|
||||
}
|
||||
await dialogue
|
||||
@@ -222,7 +222,7 @@ async function verifyRemoteSelectionDialogue(mode: DialogueMode): Promise<string
|
||||
);
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const modal = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Enter Server Information" }),
|
||||
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
||||
});
|
||||
if (mode === "mobile") {
|
||||
const previousDialogue = await modal.locator(".modal").last().elementHandle();
|
||||
@@ -243,7 +243,7 @@ async function verifyCouchDBSettingsDialogue(mode: DialogueMode): Promise<string
|
||||
await openRemoteSelectionDialogue();
|
||||
await withObsidianPage(obsidianRemoteDebuggingPort(), async (page) => {
|
||||
const remoteSelection = page.locator(".modal-container").filter({
|
||||
has: page.locator(".modal-title").filter({ hasText: "Enter Server Information" }),
|
||||
has: page.locator(".modal-title").filter({ hasText: "Choose a synchronisation remote" }),
|
||||
});
|
||||
await remoteSelection
|
||||
.locator("label")
|
||||
|
||||
@@ -20,6 +20,10 @@ const testSteps: Step[] = [
|
||||
{ name: "P2P status pane", args: ["run", "test:e2e:obsidian:p2p-pane"] },
|
||||
{ name: "vault reflection", args: ["run", "test:e2e:obsidian:vault-reflection"] },
|
||||
{ name: "CouchDB upload", args: ["run", "test:e2e:obsidian:couchdb-upload"] },
|
||||
{
|
||||
name: "manual CouchDB setup workflow",
|
||||
args: ["run", "test:e2e:obsidian:couchdb-manual-setup-workflow"],
|
||||
},
|
||||
{
|
||||
name: "CLI to real Obsidian synchronisation",
|
||||
args: ["run", "test:e2e:obsidian:cli-to-obsidian-sync"],
|
||||
|
||||
@@ -13,6 +13,7 @@ const focusedScenarios = new Set([
|
||||
"p2p-pane",
|
||||
"vault-reflection",
|
||||
"couchdb-upload",
|
||||
"couchdb-manual-setup-workflow",
|
||||
"cli-to-obsidian-sync",
|
||||
"minio-upload",
|
||||
"object-storage-setup-uri-workflow",
|
||||
|
||||
Reference in New Issue
Block a user