Files

244 lines
7.2 KiB
JavaScript

const PROFILE_EXPORT_MAGIC = "overlord-build-profile";
function sanitizeProfileName(name) {
if (typeof name !== "string") return "";
const trimmed = name.trim();
if (!trimmed || trimmed.length > 64) return "";
if (!/^[A-Za-z0-9 _.-]+$/.test(trimmed)) return "";
return trimmed;
}
function exportProfileToFile(name, config) {
const payload = {
type: PROFILE_EXPORT_MAGIC,
version: 1,
name,
exportedAt: Date.now(),
config,
};
const json = JSON.stringify(payload, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
const safeName = (name || "build-profile").replace(/[^A-Za-z0-9._-]/g, "_");
a.href = url;
a.download = `${safeName}.json`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}
export function createBuildProfileManager({
elements,
collectFormSettings,
applyFormSettings,
saveFormSettings,
}) {
const {
profileSelect,
profileNameInput,
profileSaveBtn,
profileLoadBtn,
profileDeleteBtn,
profileExportBtn,
profileImportBtn,
profileImportFile,
} = elements;
let buildProfiles = [];
function renderProfileOptions(selectedName = "") {
if (!profileSelect) return;
profileSelect.innerHTML = "";
const placeholder = document.createElement("option");
placeholder.value = "";
placeholder.textContent = "Select a saved profile...";
profileSelect.appendChild(placeholder);
buildProfiles.forEach((profile) => {
const option = document.createElement("option");
option.value = profile.name;
option.textContent = profile.name;
if (selectedName && selectedName === profile.name) option.selected = true;
profileSelect.appendChild(option);
});
}
function getSelectedProfile() {
if (!profileSelect || !profileSelect.value) return null;
return buildProfiles.find((profile) => profile.name === profileSelect.value) || null;
}
async function loadBuildProfiles(selectedName = "") {
try {
const res = await fetch("/api/build/profiles", { credentials: "include" });
if (!res.ok) {
buildProfiles = [];
renderProfileOptions();
return;
}
const data = await res.json();
buildProfiles = Array.isArray(data?.profiles) ? data.profiles : [];
renderProfileOptions(selectedName || profileSelect?.value || "");
} catch (err) {
console.error("Failed to load build profiles:", err);
buildProfiles = [];
renderProfileOptions();
}
}
async function saveCurrentProfile() {
const inputName = profileNameInput?.value || "";
const selectedName = profileSelect?.value || "";
const profileName = sanitizeProfileName(inputName || selectedName);
if (!profileName) {
alert("Enter a valid profile name (1-64 chars; letters, numbers, spaces, . _ -)");
profileNameInput?.focus();
return;
}
const config = collectFormSettings();
const res = await fetch("/api/build/profiles", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ name: profileName, config }),
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || "Failed to save profile");
}
if (profileNameInput) profileNameInput.value = profileName;
await loadBuildProfiles(profileName);
}
async function importProfileFromFile(file) {
const text = await file.text();
let parsed;
try {
parsed = JSON.parse(text);
} catch {
throw new Error("Invalid JSON file");
}
const fallbackName = file.name.replace(/\.json$/i, "");
const importedName = sanitizeProfileName(parsed?.name || fallbackName);
if (!importedName) {
throw new Error("Invalid profile name in imported file");
}
const importedConfig = parsed?.config;
if (!importedConfig || typeof importedConfig !== "object" || Array.isArray(importedConfig)) {
throw new Error("Imported profile is missing a valid config object");
}
applyFormSettings(importedConfig);
saveFormSettings();
if (profileNameInput) profileNameInput.value = importedName;
await saveCurrentProfile();
}
function bindProfileControls() {
if (profileSelect) {
profileSelect.addEventListener("change", () => {
const profile = getSelectedProfile();
if (profileNameInput) {
profileNameInput.value = profile?.name || "";
}
});
}
if (profileSaveBtn) {
profileSaveBtn.addEventListener("click", async () => {
try {
await saveCurrentProfile();
alert("Profile saved.");
} catch (err) {
alert(err?.message || "Failed to save profile");
}
});
}
if (profileLoadBtn) {
profileLoadBtn.addEventListener("click", () => {
const profile = getSelectedProfile();
if (!profile) {
alert("Select a profile to load.");
return;
}
applyFormSettings(profile.config || {});
saveFormSettings();
if (profileNameInput) profileNameInput.value = profile.name;
alert(`Loaded profile: ${profile.name}`);
});
}
if (profileDeleteBtn) {
profileDeleteBtn.addEventListener("click", async () => {
const profile = getSelectedProfile();
if (!profile) {
alert("Select a profile to delete.");
return;
}
if (!confirm(`Delete profile \"${profile.name}\"?`)) {
return;
}
try {
const res = await fetch(`/api/build/profiles/${encodeURIComponent(profile.name)}`, {
method: "DELETE",
credentials: "include",
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data.error || "Failed to delete profile");
}
await loadBuildProfiles();
if (profileNameInput) profileNameInput.value = "";
alert("Profile deleted.");
} catch (err) {
alert(err?.message || "Failed to delete profile");
}
});
}
if (profileExportBtn) {
profileExportBtn.addEventListener("click", () => {
const profile = getSelectedProfile();
if (profile) {
exportProfileToFile(profile.name, profile.config || {});
return;
}
const fallbackName = sanitizeProfileName(profileNameInput?.value || "") || "current-build-profile";
exportProfileToFile(fallbackName, collectFormSettings());
});
}
if (profileImportBtn && profileImportFile) {
profileImportBtn.addEventListener("click", () => profileImportFile.click());
profileImportFile.addEventListener("change", async () => {
const file = profileImportFile.files?.[0];
profileImportFile.value = "";
if (!file) return;
try {
await importProfileFromFile(file);
alert("Profile imported and saved.");
} catch (err) {
alert(err?.message || "Failed to import profile");
}
});
}
}
return {
bindProfileControls,
loadBuildProfiles,
sanitizeProfileName,
};
}