feat(523): Settings -> Data -> Scheduled backups - the verdict first, then destination, schedule, Test, Back up now and recent runs
BackupScheduleSettings.vue leads with whether the data is backed up (last good backup, or Overdue), then what is wrong (last failure in words, an S3 secret this SECRET_KEY cannot open, incomplete config), then the form: daily toggle and UTC hour, folder or S3 bucket, Test destination with unsaved values, Back up now (polls while running, re-armed in finally), and the last runs. The secret is write-only. formatBytes moves to utils/bytes.ts for both views. Restore from Backup now also takes the .tar, and says to use the CLI on a 413. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,8 @@ import PaginationBar from "@/components/PaginationBar.vue";
|
||||
import TagInput from "@/components/TagInput.vue";
|
||||
import MomentsSettings from "@/components/MomentsSettings.vue";
|
||||
import ReplyShapesSettings from "@/components/ReplyShapesSettings.vue";
|
||||
import BackupScheduleSettings from "@/components/BackupScheduleSettings.vue";
|
||||
import { formatBytes } from "@/utils/bytes";
|
||||
import { fmtDate, fmtLogStamp } from "@/utils/dateFormat";
|
||||
import { fetchVersion, type VersionPayload } from "@/api/version";
|
||||
|
||||
@@ -640,13 +642,6 @@ const dbHealth = ref<DbHealth | null>(null);
|
||||
const loadingHealth = ref(false);
|
||||
const DEAD_PCT_WARN = 20; // dead-tuple ratio above this = autovacuum falling behind
|
||||
|
||||
function formatBytes(n: number): string {
|
||||
if (n < 1024) return `${n} B`;
|
||||
const units = ["KB", "MB", "GB", "TB"];
|
||||
let v = n / 1024, i = 0;
|
||||
while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; }
|
||||
return `${v.toFixed(v >= 10 || i === 0 ? 0 : 1)} ${units[i]}`;
|
||||
}
|
||||
const pluginInstallCommands = computed(() => {
|
||||
const mkt = (pluginMarketplaceUrl.value || '').trim()
|
||||
|| serverMarketplaceUrl.value
|
||||
@@ -1368,14 +1363,21 @@ async function handleRestoreFile(event: Event) {
|
||||
if (!file) return;
|
||||
restoring.value = true;
|
||||
try {
|
||||
const text = await file.text();
|
||||
const data = JSON.parse(text);
|
||||
// The scheduled backup's .tar goes up as it is; the JSON download is
|
||||
// parsed first so a damaged file fails here, before the upload.
|
||||
const isTar = file.name.toLowerCase().endsWith(".tar");
|
||||
const res = await fetch("/api/admin/restore", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
headers: { "Content-Type": isTar ? "application/x-tar" : "application/json" },
|
||||
body: isTar ? file : JSON.stringify(JSON.parse(await file.text())),
|
||||
signal: bulkDeadline(),
|
||||
});
|
||||
if (res.status === 413) {
|
||||
throw new Error(
|
||||
"the file is bigger than the web upload allows. Restore it with " +
|
||||
"`python -m scribe.restore <file>` inside the Scribe container instead.",
|
||||
);
|
||||
}
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({ error: `Error ${res.status}` }));
|
||||
throw new Error((body as Record<string, string>).error || `Error ${res.status}`);
|
||||
@@ -2650,12 +2652,16 @@ async function deleteUser(userId: number) {
|
||||
<input
|
||||
ref="restoreFileInput"
|
||||
type="file"
|
||||
accept=".json"
|
||||
accept=".json,.tar"
|
||||
class="hidden-file-input"
|
||||
@change="handleRestoreFile"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="settings-section full-width">
|
||||
<h2>Scheduled backups</h2>
|
||||
<BackupScheduleSettings />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user