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();
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) {

View file

@ -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);
}

View file

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