diff --git a/src/app/api/migrations/v1/route.ts b/src/app/api/migrations/v1/route.ts index 0ea512a..0cca347 100644 --- a/src/app/api/migrations/v1/route.ts +++ b/src/app/api/migrations/v1/route.ts @@ -114,8 +114,8 @@ export async function POST(request: NextRequest) { } } - await ensureDocumentsV1Ready(); - await ensureAudiobooksV1Ready(); + const documentsMigrated = await ensureDocumentsV1Ready(); + const audiobooksMigrated = await ensureAudiobooksV1Ready(); const rekey = await rekeyAudiobooksV1(mappings); const documentsReady = await isDocumentsV1Ready(); @@ -125,6 +125,8 @@ export async function POST(request: NextRequest) { success: true, documentsReady, audiobooksReady, + documentsMigrated, + audiobooksMigrated, rekey, }); } catch (error) { diff --git a/src/contexts/ConfigContext.tsx b/src/contexts/ConfigContext.tsx index 9b8a57e..f8443a2 100644 --- a/src/contexts/ConfigContext.tsx +++ b/src/contexts/ConfigContext.tsx @@ -103,11 +103,31 @@ export function ConfigProvider({ children }: { children: ReactNode }) { // Run server-side v1 migrations proactively, since the client may now // reference SHA-based IDs immediately after the Dexie migration. - await fetch('/api/migrations/v1', { + const response = await fetch('/api/migrations/v1', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ mappings }), - }).catch(() => undefined); + }).catch(() => null); + + if (response?.ok) { + const data = await response.json(); + const didMigrate = + data.documentsMigrated || + data.audiobooksMigrated || + (data.rekey?.renamed ?? 0) > 0 || + (data.rekey?.merged ?? 0) > 0; + + if (didMigrate) { + toast.success('Library migration complete', { + duration: 5000, + icon: '📦', + style: { + background: 'var(--offbase)', + color: 'var(--foreground)', + }, + }); + } + } } catch (error) { console.warn('Startup migrations failed:', error); } diff --git a/src/lib/server/docstore.ts b/src/lib/server/docstore.ts index b6dd3da..07ac725 100644 --- a/src/lib/server/docstore.ts +++ b/src/lib/server/docstore.ts @@ -101,18 +101,18 @@ function safeDocumentName(rawName: string, fallback: string): string { return baseName.replaceAll('\u0000', '').slice(0, 240) || fallback; } -export async function ensureDocumentsV1Ready(): Promise { +export async function ensureDocumentsV1Ready(): Promise { await mkdir(DOCSTORE_DIR, { recursive: true }); await mkdir(DOCUMENTS_V1_DIR, { recursive: true }); const state = await loadMigrationState(); if (state.documentsV1Migrated && !(await hasLegacyDocumentFiles())) { - return; + return false; } if (!(await hasLegacyDocumentFiles())) { await saveMigrationState({ documentsV1Migrated: true }); - return; + return false; } let entries: Array = []; @@ -165,6 +165,7 @@ export async function ensureDocumentsV1Ready(): Promise { } await saveMigrationState({ documentsV1Migrated: !(await hasLegacyDocumentFiles()) }); + return true; } async function hasLegacyAudiobookDirs(): Promise { @@ -269,7 +270,7 @@ async function mergeDirectoryContents(sourceDir: string, targetDir: string): Pro return { moved, skipped }; } -export async function ensureAudiobooksV1Ready(): Promise { +export async function ensureAudiobooksV1Ready(): Promise { await mkdir(DOCSTORE_DIR, { recursive: true }); await mkdir(AUDIOBOOKS_V1_DIR, { recursive: true }); @@ -284,7 +285,7 @@ export async function ensureAudiobooksV1Ready(): Promise { if (hasExtraKeys) { await saveMigrationState({ audiobooksV1Migrated: true }); } - return; + return false; } let entries: Array = []; @@ -333,6 +334,7 @@ export async function ensureAudiobooksV1Ready(): Promise { const finalLegacyRemaining = await hasLegacyAudiobookDirs(); const finalLegacyChaptersRemaining = await hasLegacyAudiobookChapterLayout(); await saveMigrationState({ audiobooksV1Migrated: !finalLegacyRemaining && !finalLegacyChaptersRemaining }); + return true; } type LegacyChapterMeta = {