feat: migrate storage from JSON files to SQLite
Some checks failed
Lint & Test / test (push) Failing after 28s

Replace 22 individual JSON store files with a single SQLite database
(data/ledgrab.db). All entity stores now use BaseSqliteStore backed by
SQLite with WAL mode, write-through caching, and thread-safe access.

- Add Database class with SQLite backup/restore API
- Add BaseSqliteStore as drop-in replacement for BaseJsonStore
- Convert all 16 entity stores to SQLite
- Move global settings (MQTT, external URL, auto-backup) to SQLite
  settings table
- Replace JSON backup/restore with SQLite snapshot backups (.db files)
- Remove partial export/import feature (backend + frontend)
- Update demo seed to write directly to SQLite
- Add "Backup Now" button to settings UI
- Remove StorageConfig file path fields (single database_file remains)
This commit is contained in:
2026-03-25 00:03:19 +03:00
parent 29fb944494
commit 9dfd2365f4
38 changed files with 941 additions and 880 deletions

View File

@@ -191,10 +191,9 @@ import {
import {
openSettingsModal, closeSettingsModal, switchSettingsTab,
downloadBackup, handleRestoreFileSelected,
saveAutoBackupSettings, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
saveAutoBackupSettings, triggerBackupNow, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
restartServer, saveMqttSettings,
loadApiKeysList,
downloadPartialExport, handlePartialImportFileSelected,
connectLogViewer, disconnectLogViewer, clearLogViewer, applyLogFilter,
openLogOverlay, closeLogOverlay,
loadLogLevel, setLogLevel,
@@ -536,21 +535,20 @@ Object.assign(window, {
openCommandPalette,
closeCommandPalette,
// settings (tabs / backup / restore / auto-backup / MQTT / partial export-import / api keys / log level)
// settings (tabs / backup / restore / auto-backup / MQTT / api keys / log level)
openSettingsModal,
closeSettingsModal,
switchSettingsTab,
downloadBackup,
handleRestoreFileSelected,
saveAutoBackupSettings,
triggerBackupNow,
restoreSavedBackup,
downloadSavedBackup,
deleteSavedBackup,
restartServer,
saveMqttSettings,
loadApiKeysList,
downloadPartialExport,
handlePartialImportFileSelected,
connectLogViewer,
disconnectLogViewer,
clearLogViewer,

View File

@@ -419,6 +419,22 @@ export async function saveAutoBackupSettings(): Promise<void> {
}
}
export async function triggerBackupNow(): Promise<void> {
try {
const resp = await fetchWithAuth('/system/auto-backup/trigger', { method: 'POST' });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${resp.status}`);
}
showToast(t('settings.auto_backup.backup_created'), 'success');
loadBackupList();
loadAutoBackupSettings();
} catch (err) {
console.error('Backup failed:', err);
showToast(t('settings.auto_backup.backup_error') + ': ' + err.message, 'error');
}
}
// ─── Saved backup list ────────────────────────────────────
export async function loadBackupList(): Promise<void> {
@@ -566,76 +582,6 @@ export async function loadApiKeysList(): Promise<void> {
}
}
// ─── Partial Export / Import ───────────────────────────────────
export async function downloadPartialExport(): Promise<void> {
const storeKey = (document.getElementById('settings-partial-store') as HTMLSelectElement).value;
try {
const resp = await fetchWithAuth(`/system/export/${encodeURIComponent(storeKey)}`, { timeout: 30000 });
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${resp.status}`);
}
const blob = await resp.blob();
const disposition = resp.headers.get('Content-Disposition') || '';
const match = disposition.match(/filename="(.+?)"/);
const filename = match ? match[1] : `ledgrab-${storeKey}.json`;
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(a.href);
showToast(t('settings.partial.export_success'), 'success');
} catch (err) {
console.error('Partial export failed:', err);
showToast(t('settings.partial.export_error') + ': ' + err.message, 'error');
}
}
export async function handlePartialImportFileSelected(input: HTMLInputElement): Promise<void> {
const file = input.files![0];
input.value = '';
if (!file) return;
const storeKey = (document.getElementById('settings-partial-store') as HTMLSelectElement).value;
const merge = (document.getElementById('settings-partial-merge') as HTMLInputElement).checked;
const confirmMsg = merge
? t('settings.partial.import_confirm_merge').replace('{store}', storeKey)
: t('settings.partial.import_confirm_replace').replace('{store}', storeKey);
const confirmed = await showConfirm(confirmMsg);
if (!confirmed) return;
try {
const formData = new FormData();
formData.append('file', file);
const url = `${API_BASE}/system/import/${encodeURIComponent(storeKey)}?merge=${merge}`;
const resp = await fetch(url, {
method: 'POST',
headers: { 'Authorization': `Bearer ${apiKey}` },
body: formData,
});
if (!resp.ok) {
const err = await resp.json().catch(() => ({}));
throw new Error(err.detail || `HTTP ${resp.status}`);
}
const data = await resp.json();
showToast(data.message || t('settings.partial.import_success'), 'success');
settingsModal.forceClose();
} catch (err) {
console.error('Partial import failed:', err);
showToast(t('settings.partial.import_error') + ': ' + err.message, 'error');
}
}
// ─── Log Level ────────────────────────────────────────────────
export async function loadLogLevel(): Promise<void> {

View File

@@ -358,14 +358,13 @@ interface Window {
downloadBackup: (...args: any[]) => any;
handleRestoreFileSelected: (...args: any[]) => any;
saveAutoBackupSettings: (...args: any[]) => any;
triggerBackupNow: (...args: any[]) => any;
restoreSavedBackup: (...args: any[]) => any;
downloadSavedBackup: (...args: any[]) => any;
deleteSavedBackup: (...args: any[]) => any;
restartServer: (...args: any[]) => any;
saveMqttSettings: (...args: any[]) => any;
loadApiKeysList: (...args: any[]) => any;
downloadPartialExport: (...args: any[]) => any;
handlePartialImportFileSelected: (...args: any[]) => any;
connectLogViewer: (...args: any[]) => any;
disconnectLogViewer: (...args: any[]) => any;
clearLogViewer: (...args: any[]) => any;