Merge branch 'main' into feat_tweak_auto_adjust

This commit is contained in:
vorotamoroz
2026-05-20 11:01:51 +01:00
13 changed files with 613 additions and 359 deletions

142
src/common/reportTool.ts Normal file
View File

@@ -0,0 +1,142 @@
import { REMOTE_COUCHDB, REMOTE_MINIO } from "@lib/common/models/setting.const";
import type { ObsidianLiveSyncSettings } from "@lib/common/models/setting.type";
import { generateCredentialObject } from "@lib/replication/httplib";
import { parseHeaderValues } from "@lib/common/utils";
import { requestToCouchDBWithCredentials } from "./utils";
import { LOG_LEVEL_VERBOSE, Logger } from "@lib/common/logger";
import { DEFAULT_SETTINGS } from "@lib/common/models/setting.const.defaults";
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
import { compatGlobal } from "@lib/common/coreEnvFunctions";
import { manifestVersion, packageVersion } from "@lib/common/coreEnvVars";
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
function redactObject(obj: Record<string, any>, dotted: string, redactedValue = "REDACTED") {
const keys = dotted.split(".");
let current = obj;
for (let i = 0; i < keys.length - 1; i++) {
const key = keys[i];
if (!(key in current)) {
current[key] = {} as Record<string, any>;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
current = current[key];
}
const lastKey = keys[keys.length - 1];
if (lastKey in current) {
current[lastKey] = redactedValue;
}
return obj;
}
export async function generateReport(settings: ObsidianLiveSyncSettings, core: LiveSyncBaseCore) {
let responseConfig: Record<string, any> = {};
const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷";
if (settings.remoteType == REMOTE_COUCHDB) {
try {
const credential = generateCredentialObject(settings);
const customHeaders = parseHeaderValues(settings.couchDB_CustomHeaders);
const r = await requestToCouchDBWithCredentials(
settings.couchDB_URI,
credential,
window.origin,
undefined,
undefined,
undefined,
customHeaders
);
responseConfig = r.json as Record<string, any>;
redactObject(responseConfig, "couch_httpd_auth.secret");
redactObject(responseConfig, "couch_httpd_auth.authentication_db");
redactObject(responseConfig, "couch_httpd_auth.authentication_redirect");
redactObject(responseConfig, "couchdb.uuid");
redactObject(responseConfig, "admins");
redactObject(responseConfig, "users");
redactObject(responseConfig, "chttpd_auth.secret");
delete responseConfig["jwt_keys"];
} catch (ex) {
Logger(ex, LOG_LEVEL_VERBOSE);
responseConfig = {
error: "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour.",
};
}
} else if (settings.remoteType == REMOTE_MINIO) {
responseConfig = { error: "Object Storage Synchronisation" };
//
}
const defaultKeys = Object.keys(DEFAULT_SETTINGS) as (keyof ObsidianLiveSyncSettings)[];
const pluginConfig = JSON.parse(JSON.stringify(settings)) as ObsidianLiveSyncSettings;
const pluginKeys = Object.keys(pluginConfig);
for (const key of pluginKeys) {
if (defaultKeys.includes(key as keyof ObsidianLiveSyncSettings)) continue;
delete pluginConfig[key as keyof ObsidianLiveSyncSettings];
}
pluginConfig.couchDB_DBNAME = REDACTED;
pluginConfig.couchDB_PASSWORD = REDACTED;
const scheme = pluginConfig.couchDB_URI.startsWith("http:")
? "(HTTP)"
: pluginConfig.couchDB_URI.startsWith("https:")
? "(HTTPS)"
: "";
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI) ? "cloudant" : `self-hosted${scheme}`;
pluginConfig.couchDB_USER = REDACTED;
pluginConfig.passphrase = REDACTED;
pluginConfig.encryptedPassphrase = REDACTED;
pluginConfig.encryptedCouchDBConnection = REDACTED;
pluginConfig.accessKey = REDACTED;
pluginConfig.secretKey = REDACTED;
const redact = (source: string) => `${REDACTED}(${source.length} letters)`;
const toSchemeOnly = (uri: string) => {
try {
return `${new URL(uri).protocol}//`;
} catch {
const matched = uri.match(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//);
return matched?.[0] ?? REDACTED;
}
};
pluginConfig.remoteConfigurations = Object.fromEntries(
Object.entries(pluginConfig.remoteConfigurations || {}).map(([id, config]) => [
id,
{
...config,
uri: toSchemeOnly(config.uri),
},
])
);
pluginConfig.region = redact(pluginConfig.region);
pluginConfig.bucket = redact(pluginConfig.bucket);
pluginConfig.pluginSyncExtendedSetting = {};
pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID);
pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase);
pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID);
pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays);
pluginConfig.jwtKey = redact(pluginConfig.jwtKey);
pluginConfig.jwtSub = redact(pluginConfig.jwtSub);
pluginConfig.jwtKid = redact(pluginConfig.jwtKid);
pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders);
pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders);
pluginConfig.P2P_turnCredential = redact(pluginConfig.P2P_turnCredential);
pluginConfig.P2P_turnUsername = redact(pluginConfig.P2P_turnUsername);
pluginConfig.P2P_turnServers = `(${pluginConfig.P2P_turnServers.split(",").length} servers configured)`;
const endpoint = pluginConfig.endpoint;
if (endpoint == "") {
pluginConfig.endpoint = "Not configured or AWS";
} else {
const endpointScheme = pluginConfig.endpoint.startsWith("http:")
? "(HTTP)"
: pluginConfig.endpoint.startsWith("https:")
? "(HTTPS)"
: "";
pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`;
}
const obsidianInfo = {
navigator: compatGlobal.navigator.userAgent,
fileSystem: core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
};
const result = {
obsidianInfo,
responseConfig,
pluginConfig,
manifestVersion,
packageVersion,
};
return result;
}

View File

@@ -16,10 +16,7 @@
import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { ConnectionStringParser } from "@lib/common/ConnectionString";
import type { P2PSyncSetting, RemoteConfiguration } from "@lib/common/models/setting.type";
import {
activateP2PRemoteConfiguration,
createRemoteConfigurationId,
} from "@lib/serviceFeatures/remoteConfig";
import { activateP2PRemoteConfiguration, createRemoteConfigurationId } from "@lib/serviceFeatures/remoteConfig";
import { extractP2PRoomSuffix } from "@lib/common/utils";
import { SetupManager } from "@/modules/features/SetupManager";
import SetupRemoteP2P from "@/modules/features/SetupWizard/dialogs/SetupRemoteP2P.svelte";
@@ -36,9 +33,7 @@
let replicatingPeerId = $state<string | null>(null);
let communicatingUntil = $state<Record<string, number>>({});
const COMMUNICATION_HOLD_MS = 2500;
let syncOnReplicationSetting = $state(
core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? ""
);
let syncOnReplicationSetting = $state(core.services.setting.currentSettings()?.P2P_SyncOnReplication ?? "");
type P2PRemoteOption = {
id: string;
name: string;
@@ -51,12 +46,19 @@
let selectingP2PRemote = $state(false);
function addToList(item: string, list: string): string {
const items = list.split(",").map((e) => e.trim()).filter((e) => e);
const items = list
.split(",")
.map((e) => e.trim())
.filter((e) => e);
if (!items.includes(item)) items.push(item);
return items.join(",");
}
function removeFromList(item: string, list: string): string {
return list.split(",").map((e) => e.trim()).filter((e) => e && e !== item).join(",");
return list
.split(",")
.map((e) => e.trim())
.filter((e) => e && e !== item)
.join(",");
}
function markCommunicating(peerId: string) {
@@ -409,7 +411,12 @@
</option>
{/each}
</select>
<button class="icon-button" onclick={() => createAndSelectP2PRemote()} title="Create P2P remote" aria-label="Create P2P remote">
<button
class="icon-button"
onclick={() => createAndSelectP2PRemote()}
title="Create P2P remote"
aria-label="Create P2P remote"
>
+
</button>
</div>
@@ -442,7 +449,8 @@
<div class="peer-item">
<div class="peer-info">
<div class="peer-name">
{peer.name} : <span class="peer-id-mini" title={peer.peerId}>({peer.peerId.slice(0, 8)})</span>
{peer.name} :
<span class="peer-id-mini" title={peer.peerId}>({peer.peerId.slice(0, 8)})</span>
{#if isCommunicating(peer.peerId)}
<span class="comm-icon" title="Communicating" aria-label="Communicating">📡</span>
{/if}
@@ -460,11 +468,11 @@
<button
class="emoji-button"
disabled={replicatingPeerId !== null}
title={replicatingPeerId === peer.peerId ? 'Replicating...' : 'Replicate now'}
aria-label={replicatingPeerId === peer.peerId ? 'Replicating' : 'Replicate now'}
title={replicatingPeerId === peer.peerId ? "Replicating..." : "Replicate now"}
aria-label={replicatingPeerId === peer.peerId ? "Replicating" : "Replicate now"}
onclick={() => startReplication(peer)}
>
{replicatingPeerId === peer.peerId ? '⏳' : '🔄'}
{replicatingPeerId === peer.peerId ? "⏳" : "🔄"}
</button>
<button
class="action-button"
@@ -478,25 +486,31 @@
<span class="decision-label">WATCH</span>
<button
class="emoji-button {isWatching(peer.peerId) ? 'is-watching' : ''}"
title={isWatching(peer.peerId) ? 'Watching this peer \u2014 click to stop' : 'Watch this peer\'s changes'}
aria-label={isWatching(peer.peerId) ? 'Stop watching' : 'Watch peer'}
title={isWatching(peer.peerId)
? "Watching this peer \u2014 click to stop"
: "Watch this peer's changes"}
aria-label={isWatching(peer.peerId) ? "Stop watching" : "Watch peer"}
disabled={!canEditP2PSettings()}
onclick={() => toggleWatch(peer.peerId)}
>
{isWatching(peer.peerId) ? '🔔' : '🔕'}
{isWatching(peer.peerId) ? "🔔" : "🔕"}
</button>
</div> <div class="decision-row watch-row">
</div>
<div class="decision-row watch-row">
<span class="decision-label">SYNC</span>
<button
class="emoji-button {isSyncTarget(peer.name) ? 'is-watching' : ''}"
title={isSyncTarget(peer.name) ? 'Sync target \u2014 click to remove' : 'Set as sync target'}
aria-label={isSyncTarget(peer.name) ? 'Remove sync target' : 'Set sync target'}
title={isSyncTarget(peer.name)
? "Sync target \u2014 click to remove"
: "Set as sync target"}
aria-label={isSyncTarget(peer.name) ? "Remove sync target" : "Set sync target"}
disabled={!canEditP2PSettings()}
onclick={() => toggleSyncTarget(peer)}
>
{isSyncTarget(peer.name) ? '🔗' : '⛓️‍💥'}
{isSyncTarget(peer.name) ? "🔗" : "⛓️‍💥"}
</button>
</div> {:else}
</div>
{:else}
<div class="decision-status">
<span class="badge status-chip {getAcceptanceStatusClass(peer)}">
{getAcceptanceStatus(peer)}
@@ -571,7 +585,6 @@
display: flex;
flex-direction: column;
gap: 1rem;
padding: 0.75rem;
}
.peers-section {
@@ -584,7 +597,7 @@
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: nowrap;
flex-wrap: wrap;
gap: 0.5rem;
}
@@ -603,8 +616,9 @@
}
.remote-picker {
max-width: 14rem;
min-width: 8rem;
max-width: 10rem;
min-width: 1em;
flex-shrink: 1;
height: 1.9rem;
border: 1px solid var(--divider-color);
border-radius: 0.4rem;
@@ -648,6 +662,7 @@
.peers-header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
gap: 0.5rem;
@@ -873,5 +888,4 @@
font-size: 0.9rem;
padding: 1rem;
}
</style>
</style>

View File

@@ -121,7 +121,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
return;
}
const isHidden = document.hidden;
const isHidden = activeWindow.document.hidden;
if (this.isLastHidden === isHidden) {
return;
}
@@ -134,7 +134,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
} else {
// suspend all temporary.
if (this.services.appLifecycle.isSuspended()) return;
if (!this.hasFocus) return;
// Do not block resume by focus state here; visibility recovery should be enough.
await this.services.appLifecycle.onResuming();
await this.services.appLifecycle.onResumed();
}

View File

@@ -25,7 +25,7 @@ import {
EVENT_ON_UNRESOLVED_ERROR,
} from "../../common/events.ts";
import { AbstractObsidianModule } from "../AbstractObsidianModule.ts";
import { addIcon, normalizePath, Notice } from "../../deps.ts";
import { addIcon, debounce, normalizePath, Notice, stringifyYaml, type WorkspaceLeaf } from "../../deps.ts";
import { LOG_LEVEL_NOTICE, setGlobalLogFunction } from "octagonal-wheels/common/logger";
import { LogPaneView, VIEW_TYPE_LOG } from "./Log/LogPaneView.ts";
import { serialized } from "octagonal-wheels/concurrency/lock";
@@ -41,6 +41,8 @@ import {
} from "@lib/string_and_binary/path.ts";
import { MARK_LOG_NETWORK_ERROR, MARK_LOG_SEPARATOR } from "@lib/services/lib/logUtils.ts";
import { NetworkWarningStyles } from "@lib/common/models/setting.const.ts";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
import { generateReport } from "@/common/reportTool.ts";
// This module cannot be a core module because it depends on the Obsidian UI.
@@ -50,18 +52,51 @@ const globalLogFunction = (message: any, level?: number, key?: string) => {
const messageX =
message instanceof Error
? new LiveSyncError("[Error Logged]: " + message.message, { cause: message })
: message;
: typeof message === "string"
? message
: JSON.stringify(message);
const entry = { message: messageX, level, key } as LogEntry;
recentLogEntries.value = [...recentLogEntries.value, entry];
};
setGlobalLogFunction(globalLogFunction);
let recentLogs = [] as string[];
// Keep the recent logs in memory for display, but also keep a longer history in logForDump for when the user wants to see more logs.
// logForDump is not reactive and is only used for dumping logs when requested, while recentLogs is reactive and is used for displaying logs in the UI.
const logForDump = [] as string[];
function addLog(log: string) {
recentLogs = [...recentLogs, log].splice(-200);
logMessages.value = recentLogs;
logForDump.push(log);
while (logForDump.length > 1000) {
logForDump.shift();
}
}
// Display log is kept separate from the full log history to optimize performance and memory usage.
// And debounce the updates to the display log to avoid excessive UI updates when there are many log entries in a short time.
const logForDisplay = [] as string[];
const updateLogMessage = debounce(() => {
logMessages.value = [...logForDisplay];
}, 25);
function addDisplayLog(log: string) {
logForDisplay.push(log);
while (logForDisplay.length > 200) {
logForDisplay.shift();
}
updateLogMessage();
}
const redactPatterns = [/PBKDF2 salt \(Security Seed\):.*$/];
function redactLog(log: string) {
let redactedLog = log;
for (const pattern of redactPatterns) {
redactedLog = redactedLog.replace(pattern, (match) => {
return match.split(":")[0] + ": [REDACTED]";
});
}
return redactedLog;
}
// logStore.intercept(e => e.slice(Math.min(e.length - 200, 0)));
const showDebugLog = false;
@@ -86,15 +121,15 @@ export class ModuleLog extends AbstractObsidianModule {
// const emptyMark = `\u{2003}`;
function padLeftSpComputed(numI: ReactiveValue<number>, mark: string) {
const formatted = reactiveSource("");
let timer: ReturnType<typeof setTimeout> | undefined = undefined;
let timer: number | undefined = undefined;
let maxLen = 1;
numI.onChanged((numX) => {
const num = numX.value;
const numLen = `${Math.abs(num)}`.length + 1;
maxLen = maxLen < numLen ? numLen : maxLen;
if (timer) clearTimeout(timer);
if (timer) compatGlobal.clearTimeout(timer);
if (num == 0) {
timer = setTimeout(() => {
timer = compatGlobal.setTimeout(() => {
formatted.value = "";
maxLen = 1;
}, 3000);
@@ -323,7 +358,7 @@ export class ModuleLog extends AbstractObsidianModule {
if (this.nextFrameQueue) {
return;
}
this.nextFrameQueue = requestAnimationFrame(() => {
this.nextFrameQueue = compatGlobal.requestAnimationFrame(() => {
this.nextFrameQueue = undefined;
const { message, status } = this.statusBarLabels.value;
// const recent = logMessages.value;
@@ -346,7 +381,8 @@ export class ModuleLog extends AbstractObsidianModule {
(a, b) => (a < b.ttl ? a : b.ttl),
Number.MAX_SAFE_INTEGER
);
if (this.logLines.length > 0) setTimeout(() => this.applyStatusBarText(), minimumNext - now);
if (this.logLines.length > 0)
compatGlobal.setTimeout(() => this.applyStatusBarText(), minimumNext - now);
const recent = this.logLines.map((e) => e.message);
const recentLogs = recent.reverse().join("\n");
if (isDirty("recentLogs", recentLogs)) this.logHistory!.innerText = recentLogs;
@@ -368,7 +404,7 @@ export class ModuleLog extends AbstractObsidianModule {
if (this.statusDiv) {
this.statusDiv.remove();
}
document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove());
compatGlobal.document.querySelectorAll(`.livesync-status`)?.forEach((e) => e.remove());
return Promise.resolve(true);
}
_everyOnloadStart(): Promise<boolean> {
@@ -390,7 +426,28 @@ export class ModuleLog extends AbstractObsidianModule {
void this.services.API.showWindow(VIEW_TYPE_LOG);
},
});
this.registerView(VIEW_TYPE_LOG, (leaf) => new LogPaneView(leaf, this.plugin));
this.addCommand({
id: "dump-debug-info",
name: "Generate full report for opening the issue with debug info",
callback: async () => {
const recentLog = [...logForDump];
const report = await generateReport(this.services.setting.currentSettings(), this.core);
const info = {
...report,
recentLog: recentLog.map(redactLog),
};
const yaml = `\`\`\`\`
# ---- Debug Info Dump ----
${stringifyYaml(info)}
\`\`\`\``;
if (await this.services.UI.promptCopyToClipboard("Debug info", yaml)) {
new Notice(
"Debug info copied to clipboard. You can paste it in the issue. Be careful as it may contain sensitive information, review it before sharing."
);
}
},
});
this.registerView(VIEW_TYPE_LOG, (leaf: WorkspaceLeaf) => new LogPaneView(leaf, this.plugin));
return Promise.resolve(true);
}
private _everyOnloadAfterLoadSettings(): Promise<boolean> {
@@ -404,7 +461,7 @@ export class ModuleLog extends AbstractObsidianModule {
void this.setFileStatus();
});
const w = document.querySelectorAll(`.livesync-status`);
const w = compatGlobal.document.querySelectorAll(`.livesync-status`);
w.forEach((e) => e.remove());
this.observeForLogs();
@@ -421,6 +478,8 @@ export class ModuleLog extends AbstractObsidianModule {
this.statusBar?.addClass("syncstatusbar");
}
this.adjustStatusDivPosition();
this._log("Log module loaded", LOG_LEVEL_INFO);
this._log("Verbose log", LOG_LEVEL_VERBOSE);
return Promise.resolve(true);
}
@@ -444,11 +503,12 @@ export class ModuleLog extends AbstractObsidianModule {
if (level == LOG_LEVEL_DEBUG && !showDebugLog) {
return;
}
let memoOnly = false;
if (level <= LOG_LEVEL_INFO && this.settings && this.settings.lessInformationInLog) {
return;
memoOnly = true;
}
if (this.settings && !this.settings.showVerboseLog && level == LOG_LEVEL_VERBOSE) {
return;
memoOnly = true;
}
const vaultName = this.services.vault.getVaultName();
const now = new Date();
@@ -469,6 +529,15 @@ export class ModuleLog extends AbstractObsidianModule {
? `${errorInfo}`
: JSON.stringify(message, null, 2);
const newMessage = timestamp + "->" + messageContent;
if (this.settings?.writeLogToTheFile) {
this.writeLogToTheFile(now, vaultName, newMessage);
}
addLog(newMessage);
if (memoOnly) {
return;
}
addDisplayLog(newMessage);
if (message instanceof Error) {
console.error(vaultName + ":" + newMessage);
} else if (level >= LOG_LEVEL_INFO) {
@@ -479,10 +548,6 @@ export class ModuleLog extends AbstractObsidianModule {
if (!this.settings?.showOnlyIconsOnEditor) {
this.statusLog.value = messageContent;
}
if (this.settings?.writeLogToTheFile) {
this.writeLogToTheFile(now, vaultName, newMessage);
}
addLog(newMessage);
this.logLines.push({ ttl: now.getTime() + 3000, message: newMessage });
if (level >= LOG_LEVEL_NOTICE) {

View File

@@ -39,6 +39,7 @@ import { EVENT_REQUEST_SHOW_HISTORY } from "../../../common/obsidianEvents.ts";
import { generateCredentialObject } from "../../../lib/src/replication/httplib.ts";
import type { ObsidianLiveSyncSettingTab } from "./ObsidianLiveSyncSettingTab.ts";
import type { PageFunctions } from "./SettingPane.ts";
import { generateReport } from "@/common/reportTool.ts";
export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement, { addPanel }: PageFunctions): void {
// const hatchWarn = this.createEl(paneEl, "div", { text: `To stop the boot up sequence for fixing problems on databases, you can put redflag.md on top of your vault (Rebooting obsidian is required).` });
// hatchWarn.addClass("op-warn-info");
@@ -69,140 +70,14 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
eventHub.emitEvent(EVENT_REQUEST_RUN_FIX_INCOMPLETE);
})
);
new Setting(paneEl).setName($msg("Prepare the 'report' to create an issue")).addButton((button) =>
button
.setButtonText($msg("Copy Report to clipboard"))
.setCta()
.setDisabled(false)
.onClick(async () => {
let responseConfig: any = {};
const REDACTED = "𝑅𝐸𝐷𝐴𝐶𝑇𝐸𝐷";
if (this.editingSettings.remoteType == REMOTE_COUCHDB) {
try {
const credential = generateCredentialObject(this.editingSettings);
const customHeaders = parseHeaderValues(this.editingSettings.couchDB_CustomHeaders);
const r = await requestToCouchDBWithCredentials(
this.editingSettings.couchDB_URI,
credential,
window.origin,
undefined,
undefined,
undefined,
customHeaders
);
Logger(JSON.stringify(r.json, null, 2));
responseConfig = r.json;
responseConfig["couch_httpd_auth"].secret = REDACTED;
responseConfig["couch_httpd_auth"].authentication_db = REDACTED;
responseConfig["couch_httpd_auth"].authentication_redirect = REDACTED;
responseConfig["couchdb"].uuid = REDACTED;
responseConfig["admins"] = REDACTED;
delete responseConfig["jwt_keys"];
if ("secret" in responseConfig["chttpd_auth"])
responseConfig["chttpd_auth"].secret = REDACTED;
} catch (ex) {
Logger(ex, LOG_LEVEL_VERBOSE);
responseConfig = {
error: "Requesting information from the remote CouchDB has failed. If you are using IBM Cloudant, this is normal behaviour.",
};
}
} else if (this.editingSettings.remoteType == REMOTE_MINIO) {
responseConfig = { error: "Object Storage Synchronisation" };
//
}
const defaultKeys = Object.keys(DEFAULT_SETTINGS) as (keyof ObsidianLiveSyncSettings)[];
const pluginConfig = JSON.parse(JSON.stringify(this.editingSettings)) as ObsidianLiveSyncSettings;
const pluginKeys = Object.keys(pluginConfig);
for (const key of pluginKeys) {
if (defaultKeys.includes(key as any)) continue;
delete pluginConfig[key as keyof ObsidianLiveSyncSettings];
}
pluginConfig.couchDB_DBNAME = REDACTED;
pluginConfig.couchDB_PASSWORD = REDACTED;
const scheme = pluginConfig.couchDB_URI.startsWith("http:")
? "(HTTP)"
: pluginConfig.couchDB_URI.startsWith("https:")
? "(HTTPS)"
: "";
pluginConfig.couchDB_URI = isCloudantURI(pluginConfig.couchDB_URI)
? "cloudant"
: `self-hosted${scheme}`;
pluginConfig.couchDB_USER = REDACTED;
pluginConfig.passphrase = REDACTED;
pluginConfig.encryptedPassphrase = REDACTED;
pluginConfig.encryptedCouchDBConnection = REDACTED;
pluginConfig.accessKey = REDACTED;
pluginConfig.secretKey = REDACTED;
const redact = (source: string) => `${REDACTED}(${source.length} letters)`;
const toSchemeOnly = (uri: string) => {
try {
return `${new URL(uri).protocol}//`;
} catch {
const matched = uri.match(/^[A-Za-z][A-Za-z0-9+.-]*:\/\//);
return matched?.[0] ?? REDACTED;
}
};
pluginConfig.remoteConfigurations = Object.fromEntries(
Object.entries(pluginConfig.remoteConfigurations || {}).map(([id, config]) => [
id,
{
...config,
uri: toSchemeOnly(config.uri),
},
])
);
pluginConfig.region = redact(pluginConfig.region);
pluginConfig.bucket = redact(pluginConfig.bucket);
pluginConfig.pluginSyncExtendedSetting = {};
pluginConfig.P2P_AppID = redact(pluginConfig.P2P_AppID);
pluginConfig.P2P_passphrase = redact(pluginConfig.P2P_passphrase);
pluginConfig.P2P_roomID = redact(pluginConfig.P2P_roomID);
pluginConfig.P2P_relays = redact(pluginConfig.P2P_relays);
pluginConfig.jwtKey = redact(pluginConfig.jwtKey);
pluginConfig.jwtSub = redact(pluginConfig.jwtSub);
pluginConfig.jwtKid = redact(pluginConfig.jwtKid);
pluginConfig.bucketCustomHeaders = redact(pluginConfig.bucketCustomHeaders);
pluginConfig.couchDB_CustomHeaders = redact(pluginConfig.couchDB_CustomHeaders);
pluginConfig.P2P_turnCredential = redact(pluginConfig.P2P_turnCredential);
pluginConfig.P2P_turnUsername = redact(pluginConfig.P2P_turnUsername);
pluginConfig.P2P_turnServers = `(${pluginConfig.P2P_turnServers.split(",").length} servers configured)`;
const endpoint = pluginConfig.endpoint;
if (endpoint == "") {
pluginConfig.endpoint = "Not configured or AWS";
} else {
const endpointScheme = pluginConfig.endpoint.startsWith("http:")
? "(HTTP)"
: pluginConfig.endpoint.startsWith("https:")
? "(HTTPS)"
: "";
pluginConfig.endpoint = `${endpoint.indexOf(".r2.cloudflarestorage.") !== -1 ? "R2" : "self-hosted?"}(${endpointScheme})`;
}
const obsidianInfo = {
navigator: navigator.userAgent,
fileSystem: this.core.services.vault.isStorageInsensitive() ? "insensitive" : "sensitive",
};
const msgConfig = `# ---- Obsidian info ----
${stringifyYaml(obsidianInfo)}
---
# ---- remote config ----
${stringifyYaml(responseConfig)}
---
# ---- Plug-in config ----
${stringifyYaml({
version: this.manifestVersion,
...pluginConfig,
})}`;
console.log(msgConfig);
if ((await this.services.UI.promptCopyToClipboard("Generated report", msgConfig)) == true) {
// await navigator.clipboard.writeText(msgConfig);
// Logger(
// `Generated report has been copied to clipboard. Please report the issue with this! Thank you for your cooperation!`,
// LOG_LEVEL_NOTICE
// );
}
await this.app.commands.executeCommandById("obsidian-livesync:dump-debug-info");
})
);
new Setting(paneEl)