Phase 4 - board templates, auto-backup, version history

This commit is contained in:
2026-02-16 14:55:58 +02:00
parent 9f60fa233e
commit 338a5ff3ea
7 changed files with 480 additions and 10 deletions

View File

@@ -12,6 +12,7 @@ import { invoke } from "@tauri-apps/api/core";
import { boardSchema, appSettingsSchema } from "./schemas";
import type { Board, BoardMeta } from "@/types/board";
import type { AppSettings } from "@/types/settings";
import type { BoardTemplate } from "@/types/template";
// ---------------------------------------------------------------------------
// Path helpers - portable: all data lives next to the exe
@@ -36,6 +37,16 @@ async function getSettingsPath(): Promise<string> {
return join(base, "settings.json");
}
async function getTemplatesDir(): Promise<string> {
const base = await getBaseDir();
return join(base, "templates");
}
async function getBackupsDir(boardId: string): Promise<string> {
const base = await getBaseDir();
return join(base, "backups", boardId);
}
function boardFilePath(boardsDir: string, boardId: string): Promise<string> {
return join(boardsDir, `${boardId}.json`);
}
@@ -63,6 +74,16 @@ export async function ensureDataDirs(): Promise<void> {
if (!(await exists(attachmentsDir))) {
await mkdir(attachmentsDir, { recursive: true });
}
const templatesDir = await getTemplatesDir();
if (!(await exists(templatesDir))) {
await mkdir(templatesDir, { recursive: true });
}
const backupsDir = await join(base, "backups");
if (!(await exists(backupsDir))) {
await mkdir(backupsDir, { recursive: true });
}
}
// ---------------------------------------------------------------------------
@@ -124,6 +145,7 @@ export async function listBoards(): Promise<BoardMeta[]> {
color: board.color,
cardCount: Object.keys(board.cards).length,
columnCount: board.columns.length,
createdAt: board.createdAt,
updatedAt: board.updatedAt,
});
} catch {
@@ -160,6 +182,14 @@ export async function saveBoard(board: Board): Promise<void> {
try {
const previous = await readTextFile(filePath);
await writeTextFile(backupPath, previous);
// Create timestamped backup (throttled: only if last backup > 5 min ago)
const backups = await listBackups(board.id);
const recentThreshold = new Date(Date.now() - 5 * 60 * 1000).toISOString();
if (backups.length === 0 || backups[0].timestamp < recentThreshold) {
await createBackup(JSON.parse(previous) as Board);
await pruneBackups(board.id);
}
} catch {
// If we can't create a backup, continue saving anyway
}
@@ -274,6 +304,112 @@ export async function searchAllBoards(query: string): Promise<SearchResult[]> {
// Attachment helpers
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// Templates
// ---------------------------------------------------------------------------
export async function listTemplates(): Promise<BoardTemplate[]> {
const dir = await getTemplatesDir();
if (!(await exists(dir))) return [];
const entries = await readDir(dir);
const templates: BoardTemplate[] = [];
for (const entry of entries) {
if (!entry.name || !entry.name.endsWith(".json")) continue;
try {
const filePath = await join(dir, entry.name);
const raw = await readTextFile(filePath);
templates.push(JSON.parse(raw));
} catch { continue; }
}
return templates;
}
export async function saveTemplate(template: BoardTemplate): Promise<void> {
const dir = await getTemplatesDir();
const filePath = await join(dir, `${template.id}.json`);
await writeTextFile(filePath, JSON.stringify(template, null, 2));
}
export async function deleteTemplate(templateId: string): Promise<void> {
const dir = await getTemplatesDir();
const filePath = await join(dir, `${templateId}.json`);
if (await exists(filePath)) {
await remove(filePath);
}
}
// ---------------------------------------------------------------------------
// Backups / Version History
// ---------------------------------------------------------------------------
export interface BackupEntry {
filename: string;
timestamp: string;
cardCount: number;
columnCount: number;
}
export async function listBackups(boardId: string): Promise<BackupEntry[]> {
const dir = await getBackupsDir(boardId);
if (!(await exists(dir))) return [];
const entries = await readDir(dir);
const backups: BackupEntry[] = [];
for (const entry of entries) {
if (!entry.name || !entry.name.endsWith(".json")) continue;
try {
const filePath = await join(dir, entry.name);
const raw = await readTextFile(filePath);
const data = JSON.parse(raw);
const board = boardSchema.parse(data);
const isoMatch = entry.name.match(/\d{4}-\d{2}-\d{2}T[\d.]+Z/);
backups.push({
filename: entry.name,
timestamp: isoMatch ? isoMatch[0].replace(/-(?=\d{2}-\d{2}T)/g, "-") : board.updatedAt,
cardCount: Object.keys(board.cards).length,
columnCount: board.columns.length,
});
} catch { continue; }
}
backups.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
return backups;
}
export async function createBackup(board: Board): Promise<void> {
const dir = await getBackupsDir(board.id);
if (!(await exists(dir))) {
await mkdir(dir, { recursive: true });
}
const ts = new Date().toISOString().replace(/:/g, "-");
const filename = `${board.id}-${ts}.json`;
const filePath = await join(dir, filename);
await writeTextFile(filePath, JSON.stringify(board, null, 2));
}
export async function pruneBackups(boardId: string, keep: number = 10): Promise<void> {
const backups = await listBackups(boardId);
if (backups.length <= keep) return;
const dir = await getBackupsDir(boardId);
const toDelete = backups.slice(keep);
for (const backup of toDelete) {
try {
const filePath = await join(dir, backup.filename);
await remove(filePath);
} catch { /* skip */ }
}
}
export async function restoreBackupFile(boardId: string, filename: string): Promise<Board> {
const dir = await getBackupsDir(boardId);
const filePath = await join(dir, filename);
const raw = await readTextFile(filePath);
const data = JSON.parse(raw);
return boardSchema.parse(data) as Board;
}
// ---------------------------------------------------------------------------
// Attachment helpers
// ---------------------------------------------------------------------------
export async function copyAttachment(
boardId: string,
sourcePath: string,