mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-10-21 16:52:32 +00:00
Add read-only database diagnostics
This commit is contained in:
@@ -16,6 +16,7 @@ import { Semaphore } from "octagonal-wheels/concurrency/semaphore";
|
||||
import { LiveSyncSetting as Setting } from "./LiveSyncSetting.ts";
|
||||
import {
|
||||
EVENT_ANALYSE_DB_USAGE,
|
||||
EVENT_INSPECT_DB_INTEGRITY,
|
||||
EVENT_REQUEST_CHECK_REMOTE_SIZE,
|
||||
EVENT_REQUEST_RUN_DOCTOR,
|
||||
EVENT_REQUEST_RUN_FIX_INCOMPLETE,
|
||||
@@ -113,6 +114,14 @@ export function paneHatch(this: ObsidianLiveSyncSettingTab, paneEl: HTMLElement,
|
||||
await chooseAndCopyFileDatabaseInfo(this.core);
|
||||
})
|
||||
);
|
||||
new Setting(paneEl)
|
||||
.setName($msg("Inspect database integrity"))
|
||||
.setDesc(
|
||||
$msg("Read normal-file Metadata and current conflicts locally and in CouchDB, then compare content for suspect groups. Choose original names or pseudonyms before starting.")
|
||||
)
|
||||
.addButton((button) =>
|
||||
button.setButtonText($msg("Inspect")).onClick(() => eventHub.emitEvent(EVENT_INSPECT_DB_INTEGRITY))
|
||||
);
|
||||
new Setting(paneEl)
|
||||
.setName($msg("Analyse database usage"))
|
||||
.setDesc(
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from "svelte";
|
||||
import type { GuestDialogProps } from "@/modules/services/LiveSyncUI/svelteDialog";
|
||||
import DialogHeader from "@/modules/services/LiveSyncUI/components/DialogHeader.svelte";
|
||||
import { $msg as translate } from "@/common/translation";
|
||||
import type { DatabaseDiagnosticDialogOptions } from "@/serviceFeatures/databaseDiagnosticDialogTypes";
|
||||
import type { DatabaseDiagnosticOutput } from "@/serviceFeatures/databaseDiagnostics";
|
||||
import type { DiagnosticNameMode } from "@vrtmrz/livesync-commonlib/diagnostics";
|
||||
|
||||
const { setResult, getInitialData }: GuestDialogProps<"close", DatabaseDiagnosticDialogOptions> = $props();
|
||||
let options = $state<DatabaseDiagnosticDialogOptions>();
|
||||
let phase = $state<"choice" | "running" | "result" | "failed">("choice");
|
||||
let nameMode = $state<DiagnosticNameMode>("pseudonym");
|
||||
let processed = $state(0);
|
||||
let cancelling = $state(false);
|
||||
let output = $state<DatabaseDiagnosticOutput>();
|
||||
let selectedFormat = $state("");
|
||||
let actionStatus = $state("");
|
||||
let disposed = false;
|
||||
const selected = $derived(output?.artifacts.find((artifact) => artifact.format === selectedFormat));
|
||||
const title = $derived(
|
||||
options?.kind === "integrity" ? translate("Inspect database integrity") : translate("Analyse database usage")
|
||||
);
|
||||
const statusText = $derived(
|
||||
output
|
||||
? {
|
||||
complete: translate("Inspection completed. Review the findings and coverage before sharing."),
|
||||
partial: translate("Inspection partially completed. Some information could not be checked; review the reported reasons."),
|
||||
failed: translate("Inspection failed. The report records the available evidence and reasons."),
|
||||
cancelled: translate("Inspection cancelled. The report contains only the evidence collected so far."),
|
||||
}[output.status]
|
||||
: ""
|
||||
);
|
||||
onMount(() => {
|
||||
options = getInitialData?.();
|
||||
});
|
||||
onDestroy(() => {
|
||||
disposed = true;
|
||||
options?.cancel();
|
||||
});
|
||||
|
||||
async function begin() {
|
||||
if (!options || phase !== "choice") return;
|
||||
phase = "running";
|
||||
try {
|
||||
const result = await options.run(nameMode, (count) => {
|
||||
if (!disposed) processed = count;
|
||||
});
|
||||
if (disposed) return;
|
||||
output = result;
|
||||
selectedFormat = result.artifacts[0].format;
|
||||
phase = "result";
|
||||
} catch {
|
||||
if (!disposed) phase = "failed";
|
||||
}
|
||||
}
|
||||
|
||||
async function copyReport(event: MouseEvent) {
|
||||
if (!selected) return;
|
||||
const owner = (event.currentTarget as HTMLElement).ownerDocument.defaultView;
|
||||
try {
|
||||
await owner?.navigator.clipboard.writeText(selected.content);
|
||||
actionStatus = translate("Report copied.");
|
||||
} catch {
|
||||
actionStatus = translate("Copy failed. Select the report text and copy it manually.");
|
||||
}
|
||||
}
|
||||
|
||||
function saveReport(event: MouseEvent) {
|
||||
if (!selected) return;
|
||||
const document = (event.currentTarget as HTMLElement).ownerDocument;
|
||||
const url = URL.createObjectURL(new Blob([selected.content], { type: "text/plain;charset=utf-8" }));
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = selected.filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
actionStatus = translate("Download requested. You can also copy the report if downloads are unavailable on this device.");
|
||||
}
|
||||
</script>
|
||||
|
||||
<DialogHeader {title} />
|
||||
<div class="database-diagnostic" data-diagnostic-phase={phase}>
|
||||
{#if phase === "choice"}
|
||||
<p>{translate("Choose how names appear in this run. Pseudonyms replace paths and identifiers consistently. Original names include private filenames. File contents and credentials are omitted in either mode.")}</p>
|
||||
<p>
|
||||
{options?.kind === "usage"
|
||||
? translate("Read available local revisions and report their Chunk references. Settings are left unchanged.")
|
||||
: translate("Read normal-file Metadata, current conflicts, and content for suspect groups from this device and CouchDB. Other remote types and older revision history are outside this inspection. Results do not authorise deletion.")}
|
||||
</p>
|
||||
<label>
|
||||
{translate("Names in this report")}
|
||||
<select bind:value={nameMode}>
|
||||
<option value="pseudonym">{translate("Pseudonyms (recommended)")}</option>
|
||||
<option value="original">{translate("Original names")}</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="diagnostic-controls">
|
||||
<button onclick={() => setResult("close")}>{translate("Cancel")}</button>
|
||||
<button class="mod-cta" disabled={!options} onclick={() => void begin()}>{translate("Begin inspection")}</button>
|
||||
</div>
|
||||
{:else if phase === "running"}
|
||||
<p role="status">
|
||||
{cancelling ? translate("Cancelling inspection…") : `${translate("Items inspected")}: ${processed}`}
|
||||
</p>
|
||||
<button disabled={cancelling} onclick={() => { cancelling = true; options?.cancel(); }}>
|
||||
{translate("Cancel inspection")}
|
||||
</button>
|
||||
{:else if phase === "result" && output}
|
||||
<p role="status">{statusText}</p>
|
||||
<p>
|
||||
{output.nameMode === "pseudonym"
|
||||
? translate("This report uses temporary pseudonyms. Counts and relationships remain visible.")
|
||||
: translate("This report contains original paths and identifiers. Review them before sharing.")}
|
||||
</p>
|
||||
{#if options?.kind === "usage" && output.status !== "complete"}
|
||||
<p>{translate("Partial Usage results are available as JSON so their coverage and reasons stay with the rows.")}</p>
|
||||
{/if}
|
||||
<label>
|
||||
{translate("Report format")}
|
||||
<select bind:value={selectedFormat}>
|
||||
{#each output.artifacts as artifact}
|
||||
<option value={artifact.format}>{artifact.format.toUpperCase()}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<textarea readonly rows="16" aria-label={translate("Diagnostic report")} value={selected?.content ?? ""}></textarea>
|
||||
<p role="status">{actionStatus}</p>
|
||||
<div class="diagnostic-controls">
|
||||
<button onclick={(event) => void copyReport(event)}>{translate("Copy report")}</button>
|
||||
<button onclick={saveReport}>{translate("Save report")}</button>
|
||||
<button onclick={() => setResult("close")}>{translate("Close")}</button>
|
||||
</div>
|
||||
{:else if phase === "failed"}
|
||||
<p role="status">{translate("The inspection could not finish. Close this dialogue and try again after database startup.")}</p>
|
||||
<button onclick={() => setResult("close")}>{translate("Close")}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.database-diagnostic { display: flex; flex-direction: column; gap: var(--size-4-2); }
|
||||
label { display: flex; align-items: center; flex-wrap: wrap; gap: var(--size-4-3); }
|
||||
.diagnostic-controls { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: var(--size-4-2); }
|
||||
button, select { min-height: 44px; }
|
||||
textarea { width: 100%; resize: vertical; font-family: var(--font-monospace); font-size: var(--font-ui-small); }
|
||||
</style>
|
||||
Reference in New Issue
Block a user