feat: add Network Backup section to Database Management

PC2 can now automatically pull and restore a fresh copy of PC1's database
on a daily schedule. Config and API key are stored in local JSON files so
they survive database restores.

- New networkSyncConfigService: file-based config (network-backup-key.json,
  network-sync-config.json) that persists through DB restores
- New networkSyncService: streams live pg_dump from source PC over HTTP and
  pipes into psql, then reconnects Prisma and applies missing migrations
- 6 new endpoints: get/regenerate API key, serve backup stream (key-auth
  only), get/save sync config, trigger immediate sync
- Hourly cron job that fires only when current hour matches configured syncHour
- NetworkBackupManager component: shows this machine's key (show/copy/regen)
  and receiver config (enable toggle, hour picker 0-23, source URL + key,
  Save + Sync Now, last sync status)
- README setup guide for both PCs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ff
2026-06-09 00:15:42 -04:00
co-authored by Claude Sonnet 4.6
parent 27d95ed752
commit f5f3768108
8 changed files with 668 additions and 1 deletions
@@ -0,0 +1,58 @@
import fs from "fs";
import path from "path";
import crypto from "crypto";
const KEY_FILE = path.resolve(process.cwd(), "network-backup-key.json");
const CONFIG_FILE = path.resolve(process.cwd(), "network-sync-config.json");
export interface NetworkSyncConfig {
enabled: boolean;
syncHour: number;
sourceUrl: string;
apiKey: string;
lastSyncAt: string | null;
lastSyncStatus: string | null;
lastSyncError: string | null;
}
export function getOrCreateApiKey(): string {
if (fs.existsSync(KEY_FILE)) {
try {
const data = JSON.parse(fs.readFileSync(KEY_FILE, "utf8"));
if (data.apiKey) return data.apiKey;
} catch {}
}
const key = crypto.randomUUID();
fs.writeFileSync(KEY_FILE, JSON.stringify({ apiKey: key }), "utf8");
return key;
}
export function regenerateApiKey(): string {
const key = crypto.randomUUID();
fs.writeFileSync(KEY_FILE, JSON.stringify({ apiKey: key }), "utf8");
return key;
}
export function readSyncConfig(): NetworkSyncConfig {
if (fs.existsSync(CONFIG_FILE)) {
try {
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"));
} catch {}
}
return {
enabled: false,
syncHour: 0,
sourceUrl: "",
apiKey: "",
lastSyncAt: null,
lastSyncStatus: null,
lastSyncError: null,
};
}
export function writeSyncConfig(patch: Partial<NetworkSyncConfig>): NetworkSyncConfig {
const current = readSyncConfig();
const updated = { ...current, ...patch };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(updated, null, 2), "utf8");
return updated;
}