mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-23 13:02:58 +00:00
Require explicit review for restored Vaults
This commit is contained in:
@@ -12,6 +12,11 @@ import {
|
||||
export type CompatibilityReviewSummaryAction = "details" | "resume" | "keep-paused" | false;
|
||||
export type CompatibilityReviewDetailsAction = "back" | false;
|
||||
|
||||
// Explicit flag-file recovery runs at priorities 5, 10, and 20. Present the
|
||||
// compatibility review only after those operations have completed; a recovery
|
||||
// handler which stops start-up also prevents this dialogue from competing with it.
|
||||
export const COMPATIBILITY_REVIEW_LAYOUT_PRIORITY = 30;
|
||||
|
||||
export interface CompatibilityReviewUi {
|
||||
showSummary(pause: CompatibilityPause): Promise<CompatibilityReviewSummaryAction>;
|
||||
showDetails(pause: CompatibilityPause): Promise<CompatibilityReviewDetailsAction>;
|
||||
@@ -139,7 +144,7 @@ export function useCompatibilityReview(core: LiveSyncCore, ui: CompatibilityRevi
|
||||
core.services.appLifecycle.onLayoutReady.addHandler(() => {
|
||||
fireAndForget(() => controller.openReview());
|
||||
return Promise.resolve(true);
|
||||
});
|
||||
}, COMPATIBILITY_REVIEW_LAYOUT_PRIORITY);
|
||||
core.services.appLifecycle.onUnload.addHandler(() => {
|
||||
controller.dispose();
|
||||
return Promise.resolve(true);
|
||||
|
||||
@@ -4,7 +4,11 @@ import {
|
||||
DATABASE_COMPATIBILITY_VERSION_KEY,
|
||||
legacyDatabaseCompatibilityVersionKey,
|
||||
} from "@/common/databaseCompatibility.ts";
|
||||
import { CompatibilityReviewController, type CompatibilityReviewUi } from "./compatibilityReview.ts";
|
||||
import {
|
||||
CompatibilityReviewController,
|
||||
type CompatibilityReviewUi,
|
||||
useCompatibilityReview,
|
||||
} from "./compatibilityReview.ts";
|
||||
|
||||
function migrationState(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
@@ -161,4 +165,26 @@ describe("compatibility review controller", () => {
|
||||
expect(fixture.ui.showSummary).not.toHaveBeenCalled();
|
||||
expect(fixture.ui.clearReminder).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("runs the review after the ordered red flag recovery handlers", () => {
|
||||
const onSettingLoaded = { addHandler: vi.fn() };
|
||||
const onLayoutReady = { addHandler: vi.fn() };
|
||||
const onUnload = { addHandler: vi.fn() };
|
||||
const core = {
|
||||
services: {
|
||||
appLifecycle: { onSettingLoaded, onLayoutReady, onUnload },
|
||||
API: { addCommand: vi.fn() },
|
||||
},
|
||||
} as never;
|
||||
const ui: CompatibilityReviewUi = {
|
||||
showSummary: vi.fn(),
|
||||
showDetails: vi.fn(),
|
||||
showReminder: vi.fn(),
|
||||
clearReminder: vi.fn(),
|
||||
};
|
||||
|
||||
useCompatibilityReview(core, ui);
|
||||
|
||||
expect(onLayoutReady.addHandler).toHaveBeenCalledWith(expect.any(Function), 30);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { CompatibilityPause, CompatibilityPauseReason } from "@/common/databaseCompatibility.ts";
|
||||
|
||||
export function compatibilityReviewSummaryMarkdown(pause: CompatibilityPause): string {
|
||||
const action = pause.resumable
|
||||
? "Before resuming, review the compatibility details and update Self-hosted LiveSync on every device which uses this remote database."
|
||||
: "This installation cannot safely acknowledge the detected state. Update Self-hosted LiveSync before attempting to synchronise again.";
|
||||
return `Remote synchronisation is paused on this device because its compatibility state requires attention.
|
||||
|
||||
${action}
|
||||
|
||||
Your automatic synchronisation preferences have not been changed. Closing this dialogue keeps synchronisation paused.`;
|
||||
}
|
||||
|
||||
function reasonMarkdown(reason: CompatibilityPauseReason): string {
|
||||
if (reason.source === "database-version") {
|
||||
if (reason.state === "upgrade") {
|
||||
return `- The last acknowledged internal database version was **${reason.acknowledgedVersion}** and this installation uses **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.state === "downgrade") {
|
||||
return `- This installation uses internal database version **${reason.currentVersion}**, but this device previously acknowledged newer version **${reason.acknowledgedVersion}**. An older installation must not resume synchronisation.`;
|
||||
}
|
||||
if (reason.state === "missing") {
|
||||
return `- No previously acknowledged internal database version was found for this existing Vault. This can happen when a Vault is copied or restored, or when it is opened with a new Obsidian profile. This installation uses version **${reason.currentVersion}**. An empty local database does not mean that it is safe to resume automatically.`;
|
||||
}
|
||||
return `- The saved internal database version marker is invalid. This installation uses version **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.source === "settings-schema") {
|
||||
if (reason.isFromFutureSchema) {
|
||||
return `- The saved settings use schema **${reason.sourceVersion}**, which is newer than schema **${reason.currentVersion}** supported by this installation.`;
|
||||
}
|
||||
return `- The settings were migrated from schema **${reason.sourceVersion}** to **${reason.currentVersion}** and require review before synchronisation resumes.`;
|
||||
}
|
||||
const escapedMessage = reason.message.replace(/[\\`*_{}[\]()<>#+.!|-]/gu, "\\$&");
|
||||
return `- An earlier compatibility review remains pending: ${escapedMessage}`;
|
||||
}
|
||||
|
||||
export function compatibilityReviewDetailsMarkdown(pause: CompatibilityPause): string {
|
||||
const resolution = pause.resumable
|
||||
? "After all devices have been updated, return to the compatibility review summary and explicitly resume synchronisation. The current internal version will only then be recorded as acknowledged."
|
||||
: "Install a compatible current version of Self-hosted LiveSync. This pause cannot be dismissed by the current installation.";
|
||||
return `## Why synchronisation is paused
|
||||
|
||||
${pause.reasons.map(reasonMarkdown).join("\n")}
|
||||
|
||||
## What the pause changes
|
||||
|
||||
- Remote replication is blocked before work begins.
|
||||
- Your saved automatic synchronisation preferences remain unchanged.
|
||||
- Closing either dialogue leaves the safety gate active.
|
||||
|
||||
## What to do next
|
||||
|
||||
${resolution}`;
|
||||
}
|
||||
@@ -1,70 +1,21 @@
|
||||
import { Notice } from "@/deps.ts";
|
||||
import type { Confirm } from "@vrtmrz/livesync-commonlib/compat/interfaces/Confirm";
|
||||
import type { CompatibilityPause, CompatibilityPauseReason } from "@/common/databaseCompatibility.ts";
|
||||
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
|
||||
import type {
|
||||
CompatibilityReviewDetailsAction,
|
||||
CompatibilityReviewSummaryAction,
|
||||
CompatibilityReviewUi,
|
||||
} from "./compatibilityReview.ts";
|
||||
import {
|
||||
compatibilityReviewDetailsMarkdown,
|
||||
compatibilityReviewSummaryMarkdown,
|
||||
} from "./compatibilityReviewMarkdown.ts";
|
||||
|
||||
const REVIEW_DETAILS = "Review compatibility details";
|
||||
const KEEP_PAUSED = "Keep synchronisation paused";
|
||||
const RESUME = "Resume synchronisation";
|
||||
const BACK = "Back to compatibility review";
|
||||
|
||||
function summaryMarkdown(pause: CompatibilityPause): string {
|
||||
const action = pause.resumable
|
||||
? "Before resuming, review the compatibility details and update Self-hosted LiveSync on every device which uses this remote database."
|
||||
: "This installation cannot safely acknowledge the detected state. Update Self-hosted LiveSync before attempting to synchronise again.";
|
||||
return `Remote synchronisation is paused on this device because its compatibility state requires attention.
|
||||
|
||||
${action}
|
||||
|
||||
Your automatic synchronisation preferences have not been changed. Closing this dialogue keeps synchronisation paused.`;
|
||||
}
|
||||
|
||||
function reasonMarkdown(reason: CompatibilityPauseReason): string {
|
||||
if (reason.source === "database-version") {
|
||||
if (reason.state === "upgrade") {
|
||||
return `- The last acknowledged internal database version was **${reason.acknowledgedVersion}** and this installation uses **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.state === "downgrade") {
|
||||
return `- This installation uses internal database version **${reason.currentVersion}**, but this device previously acknowledged newer version **${reason.acknowledgedVersion}**. An older installation must not resume synchronisation.`;
|
||||
}
|
||||
if (reason.state === "missing") {
|
||||
return `- No previously acknowledged internal database version was found for this existing Vault. This installation uses version **${reason.currentVersion}**.`;
|
||||
}
|
||||
return `- The saved internal database version marker is invalid. This installation uses version **${reason.currentVersion}**.`;
|
||||
}
|
||||
if (reason.source === "settings-schema") {
|
||||
if (reason.isFromFutureSchema) {
|
||||
return `- The saved settings use schema **${reason.sourceVersion}**, which is newer than schema **${reason.currentVersion}** supported by this installation.`;
|
||||
}
|
||||
return `- The settings were migrated from schema **${reason.sourceVersion}** to **${reason.currentVersion}** and require review before synchronisation resumes.`;
|
||||
}
|
||||
const escapedMessage = reason.message.replace(/[\\`*_{}[\]()<>#+.!|-]/gu, "\\$&");
|
||||
return `- An earlier compatibility review remains pending: ${escapedMessage}`;
|
||||
}
|
||||
|
||||
function detailsMarkdown(pause: CompatibilityPause): string {
|
||||
const resolution = pause.resumable
|
||||
? "After all devices have been updated, return to the compatibility review summary and explicitly resume synchronisation. The current internal version will only then be recorded as acknowledged."
|
||||
: "Install a compatible current version of Self-hosted LiveSync. This pause cannot be dismissed by the current installation.";
|
||||
return `## Why synchronisation is paused
|
||||
|
||||
${pause.reasons.map(reasonMarkdown).join("\n")}
|
||||
|
||||
## What the pause changes
|
||||
|
||||
- Remote replication is blocked before work begins.
|
||||
- Your saved automatic synchronisation preferences remain unchanged.
|
||||
- Closing either dialogue leaves the safety gate active.
|
||||
|
||||
## What to do next
|
||||
|
||||
${resolution}`;
|
||||
}
|
||||
|
||||
export class ObsidianCompatibilityReviewUi implements CompatibilityReviewUi {
|
||||
private reminder: Notice | undefined;
|
||||
|
||||
@@ -76,7 +27,7 @@ export class ObsidianCompatibilityReviewUi implements CompatibilityReviewUi {
|
||||
: ([REVIEW_DETAILS, KEEP_PAUSED] as const);
|
||||
const result = await this.confirm.confirmWithMessage(
|
||||
"Synchronisation paused for compatibility review",
|
||||
summaryMarkdown(pause),
|
||||
compatibilityReviewSummaryMarkdown(pause),
|
||||
[...buttons],
|
||||
KEEP_PAUSED,
|
||||
undefined,
|
||||
@@ -91,7 +42,7 @@ export class ObsidianCompatibilityReviewUi implements CompatibilityReviewUi {
|
||||
async showDetails(pause: CompatibilityPause): Promise<CompatibilityReviewDetailsAction> {
|
||||
const result = await this.confirm.confirmWithMessage(
|
||||
"Compatibility review details",
|
||||
detailsMarkdown(pause),
|
||||
compatibilityReviewDetailsMarkdown(pause),
|
||||
[BACK],
|
||||
BACK,
|
||||
undefined,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { CompatibilityPause } from "@/common/databaseCompatibility.ts";
|
||||
import { compatibilityReviewDetailsMarkdown } from "./compatibilityReviewMarkdown.ts";
|
||||
|
||||
describe("Obsidian compatibility review", () => {
|
||||
it("explains why a configured Vault can be missing its device-local acknowledgement", async () => {
|
||||
const pause: CompatibilityPause = {
|
||||
resumable: true,
|
||||
reasons: [
|
||||
{
|
||||
source: "database-version",
|
||||
state: "missing",
|
||||
currentVersion: 12,
|
||||
resumable: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const details = compatibilityReviewDetailsMarkdown(pause);
|
||||
expect(details).toContain("copied or restored");
|
||||
expect(details).toContain("new Obsidian profile");
|
||||
expect(details).toContain("does not mean that it is safe to resume automatically");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user