Merge current main and Maintenance keys into settings localisation

This commit is contained in:
vorotamoroz committed 2026-10-13 11:01:31 +00:00
commit d346bb27ab
138 files changed
+12767 -1856

No files matched your search

@@ -569,6 +569,7 @@ export class ObsidianLiveSyncSettingTab extends PluginSettingTab {
}
}
// Internal Metadata encryption affects future Metadata writes and is not a rebuild requirement.
isNeedRebuildLocal() {
return this.isSomeDirty([
"useIndexedDBAdapter",
@@ -24,7 +24,9 @@ export function paneMaintenance(
paneEl,
"div",
{
text: $msg("The remote database is locked for synchronization to prevent vault corruption because this device isn't marked as 'resolved'. Please backup your vault, reset the local database, and select 'Mark this device as resolved'. This warning will persist until the device is confirmed as resolved by replication."),
text: $msg(
"The remote database is locked for synchronisation to prevent vault corruption because this device is not marked as 'resolved'. Please back up your vault, reset the local database, and select 'Mark this device as resolved'. This warning will persist until replication confirms the device is resolved."
),
cls: "op-warn",
},
(c) => {
@@ -32,7 +34,7 @@ export function paneMaintenance(
c,
"button",
{
text: $msg("I've made a backup, mark this device 'resolved'"),
text: $msg("I have made a backup, mark this device as resolved"),
cls: "mod-warning",
},
(e) => {
@@ -51,7 +53,9 @@ export function paneMaintenance(
paneEl,
"div",
{
text: $msg("To prevent unwanted vault corruption, the remote database has been locked for synchronization. (This device is marked 'resolved') When all your devices are marked 'resolved', unlock the database. This warning kept showing until confirming the device is resolved by the replication"),
text: $msg(
"To prevent unwanted vault corruption, the remote database has been locked for synchronisation. (This device is marked as 'resolved'.) When all your devices are marked as 'resolved', unlock the database. This warning will continue to appear until replication confirms the device is resolved."
),
cls: "op-warn",
},
(c) =>
@@ -59,7 +63,7 @@ export function paneMaintenance(
c,
"button",
{
text: $msg("I'm ready, unlock the database"),
text: $msg("I am ready, unlock the database"),
cls: "mod-warning",
},
(e) => {
@@ -293,97 +297,101 @@ export function paneMaintenance(
// }
// );
void addPanel(paneEl, $msg("Rebuilding Operations (Remote Only)"), () => {}, this.onlyOnCouchDBOrMinIO).then((paneEl) => {
new Setting(paneEl)
.setName($msg("Perform cleanup"))
.setDesc(
$msg(
"Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."
void addPanel(paneEl, $msg("Rebuilding Operations (Remote Only)"), () => {}, this.onlyOnCouchDBOrMinIO).then(
(paneEl) => {
new Setting(paneEl)
.setName($msg("Perform cleanup"))
.setDesc(
$msg(
"Reduces storage space by discarding all non-latest revisions. This requires the same amount of free space on the remote server and the local client."
)
)
)
.addButton((button) =>
button
.setButtonText($msg("Perform"))
.setDisabled(false)
.onClick(async () => {
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction");
if (await replicator.compactRemote(this.editingSettings)) {
Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction");
} else {
Logger(`Cleanup has been failed!`, LOG_LEVEL_NOTICE, "compaction");
}
})
)
.addOnUpdate(this.onlyOnCouchDB);
.addButton((button) =>
button
.setButtonText($msg("Perform"))
.setDisabled(false)
.onClick(async () => {
const replicator = this.core.replicator as LiveSyncCouchDBReplicator;
Logger(`Cleanup has been began`, LOG_LEVEL_NOTICE, "compaction");
if (await replicator.compactRemote(this.editingSettings)) {
Logger(`Cleanup has been completed!`, LOG_LEVEL_NOTICE, "compaction");
} else {
Logger(`Cleanup has been failed!`, LOG_LEVEL_NOTICE, "compaction");
}
})
)
.addOnUpdate(this.onlyOnCouchDB);
new Setting(paneEl)
.setName($msg("Overwrite remote"))
.setDesc($msg("Overwrite remote with local DB and passphrase."))
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Send"))
.setDisabled(false)
.onClick(async () => {
await this.rebuildDB("remoteOnly");
})
);
new Setting(paneEl)
.setName($msg("Overwrite remote"))
.setDesc($msg("Overwrite remote with local DB and passphrase."))
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Send"))
.setDisabled(false)
.onClick(async () => {
await this.rebuildDB("remoteOnly");
})
);
new Setting(paneEl)
.setName($msg("Reset all journal counter"))
.setDesc($msg("Initialise all journal history, On the next sync, every item will be received and sent."))
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Reset all"))
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().resetCheckpointInfo();
Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(this.onlyOnMinIO);
new Setting(paneEl)
.setName($msg("Reset all journal counter"))
.setDesc(
$msg("Initialise all journal history, On the next sync, every item will be received and sent.")
)
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Reset all"))
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().resetCheckpointInfo();
Logger(`Journal exchange history has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(this.onlyOnMinIO);
new Setting(paneEl)
.setName($msg("Purge all journal counter"))
.setDesc($msg("Purge all download/upload cache."))
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Reset all"))
.setDisabled(false)
.onClick(() => {
this.getMinioJournalSyncClient().resetAllCaches();
Logger(`Journal download/upload cache has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(this.onlyOnMinIO);
new Setting(paneEl)
.setName($msg("Purge all journal counter"))
.setDesc($msg("Purge all download/upload cache."))
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Reset all"))
.setDisabled(false)
.onClick(() => {
this.getMinioJournalSyncClient().resetAllCaches();
Logger(`Journal download/upload cache has been cleared.`, LOG_LEVEL_NOTICE);
})
)
.addOnUpdate(this.onlyOnMinIO);
new Setting(paneEl)
.setName($msg("Fresh Start Wipe"))
.setDesc($msg("Delete all data on the remote server."))
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Delete"))
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({
...info,
receivedFiles: new Set(),
knownIDs: new Set(),
lastLocalSeq: 0,
sentIDs: new Set(),
sentFiles: new Set(),
}));
const reset = await this.resetRemoteBucket();
Logger(
reset
? `Deleted all data on remote server`
: `Fresh Start Wipe did not complete. Keep all synchronising devices stopped and run it again.`,
LOG_LEVEL_NOTICE
);
})
)
.addOnUpdate(this.onlyOnMinIO);
});
new Setting(paneEl)
.setName($msg("Fresh Start Wipe"))
.setDesc($msg("Delete all data on the remote server."))
.addButton((button) =>
setButtonDestructiveState(button)
.setButtonText($msg("Delete"))
.setDisabled(false)
.onClick(async () => {
await this.getMinioJournalSyncClient().updateCheckPointInfo((info) => ({
...info,
receivedFiles: new Set(),
knownIDs: new Set(),
lastLocalSeq: 0,
sentIDs: new Set(),
sentFiles: new Set(),
}));
const reset = await this.resetRemoteBucket();
Logger(
reset
? `Deleted all data on remote server`
: `Fresh Start Wipe did not complete. Keep all synchronising devices stopped and run it again.`,
LOG_LEVEL_NOTICE
);
})
)
.addOnUpdate(this.onlyOnMinIO);
}
);
void addPanel(paneEl, $msg("Reset")).then((paneEl) => {
new Setting(paneEl)
@@ -47,6 +47,12 @@ function getSettingsFromEditingSettings(editingSettings: AllSettings): ObsidianL
}
return workObj;
}
function syncIdDerivationSettings(target: Partial<ObsidianLiveSyncSettings>, source: ObsidianLiveSyncSettings): void {
target.idDerivationVersion = source.idDerivationVersion;
target.idDerivationKey = source.idDerivationKey;
}
function createRemoteConfigurationId(): string {
return `remote-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
}
@@ -116,7 +122,28 @@ export function paneRemoteConfig(
.onClick(async () => {
const setupManager = this.core.getModule(SetupManager);
const originalSettings = getSettingsFromEditingSettings(this.editingSettings);
await setupManager.onlyE2EEConfiguration(UserMode.Update, originalSettings);
const originalIdDerivationVersion = this.core.settings.idDerivationVersion;
const originalIdDerivationKey = this.core.settings.idDerivationKey;
const applied = await setupManager.onlyE2EEConfiguration(UserMode.Update, originalSettings);
if (applied) {
this.editingSettings.encryptInternalMetadata =
this.core.settings.encryptInternalMetadata;
if (this.initialSettings) {
this.initialSettings.encryptInternalMetadata =
this.core.settings.encryptInternalMetadata;
}
this.requestUpdate();
}
if (
this.core.settings.idDerivationVersion !== originalIdDerivationVersion ||
this.core.settings.idDerivationKey !== originalIdDerivationKey
) {
syncIdDerivationSettings(this.editingSettings, this.core.settings);
if (this.initialSettings) {
syncIdDerivationSettings(this.initialSettings, this.core.settings);
}
this.requestUpdate();
}
updateE2EESummary();
})
.setButtonText($msg("Configure"))
@@ -155,9 +182,11 @@ export function paneRemoteConfig(
const currentConfigs = cloneRemoteConfigurations(this.core.settings.remoteConfigurations);
this.editingSettings.remoteConfigurations = currentConfigs;
this.editingSettings.activeConfigurationId = this.core.settings.activeConfigurationId;
syncIdDerivationSettings(this.editingSettings, this.core.settings);
if (this.initialSettings) {
this.initialSettings.remoteConfigurations = cloneRemoteConfigurations(currentConfigs);
this.initialSettings.activeConfigurationId = this.core.settings.activeConfigurationId;
syncIdDerivationSettings(this.initialSettings, this.core.settings);
}
};
const persistRemoteConfigurations = async (synchroniseActiveRemote: boolean = false) => {
@@ -243,7 +272,10 @@ export function paneRemoteConfig(
...DEFAULT_SETTINGS,
encrypt: this.editingSettings.encrypt,
usePathObfuscation: this.editingSettings.usePathObfuscation,
encryptInternalMetadata: this.editingSettings.encryptInternalMetadata,
passphrase: this.editingSettings.passphrase,
idDerivationVersion: this.editingSettings.idDerivationVersion,
idDerivationKey: this.editingSettings.idDerivationKey,
configPassphraseStore: this.editingSettings.configPassphraseStore,
});
const addRemoteConfiguration = async () => {
@@ -2,6 +2,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
const runtime = vi.hoisted(() => ({
buttonClasses: [] as string[],
clickHandlers: [] as Array<() => Promise<void> | void>,
panels: [] as Array<{ destroy: ReturnType<typeof vi.fn> }>,
settingClasses: [] as string[],
}));
@@ -51,7 +52,8 @@ vi.mock("./LiveSyncSetting.ts", () => ({
setDestructive() {
return this;
},
onClick() {
onClick(callback: () => Promise<void> | void) {
runtime.clickHandlers.push(callback);
return this;
},
setButtonText() {
@@ -97,6 +99,7 @@ vi.mock("@vrtmrz/livesync-commonlib/compat/common/ConnectionString", () => ({
},
}));
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemote.svelte", () => ({ default: {} }));
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteE2EE.svelte", () => ({ default: {} }));
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteCouchDB.svelte", () => ({ default: {} }));
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteBucket.svelte", () => ({ default: {} }));
vi.mock("@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte", () => ({ default: {} }));
@@ -114,6 +117,7 @@ function createPanelElement(): HTMLElement {
afterEach(() => {
runtime.buttonClasses.length = 0;
runtime.clickHandlers.length = 0;
runtime.panels.length = 0;
runtime.settingClasses.length = 0;
vi.clearAllMocks();
@@ -148,4 +152,93 @@ describe("paneRemoteConfig", () => {
expect(runtime.panels[0].destroy).toHaveBeenCalledOnce();
});
it("applies an internal Metadata preference change without scheduling setup initialisation", async () => {
const originalSettings = {
encrypt: true,
passphrase: "passphrase",
E2EEAlgorithm: "v2",
usePathObfuscation: true,
encryptInternalMetadata: false,
remoteConfigurations: {},
};
const setupManager = {
onlyE2EEConfiguration: vi.fn(async () => {
host.core.settings.encryptInternalMetadata = true;
return true;
}),
};
const host = {
editingSettings: { ...originalSettings },
initialSettings: { ...originalSettings },
core: {
settings: { ...originalSettings },
getModule: vi.fn(() => setupManager),
},
lifetimeComponent: { register: vi.fn() },
requestUpdate: vi.fn(),
};
const addPanel = vi.fn((_parent: HTMLElement, heading: string) => ({
then(callback: (paneEl: HTMLElement) => void) {
if (heading === "E2EE Configuration") {
callback(createPanelElement());
}
},
}));
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
await runtime.clickHandlers[0]();
expect(setupManager.onlyE2EEConfiguration).toHaveBeenCalledOnce();
expect(host.editingSettings.encryptInternalMetadata).toBe(true);
expect(host.initialSettings.encryptInternalMetadata).toBe(true);
expect(host.requestUpdate).toHaveBeenCalledOnce();
});
it("copies applied ID derivation settings into both dialogue buffers", async () => {
const nextIdKey = "ab".repeat(32);
const originalSettings = {
encrypt: true,
passphrase: "passphrase",
E2EEAlgorithm: "v2",
usePathObfuscation: true,
encryptInternalMetadata: false,
idDerivationVersion: 0,
idDerivationKey: "",
remoteConfigurations: {},
};
const setupManager = {
onlyE2EEConfiguration: vi.fn(() => {
host.core.settings.idDerivationVersion = 1;
host.core.settings.idDerivationKey = nextIdKey;
return Promise.resolve(false);
}),
};
const host = {
editingSettings: { ...originalSettings },
initialSettings: { ...originalSettings },
core: {
settings: { ...originalSettings },
getModule: vi.fn(() => setupManager),
},
lifetimeComponent: { register: vi.fn() },
requestUpdate: vi.fn(),
};
const addPanel = vi.fn((_parent: HTMLElement, heading: string) => ({
then(callback: (paneEl: HTMLElement) => void) {
if (heading === "E2EE Configuration") {
callback(createPanelElement());
}
},
}));
paneRemoteConfig.call(host as never, {} as HTMLElement, { addPanel } as never);
await runtime.clickHandlers[0]();
expect(host.editingSettings.idDerivationVersion).toBe(1);
expect(host.editingSettings.idDerivationKey).toBe(nextIdKey);
expect(host.initialSettings.idDerivationVersion).toBe(1);
expect(host.initialSettings.idDerivationKey).toBe(nextIdKey);
expect(host.requestUpdate).toHaveBeenCalledOnce();
});
});
@@ -32,7 +32,9 @@ export function paneSelector(this: ObsidianLiveSyncSettingTab, paneEl: HTMLEleme
const nonSyncFilesSetting = new Setting(paneEl)
.setName($msg("Non-Synchronising files"))
.setDesc($msg("(RegExp) If this is set, any changes to local and remote files that match this will be skipped."));
.setDesc(
$msg("(RegExp) If this is set, any changes to local and remote files that match this will be skipped.")
);
const nonSyncFilesControl = mount(MultipleRegExpControl, {
target: nonSyncFilesSetting.controlEl,
@@ -68,6 +68,7 @@ export function getE2EEConfigSummary(setting: ObsidianLiveSyncSettings, showAdva
export function getSummaryFromPartialSettings(setting: Partial<ObsidianLiveSyncSettings>, showAdvanced = false) {
const outputSummary: Record<string, string> = {};
for (const key of Object.keys(setting) as (keyof ObsidianLiveSyncSettings)[]) {
if (key === "idDerivationKey" || key === "encryptedIdDerivationKey") continue;
const config = getConfig(key as AllSettingItemKey);
if (!config) continue;
if (config.isAdvanced && !showAdvanced) continue;