feat: Display a toast notification after library migration, powered by new migration status in the API response.

This commit is contained in:
Richard R 2026-01-19 15:27:07 -07:00
parent f11c015f7e
commit 741126c0b4
3 changed files with 33 additions and 9 deletions

View file

@ -114,8 +114,8 @@ export async function POST(request: NextRequest) {
} }
} }
await ensureDocumentsV1Ready(); const documentsMigrated = await ensureDocumentsV1Ready();
await ensureAudiobooksV1Ready(); const audiobooksMigrated = await ensureAudiobooksV1Ready();
const rekey = await rekeyAudiobooksV1(mappings); const rekey = await rekeyAudiobooksV1(mappings);
const documentsReady = await isDocumentsV1Ready(); const documentsReady = await isDocumentsV1Ready();
@ -125,6 +125,8 @@ export async function POST(request: NextRequest) {
success: true, success: true,
documentsReady, documentsReady,
audiobooksReady, audiobooksReady,
documentsMigrated,
audiobooksMigrated,
rekey, rekey,
}); });
} catch (error) { } catch (error) {

View file

@ -103,11 +103,31 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
// Run server-side v1 migrations proactively, since the client may now // Run server-side v1 migrations proactively, since the client may now
// reference SHA-based IDs immediately after the Dexie migration. // reference SHA-based IDs immediately after the Dexie migration.
await fetch('/api/migrations/v1', { const response = await fetch('/api/migrations/v1', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mappings }), 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) { } catch (error) {
console.warn('Startup migrations failed:', error); console.warn('Startup migrations failed:', error);
} }

View file

@ -101,18 +101,18 @@ function safeDocumentName(rawName: string, fallback: string): string {
return baseName.replaceAll('\u0000', '').slice(0, 240) || fallback; return baseName.replaceAll('\u0000', '').slice(0, 240) || fallback;
} }
export async function ensureDocumentsV1Ready(): Promise<void> { export async function ensureDocumentsV1Ready(): Promise<boolean> {
await mkdir(DOCSTORE_DIR, { recursive: true }); await mkdir(DOCSTORE_DIR, { recursive: true });
await mkdir(DOCUMENTS_V1_DIR, { recursive: true }); await mkdir(DOCUMENTS_V1_DIR, { recursive: true });
const state = await loadMigrationState(); const state = await loadMigrationState();
if (state.documentsV1Migrated && !(await hasLegacyDocumentFiles())) { if (state.documentsV1Migrated && !(await hasLegacyDocumentFiles())) {
return; return false;
} }
if (!(await hasLegacyDocumentFiles())) { if (!(await hasLegacyDocumentFiles())) {
await saveMigrationState({ documentsV1Migrated: true }); await saveMigrationState({ documentsV1Migrated: true });
return; return false;
} }
let entries: Array<import('fs').Dirent> = []; let entries: Array<import('fs').Dirent> = [];
@ -165,6 +165,7 @@ export async function ensureDocumentsV1Ready(): Promise<void> {
} }
await saveMigrationState({ documentsV1Migrated: !(await hasLegacyDocumentFiles()) }); await saveMigrationState({ documentsV1Migrated: !(await hasLegacyDocumentFiles()) });
return true;
} }
async function hasLegacyAudiobookDirs(): Promise<boolean> { async function hasLegacyAudiobookDirs(): Promise<boolean> {
@ -269,7 +270,7 @@ async function mergeDirectoryContents(sourceDir: string, targetDir: string): Pro
return { moved, skipped }; return { moved, skipped };
} }
export async function ensureAudiobooksV1Ready(): Promise<void> { export async function ensureAudiobooksV1Ready(): Promise<boolean> {
await mkdir(DOCSTORE_DIR, { recursive: true }); await mkdir(DOCSTORE_DIR, { recursive: true });
await mkdir(AUDIOBOOKS_V1_DIR, { recursive: true }); await mkdir(AUDIOBOOKS_V1_DIR, { recursive: true });
@ -284,7 +285,7 @@ export async function ensureAudiobooksV1Ready(): Promise<void> {
if (hasExtraKeys) { if (hasExtraKeys) {
await saveMigrationState({ audiobooksV1Migrated: true }); await saveMigrationState({ audiobooksV1Migrated: true });
} }
return; return false;
} }
let entries: Array<import('fs').Dirent> = []; let entries: Array<import('fs').Dirent> = [];
@ -333,6 +334,7 @@ export async function ensureAudiobooksV1Ready(): Promise<void> {
const finalLegacyRemaining = await hasLegacyAudiobookDirs(); const finalLegacyRemaining = await hasLegacyAudiobookDirs();
const finalLegacyChaptersRemaining = await hasLegacyAudiobookChapterLayout(); const finalLegacyChaptersRemaining = await hasLegacyAudiobookChapterLayout();
await saveMigrationState({ audiobooksV1Migrated: !finalLegacyRemaining && !finalLegacyChaptersRemaining }); await saveMigrationState({ audiobooksV1Migrated: !finalLegacyRemaining && !finalLegacyChaptersRemaining });
return true;
} }
type LegacyChapterMeta = { type LegacyChapterMeta = {