From 39ef429d7bcefdc062320b43200b40c683b9f54c Mon Sep 17 00:00:00 2001 From: vorotamoroz Date: Fri, 23 Oct 2026 16:07:35 +0000 Subject: [PATCH] Clarify device review guidance and follow dialogue windows --- docs/settings.md | 2 +- .../messages/LiveSyncProvisionalMessages.ts | 13 ++-- .../SettingDialogue/PaneRemoteConfig.ts | 2 +- .../dialogues/JoinedDevicesDialog.svelte | 2 +- src/serviceFeatures/useJoinedDevices.ts | 22 ++++-- test/e2e-obsidian/scripts/joined-devices.ts | 76 ++++++++++++++----- updates.md | 4 +- 7 files changed, 81 insertions(+), 40 deletions(-) diff --git a/docs/settings.md b/docs/settings.md index c2b33e09..1a81032c 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -236,7 +236,7 @@ The active connection type. This is automatically projected to the legacy config ### Joined devices -Open **Joined devices** to read the device records on the active CouchDB or Object Storage connection. The same dialogue is available from the command palette and version Notices. **Refresh** reads the remote again. This device appears first, including when its record is absent from the remote. Opening or refreshing this dialogue does not create a missing CouchDB database. +Choose **Review devices** under **Joined devices** to read the device records on the active CouchDB or Object Storage connection. The same **Review devices** action is available from the command palette and version Notices. **Refresh** reads the remote again. This device appears first, including when its record is absent from the remote. Opening or refreshing this dialogue does not create a missing CouchDB database. The list shows each device ID, recorded platform, plug-in version, app version, and last recorded connection time. Older clients may have incomplete information. These records do not establish whether a device is online. On a locked remote, a device outside the accepted list is shown as **Locked out**. diff --git a/src/common/messages/LiveSyncProvisionalMessages.ts b/src/common/messages/LiveSyncProvisionalMessages.ts index 44d4c2bc..054bb7ba 100644 --- a/src/common/messages/LiveSyncProvisionalMessages.ts +++ b/src/common/messages/LiveSyncProvisionalMessages.ts @@ -26,8 +26,8 @@ export const liveSyncProvisionalEnglishMessages = { "This remote does not currently have a record of joined devices.", "This list shows device information recorded by the remote. It does not show whether a device is online now.": "This list shows device information recorded by the remote. It does not show whether a device is online now.", - "This device is locked out. Use the existing recovery process to join again.": - "This device is locked out. Use the existing recovery process to join again.", + "This device has been locked out. Use the existing recovery process to join again.": + "This device has been locked out. Use the existing recovery process to join again.", "This device": "This device", "This remote has been cleaned. Use the existing recovery process to join devices again as needed.": "This remote has been cleaned. Use the existing recovery process to join devices again as needed.", @@ -42,10 +42,10 @@ export const liveSyncProvisionalEnglishMessages = { Refresh: "Refresh", "Plug-in version": "Plug-in version", Platform: "Platform", - "Other joined devices last reported an older plug-in version: ${versions}.": - "Other joined devices last reported an older plug-in version: ${versions}.", - "Other joined devices last reported a newer plug-in version: ${versions}.": - "Other joined devices last reported a newer plug-in version: ${versions}.", + "An older plug-in version was detected in other device records: ${versions}. We recommend checking whether those devices need updating.": + "An older plug-in version was detected in other device records: ${versions}. We recommend checking whether those devices need updating.", + "A newer plug-in version was detected in other device records: ${versions}. We recommend checking whether this device needs updating.": + "A newer plug-in version was detected in other device records: ${versions}. We recommend checking whether this device needs updating.", Open: "Open", "Not recorded": "Not recorded", "Not accepted": "Not accepted", @@ -58,6 +58,7 @@ export const liveSyncProvisionalEnglishMessages = { "Joined devices are available after database startup with an active CouchDB or Object Storage connection.": "Joined devices are available after database startup with an active CouchDB or Object Storage connection.", "Joined devices": "Joined devices", + "Review devices": "Review devices", "Forgetting…": "Forgetting…", "Forgetting the record does not delete Vault files or revoke credentials.": "Forgetting the record does not delete Vault files or revoke credentials.", diff --git a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts index 41fd1df8..81a68312 100644 --- a/src/modules/features/SettingDialogue/PaneRemoteConfig.ts +++ b/src/modules/features/SettingDialogue/PaneRemoteConfig.ts @@ -106,7 +106,7 @@ export function paneRemoteConfig( .setName($msg("Joined devices")) .setDesc($msg("Review device records for the active CouchDB or Object Storage connection.")) .addButton((button) => - button.setButtonText($msg("Open")).onClick(async () => { + button.setButtonText($msg("Review devices")).onClick(async () => { const result = await this.services.UI.commands.invoke(OPEN_JOINED_DEVICES, undefined); if (result.status === "unavailable" || result.value === "unavailable") { this.services.API.addLog( diff --git a/src/modules/services/LiveSyncUI/dialogues/JoinedDevicesDialog.svelte b/src/modules/services/LiveSyncUI/dialogues/JoinedDevicesDialog.svelte index 31b30356..91c54b0f 100644 --- a/src/modules/services/LiveSyncUI/dialogues/JoinedDevicesDialog.svelte +++ b/src/modules/services/LiveSyncUI/dialogues/JoinedDevicesDialog.svelte @@ -176,7 +176,7 @@ {#if device.self && device.lockedOut}

{translate( - "This device is locked out. Use the existing recovery process to join again." + "This device has been locked out. Use the existing recovery process to join again." )}

{/if} diff --git a/src/serviceFeatures/useJoinedDevices.ts b/src/serviceFeatures/useJoinedDevices.ts index 37d50b0f..c5fcc759 100644 --- a/src/serviceFeatures/useJoinedDevices.ts +++ b/src/serviceFeatures/useJoinedDevices.ts @@ -62,14 +62,20 @@ export function useJoinedDevicesFeature(host: JoinedDevicesHost, notices: Obsidi notices.setItem(noticeGroup, direction, { message: direction === "newer" - ? $msg("Other joined devices last reported a newer plug-in version: ${versions}.", { - versions: versions.join(", "), - }) - : $msg("Other joined devices last reported an older plug-in version: ${versions}.", { - versions: versions.join(", "), - }), + ? $msg( + "A newer plug-in version was detected in other device records: ${versions}. We recommend checking whether this device needs updating.", + { + versions: versions.join(", "), + } + ) + : $msg( + "An older plug-in version was detected in other device records: ${versions}. We recommend checking whether those devices need updating.", + { + versions: versions.join(", "), + } + ), action: { - label: $msg("Joined devices"), + label: $msg("Review devices"), onSelect: () => { if (!disposed && identity === remoteIdentity(services.setting.currentSettings())) { void services.UI.commands.invoke(OPEN_JOINED_DEVICES, undefined); @@ -190,7 +196,7 @@ export function useJoinedDevicesFeature(host: JoinedDevicesHost, notices: Obsidi services.appLifecycle.onInitialise.addHandler(() => { services.API.addCommand({ id: "joined-devices", - name: $msg("Joined devices"), + name: $msg("Review devices"), icon: "monitor-smartphone", checkCallback: (checking) => { if (!available()) return false; diff --git a/test/e2e-obsidian/scripts/joined-devices.ts b/test/e2e-obsidian/scripts/joined-devices.ts index d5736139..15d496e8 100644 --- a/test/e2e-obsidian/scripts/joined-devices.ts +++ b/test/e2e-obsidian/scripts/joined-devices.ts @@ -1,3 +1,5 @@ +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; import { DEVICE_ID_PREFERRED, MILESTONE_DOCID, NODEINFO_DOCID } from "@vrtmrz/livesync-commonlib/compat/common/types"; import { evalObsidianJson } from "../runner/cli.ts"; import { @@ -27,12 +29,7 @@ import { } from "@vrtmrz/obsidian-test-session"; import { iPhoneSafeArea, setObsidianMobileTestMode } from "../runner/mobileUi.ts"; import { startObsidianLiveSyncSession, type ObsidianLiveSyncSession } from "../runner/session.ts"; -import { - captureObsidianDialogue, - openLiveSyncSettings, - waitForVisibleObsidianDialogue, - withObsidianPage, -} from "../runner/ui.ts"; +import { openLiveSyncSettings, waitForVisibleObsidianDialogue, withObsidianPage } from "../runner/ui.ts"; import { createTemporaryVault } from "../runner/vault.ts"; import type { Locator, Page } from "playwright"; @@ -161,7 +158,9 @@ function createFixture(self: SelfDeviceInfo): DeviceFixture { async function openFromRemoteSettings(page: Page): Promise { const settings = await openLiveSyncSettings(page, uiTimeoutMs); const remoteConfiguration = await settings.openPage("Remote Configuration"); - await remoteConfiguration.getByRole("button", { name: "Open", exact: true }).click({ timeout: uiTimeoutMs }); + await remoteConfiguration + .getByRole("button", { name: "Review devices", exact: true }) + .click({ timeout: uiTimeoutMs }); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); await dialogue.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); return dialogue; @@ -185,7 +184,7 @@ async function assertInitialDialogue(dialogue: Locator, self: SelfDeviceInfo, pa await selfCard.getByText("Locked out", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); await selfCard.getByText("Not accepted", { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); await selfCard - .getByText("This device is locked out.", { exact: false }) + .getByText("This device has been locked out.", { exact: false }) .waitFor({ state: "visible", timeout: uiTimeoutMs }); await assertDetail(selfCard, "Platform", self.platform); await assertDetail(selfCard, "Plug-in version", self.pluginVersion); @@ -339,6 +338,27 @@ async function dismissNotices(page: Page): Promise { assertEqual(await notices.count(), 0, "Notices still obscured the dialogue screenshot."); } +async function captureJoinedDevices( + port: number, + filename: string, + assertReady: (page: Page) => Promise +): Promise { + const outputDirectory = process.env.E2E_OBSIDIAN_DIAGNOSTICS_DIR ?? "/tmp/obsidian-livesync-e2e"; + await mkdir(outputDirectory, { recursive: true }); + await withObsidianPage(port, async (page) => { + try { + const dialogue = await assertReady(page); + await dialogue.page().screenshot({ path: join(outputDirectory, filename), fullPage: true }); + } catch (error) { + const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", 500).catch(() => undefined); + const failurePath = join(outputDirectory, filename.replace(/\.png$/u, ".failure.png")); + await (dialogue?.page() ?? page).screenshot({ path: failurePath, fullPage: true }).catch(() => undefined); + console.error(`Joined devices UI failure screenshot: ${failurePath}`); + throw error; + } + }); +} + async function main(): Promise { const binary = requireObsidianBinary(); const cli = discoverObsidianCli(); @@ -381,24 +401,35 @@ async function main(): Promise { assert(Boolean(preparedMilestone._rev), "Remote preparation did not create a revisioned milestone document."); await putCouchDbDocument(couchDb, dbName, { ...fixture.milestone, _rev: preparedMilestone._rev }); - await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-notice.png", async (page) => { - await page.setViewportSize({ width: 1280, height: 1100 }); + await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-notice.png", async (page) => { const dialogue = await openFromRemoteSettings(page); - await assertInitialDialogue(dialogue, self, page); - await page + const dialoguePage = dialogue.page(); + await dialoguePage.setViewportSize({ width: 1280, height: 1100 }); + await assertInitialDialogue(dialogue, self, dialoguePage); + await dialoguePage .locator(".notice") - .getByText("Other joined devices last reported a newer plug-in version: 999.0.0.", { exact: true }) + .getByText( + "A newer plug-in version was detected in other device records: 999.0.0. We recommend checking whether this device needs updating.", + { exact: true } + ) .waitFor({ state: "visible", timeout: uiTimeoutMs }); + return dialogue; }); - await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-desktop.png", async (page) => { + await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-desktop.png", async (page) => { const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); await dialogue.getByRole("button", { name: "Close", exact: true }).click({ timeout: uiTimeoutMs }); - await page.locator(".notice").getByRole("button", { name: "Joined devices", exact: true }).first().click(); + await dialogue + .page() + .locator(".notice") + .getByRole("button", { name: "Review devices", exact: true }) + .first() + .click(); const reopened = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); await reopened.getByText(targetId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); - await dismissNotices(page); + await dismissNotices(reopened.page()); + return reopened; }); - await captureObsidianDialogue( + await captureJoinedDevices( session.remoteDebuggingPort, "joined-devices-forget-confirmation.png", async (page) => { @@ -406,12 +437,14 @@ async function main(): Promise { const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); await showForgetConfirmation(dialogue); await deviceCard(dialogue, targetId).locator(".forget-confirmation").scrollIntoViewIfNeeded(); + return dialogue; } ); - await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-after-forget.png", async (page) => { + await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-after-forget.png", async (page) => { const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); await confirmForgetTarget(dialogue); await dialogue.locator(".modal-content").evaluate((element) => element.scrollTo(0, 0)); + return dialogue; }); await withObsidianPage(session.remoteDebuggingPort, async (page) => { const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); @@ -423,7 +456,7 @@ async function main(): Promise { await setObsidianMobileTestMode(session.remoteDebuggingPort, true, uiTimeoutMs); await waitForLiveSyncCoreReady(cli.binary, session.cliEnv); - await captureObsidianDialogue(session.remoteDebuggingPort, "joined-devices-mobile.png", async (page) => { + await captureJoinedDevices(session.remoteDebuggingPort, "joined-devices-mobile.png", async (page) => { const opened = await page.evaluate((id) => { const app = ( window as typeof window & { @@ -435,13 +468,14 @@ async function main(): Promise { assert(opened, "The Joined devices command did not open the dialogue."); const dialogue = await waitForVisibleObsidianDialogue(page, "Joined devices", uiTimeoutMs); await dialogue.getByText(retainedId, { exact: true }).waitFor({ state: "visible", timeout: uiTimeoutMs }); - await assertScrollableMobileDialogue(page, dialogue); + await assertScrollableMobileDialogue(dialogue.page(), dialogue); assertEqual( await dialogue.locator(".device-card").count(), 2, "The refreshed mobile list did not retain both remaining devices." ); - await dismissNotices(page); + await dismissNotices(dialogue.page()); + return dialogue; }); console.log(`Joined devices scenario passed against CouchDB database ${dbName}.`); diff --git a/updates.md b/updates.md index a17be86e..534eac87 100644 --- a/updates.md +++ b/updates.md @@ -27,9 +27,9 @@ Earlier releases remain available in the 1.0 release history, the 1.0 preview hi #### Added -- We can review participating devices through **Joined devices** in Remote Configuration, the command palette, or a version Notice. +- We can choose **Review devices** in Remote Configuration, the command palette, or a version Notice to review participating devices. - Our device appears first, with its lockout state. Other records show the platform, plug-in version, app version, and last recorded connection time when available. - - Advisory Notices help us find devices which last reported newer or older plug-in versions. + - Advisory Notices highlight newer or older plug-in versions detected in device records and help us check which devices may need updating. - We can forget records belonging to lost or retired devices while preserving the remote lock state and other devices. - Forgetting a record does not delete Vault files or revoke credentials. A returning device can register again on an unlocked remote.