feat(backup): admin and account UI for server backups, import by section
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m44s

Admin settings: switch, time, retention, list with download and a
"Jetzt sichern" button. Account settings: own server backups with
download and restore (replace or add, for chosen sections, replace needs
a second tap). The MindSift import gets the same section picker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-29 16:11:49 +01:00
1 parent c97765e08e
commit 64e077dd57
13 files changed
+446 -18

No files matched your search

+32 -1
View File
@@ -4,6 +4,8 @@ import {
DashboardStats,
Document,
TrashedDocument,
BackupFile,
DataSection,
ApiKeyScope,
Backlink,
DocumentBlock,
@@ -1155,7 +1157,8 @@ export const api = {
source: 'mindsift' | 'obsidian',
file: File,
preview: boolean,
mode: 'merge' | 'restore' = 'merge'
mode: 'merge' | 'restore' = 'merge',
sections?: DataSection[]
): Promise<ImportSummary> {
const token = getToken();
const form = new FormData();
@@ -1163,6 +1166,7 @@ export const api = {
const params = new URLSearchParams();
if (preview) params.set('preview', '1');
if (mode === 'restore') params.set('mode', 'restore');
if (sections && sections.length > 0) params.set('sections', sections.join(','));
const res = await fetch(`/api/v1/import/${source}${params.toString() ? `?${params}` : ''}`, {
method: 'POST',
headers: token ? { Authorization: `Bearer ${token}` } : {},
@@ -1173,6 +1177,33 @@ export const api = {
return data as ImportSummary;
},
// --- Automatic server backups ---
async adminListBackups(): Promise<BackupFile[]> {
return ensureArray(await request<BackupFile[]>('/api/v1/admin/backups'));
},
async adminRunBackup(): Promise<BackupFile[]> {
return ensureArray(await request<BackupFile[]>('/api/v1/admin/backups', { method: 'POST' }));
},
async adminDownloadBackup(name: string): Promise<Blob> {
return downloadBlob(`/api/v1/admin/backups/${encodeURIComponent(name)}`);
},
async listMyBackups(): Promise<BackupFile[]> {
return ensureArray(await request<BackupFile[]>('/api/v1/backups'));
},
async downloadMyBackup(name: string): Promise<Blob> {
return downloadBlob(`/api/v1/backups/${encodeURIComponent(name)}`);
},
async restoreMyBackup(name: string, mode: 'merge' | 'restore', sections: DataSection[], preview = false): Promise<ImportSummary> {
const params = new URLSearchParams({ mode, sections: sections.join(',') });
if (preview) params.set('preview', '1');
return request<ImportSummary>(`/api/v1/backups/${encodeURIComponent(name)}/restore?${params}`, { method: 'POST' });
},
// Danger zone: both need the current password
async purgeOwnData(password: string): Promise<void> {
await request('/api/v1/user/data', { method: 'DELETE', body: JSON.stringify({ password }) });