Initial commit - Church Music Database
This commit is contained in:
317
legacy-site/frontend/src/migration.js
Normal file
317
legacy-site/frontend/src/migration.js
Normal file
@@ -0,0 +1,317 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user