Files
Church-Music/legacy-site/frontend/src/migration.js

318 lines
9.7 KiB
JavaScript

import { localStorageAPI, STORAGE_KEYS } from "./localStorage";
// Migration utility to copy localStorage data to backend
export async function migrateLocalStorageToBackend(customBase) {
// Determine API base dynamically from saved settings unless overridden
const apiSettings = (() => {
try {
return JSON.parse(localStorage.getItem("api_settings") || "{}");
} catch {
return {};
}
})();
const API_BASE = (() => {
if (customBase) return customBase;
if (apiSettings.protocol && apiSettings.hostname) {
const p = apiSettings.port || "";
const portPart = p ? `:${p}` : "";
return `${apiSettings.protocol}://${apiSettings.hostname}${portPart}/api`;
}
return `https://houseofprayer.ddns.net:8080/api`;
})();
console.log("[Migration] Upload API Base:", API_BASE);
try {
// Preload existing backend data to minimize duplicates
let existingSongs = [];
let existingProfiles = [];
let existingPlans = [];
try {
console.log("[Migration] Fetching existing backend data...");
const [songsRes, profilesRes, plansRes] = await Promise.all([
fetch(`${API_BASE}/songs`).then((r) => (r.ok ? r.json() : [])),
fetch(`${API_BASE}/profiles`).then((r) => (r.ok ? r.json() : [])),
fetch(`${API_BASE}/plans`).then((r) => (r.ok ? r.json() : [])),
]);
existingSongs = songsRes;
existingProfiles = profilesRes;
existingPlans = plansRes;
} catch (e) {}
const existingSongTitles = new Set(
existingSongs.map((s) => (s.title || "").toLowerCase())
);
const existingProfileIds = new Set(existingProfiles.map((p) => p.id));
const existingPlanDates = new Set(existingPlans.map((pl) => pl.date));
// 1. Migrate Songs
const songs = await localStorageAPI.getSongs("");
for (const song of songs) {
if (existingSongTitles.has((song.title || "").toLowerCase())) {
continue;
}
try {
const res = await fetch(`${API_BASE}/songs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(song),
});
if (res.ok) {
} else {
}
} catch (err) {}
}
// 2. Migrate Profiles
const profiles = await localStorageAPI.getProfiles();
for (const profile of profiles) {
if (existingProfileIds.has(profile.id)) {
continue;
}
try {
const res = await fetch(`${API_BASE}/profiles`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(profile),
});
if (res.ok) {
} else {
}
} catch (err) {}
}
// 3. Migrate Plans
const plans = await localStorageAPI.getPlans();
for (const plan of plans) {
if (existingPlanDates.has(plan.date)) {
continue;
}
try {
const planSongs = await localStorageAPI.getPlanSongs(plan.id);
const songsData = [];
for (const ps of planSongs) {
const song = await localStorageAPI.getSong(ps.song_id);
if (song) songsData.push({ ...song, order: ps.order_index });
}
const res = await fetch(`${API_BASE}/plans`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ...plan, songs: songsData }),
});
if (res.ok) {
} else {
}
} catch (err) {}
}
// 4. Migrate Profile Songs
const allProfileSongs = JSON.parse(
localStorage.getItem(STORAGE_KEYS.PROFILE_SONGS) || "[]"
);
for (const ps of allProfileSongs) {
try {
const res = await fetch(`${API_BASE}/profiles/${ps.profile_id}/songs`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ song_id: ps.song_id }),
});
if (res.ok) {
} else {
}
} catch (err) {
console.error(
`❌ Failed to migrate profile-song (profile ${ps.profile_id} song ${ps.song_id}):`,
err
);
}
}
// 5. Migrate Profile Song Keys (array form in localStorage)
const profileSongKeysArr = JSON.parse(
localStorage.getItem(STORAGE_KEYS.PROFILE_SONG_KEYS) || "[]"
);
for (const entry of profileSongKeysArr) {
if (!entry.profile_id || !entry.song_id || !entry.key) continue;
try {
const res = await fetch(
`${API_BASE}/profiles/${entry.profile_id}/songs/${entry.song_id}/key`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: entry.key }),
}
);
if (res.ok) {
console.log(
`✅ Migrated key for profile ${entry.profile_id} song ${entry.song_id}`
);
} else {
console.warn(
`⚠️ Failed to migrate key for profile ${entry.profile_id} song ${entry.song_id}`
);
}
} catch (err) {
console.error(
`❌ Failed to migrate key (profile ${entry.profile_id} song ${entry.song_id}):`,
err
);
}
}
return {
success: true,
migrated: {
songs: songs.length,
profiles: profiles.length,
plans: plans.length,
profileSongs: allProfileSongs.length,
keys: profileSongKeysArr.length,
},
};
} catch (error) {
return { success: false, error: error.message };
}
}
// Sync FROM backend TO localStorage (pull down all data)
export async function syncFromBackendToLocalStorage(customBase) {
const apiSettings = (() => {
try {
return JSON.parse(localStorage.getItem("api_settings") || "{}");
} catch {
return {};
}
})();
const API_BASE = (() => {
if (customBase) return customBase;
if (apiSettings.protocol && apiSettings.hostname) {
const p = apiSettings.port || "";
const portPart = p ? `:${p}` : "";
return `${apiSettings.protocol}://${apiSettings.hostname}${portPart}/api`;
}
return `https://houseofprayer.ddns.net:8080/api`;
})();
console.log("[Migration] Download API Base:", API_BASE);
try {
// 1. Sync Songs
const songsRes = await fetch(`${API_BASE}/songs`);
if (!songsRes.ok)
throw new Error(`Failed to fetch songs: ${songsRes.status}`);
const backendSongs = await songsRes.json();
const localSongs = await localStorageAPI.getSongs("");
const localSongIds = new Set(localSongs.map((s) => s.id));
let songsAdded = 0;
let songsUpdated = 0;
for (const song of backendSongs) {
// Fetch full song details
try {
const detailRes = await fetch(`${API_BASE}/songs/${song.id}`);
if (!detailRes.ok) {
continue;
}
const fullSong = await detailRes.json();
if (!fullSong.lyrics && !fullSong.chords) {
}
if (localSongIds.has(fullSong.id)) {
await localStorageAPI.updateSong(fullSong.id, fullSong);
songsUpdated++;
} else {
await localStorageAPI.createSong(fullSong);
songsAdded++;
}
} catch (err) {}
}
// 2. Sync Profiles
const profilesRes = await fetch(`${API_BASE}/profiles`);
if (!profilesRes.ok)
throw new Error(`Failed to fetch profiles: ${profilesRes.status}`);
const backendProfiles = await profilesRes.json();
const localProfiles = await localStorageAPI.getProfiles();
const localProfileIds = new Set(localProfiles.map((p) => p.id));
let profilesAdded = 0;
let profilesUpdated = 0;
for (const profile of backendProfiles) {
if (localProfileIds.has(profile.id)) {
await localStorageAPI.updateProfile(profile.id, profile);
profilesUpdated++;
} else {
await localStorageAPI.createProfile(profile);
profilesAdded++;
}
}
// 3. Sync Plans
const plansRes = await fetch(`${API_BASE}/plans`);
if (!plansRes.ok)
throw new Error(`Failed to fetch plans: ${plansRes.status}`);
const backendPlans = await plansRes.json();
const localPlans = await localStorageAPI.getPlans();
const localPlanIds = new Set(localPlans.map((p) => p.id));
let plansAdded = 0;
let plansUpdated = 0;
for (const plan of backendPlans) {
if (localPlanIds.has(plan.id)) {
await localStorageAPI.updatePlan(plan.id, plan);
plansUpdated++;
} else {
await localStorageAPI.createPlan(plan);
plansAdded++;
}
}
return {
success: true,
synced: {
songs: { added: songsAdded, updated: songsUpdated },
profiles: { added: profilesAdded, updated: profilesUpdated },
plans: { added: plansAdded, updated: plansUpdated },
},
};
} catch (error) {
return { success: false, error: error.message };
}
}
// Full bidirectional sync: merge localStorage and backend data
export async function fullSync(customBase) {
// First, push local changes to backend
const uploadResult = await migrateLocalStorageToBackend(customBase);
// Then, pull backend changes to local
const downloadResult = await syncFromBackendToLocalStorage(customBase);
// Trigger page refresh to show updated data without full reload
window.dispatchEvent(new CustomEvent("dataFullySynced"));
// Also trigger events that the app already listens for
window.dispatchEvent(new Event("songsChanged"));
window.dispatchEvent(new Event("plansChanged"));
return {
success: uploadResult.success && downloadResult.success,
upload: uploadResult,
download: downloadResult,
};
}
// Export functions to call from browser console
window.migrateData = migrateLocalStorageToBackend;
window.syncFromBackend = syncFromBackendToLocalStorage;
window.fullSync = fullSync;