(function () { const roots = document.querySelectorAll(".spp-portal[data-rest-url]"); roots.forEach((root) => { const api = root.dataset.restUrl; const nonce = root.dataset.nonce; const permissions = new Set(JSON.parse(root.dataset.permissions || "[]")); const can = (permission) => permissions.has(permission); let state = { view: "deployments", deployments: [], templates: [], quota: null, selectedDeployment: null, error: null, loading: true }; const request = async (path, options = {}) => { const response = await fetch(`${api}${path}`, { ...options, headers: { "Content-Type": "application/json", "X-WP-Nonce": nonce, ...(options.headers || {}) } }); const payload = await response.json().catch(() => null); if (!response.ok) { throw new Error(payload && payload.message ? payload.message : `Request failed with ${response.status}`); } return payload; }; const load = async () => { state = { ...state, loading: true, error: null }; render(); try { const [deployments, templates, quota] = await Promise.all([ request("/deployments"), request("/templates"), request("/quota") ]); state = { ...state, deployments, templates, quota, loading: false }; } catch (error) { state = { ...state, error: error.message, loading: false }; } render(); }; const lifecycle = async (id, action) => { try { const path = action === "delete" ? `/deployments/${id}` : `/deployments/${id}/${action}`; await request(path, { method: action === "delete" ? "DELETE" : "POST" }); await load(); state.selectedDeployment = action === "delete" ? null : await request(`/deployments/${id}`); state.view = action === "delete" ? "deployments" : "detail"; } catch (error) { state = { ...state, error: error.message }; } render(); }; const prolongDeployment = async (event, id) => { event.preventDefault(); const form = new FormData(event.currentTarget); try { const deployment = await request(`/deployments/${id}/prolong`, { method: "POST", body: JSON.stringify({ ttlHours: form.get("ttlHours") ? Number(form.get("ttlHours")) : undefined, neverExpire: form.get("neverExpire") === "1" }) }); await load(); state = { ...state, view: "detail", selectedDeployment: deployment }; } catch (error) { state = { ...state, error: error.message }; } render(); }; const createDeployment = async (event) => { event.preventDefault(); const form = new FormData(event.currentTarget); try { const deployment = await request("/deployments", { method: "POST", body: JSON.stringify({ templateId: Number(form.get("templateId")), name: String(form.get("name")), ttlHours: form.get("ttlHours") ? Number(form.get("ttlHours")) : undefined, neverExpire: form.get("neverExpire") === "1" }) }); await load(); state = { ...state, view: "detail", selectedDeployment: deployment }; } catch (error) { state = { ...state, error: error.message }; } render(); }; const shareDeployment = async (event, id) => { event.preventDefault(); const form = new FormData(event.currentTarget); try { await request(`/deployments/${id}/shares`, { method: "POST", body: JSON.stringify({ user: String(form.get("user")) }) }); state.selectedDeployment = await request(`/deployments/${id}`); state.error = null; } catch (error) { state = { ...state, error: error.message }; } render(); }; const removeShare = async (id, userId) => { try { await request(`/deployments/${id}/shares/${userId}`, { method: "DELETE" }); state.selectedDeployment = await request(`/deployments/${id}`); state.error = null; } catch (error) { state = { ...state, error: error.message }; } render(); }; const statusBadge = (status) => `${status.replace("_", " ")}`; const accessLabel = (deployment) => { if (deployment.accessType === "shared") { return 'SHARED'; } if (deployment.accessType === "admin") { return 'ADMIN'; } return 'OWNER'; }; const actionButton = (permission, action, label, id, className = "", disabled = false) => { if (!can(permission)) { return ""; } return ``; }; const ipList = (ips) => { if (!Array.isArray(ips) || ips.length === 0) { return "Pending"; } return ips.map(escapeHtml).join(", "); }; const dateTime = (value) => { if (!value) { return "Never"; } return new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" }).format(new Date(value.replace(" ", "T"))); }; const quotaLine = () => { if (!state.quota) { return ""; } const userLimit = state.quota.userLimitMb > 0 ? `${state.quota.userLimitMb} MB` : "Unlimited"; const globalLimit = state.quota.globalLimitMb > 0 ? `${state.quota.globalLimitMb} MB` : "Unlimited"; return `
Your RAM: ${state.quota.userUsedMb} MB / ${userLimit} Global RAM: ${state.quota.globalUsedMb} MB / ${globalLimit}
`; }; const header = () => `

Support Provisioning Portal

Template-based VM provisioning for support work.

${quotaLine()}
${can("create_deployments") ? '' : ""}
`; const deploymentsView = () => { if (state.deployments.length === 0) { return '

No deployments have been created yet.

'; } return `
${state.deployments.map((deployment) => ` `).join("")}
NameStatusAccessTemplateIP addressesExpires
${escapeHtml(deployment.name)} ${statusBadge(deployment.status)} ${accessLabel(deployment)} ${escapeHtml(deployment.templateName)} ${ipList(deployment.ipAddresses)} ${dateTime(deployment.expiresAt)}
`; }; const templatesView = () => `
${state.templates.map((template) => `

${escapeHtml(template.name)}

${escapeHtml(template.description)}

OS${template.osType} CPU${template.cpuCores} cores Memory${template.memoryMb} MB Disk${template.diskGb} GB Default TTL${template.defaultTtlHours}h
`).join("")}
`; const createView = () => { if (!can("create_deployments")) { return '

You do not have permission to create deployments.

'; } return `
`; }; const detailView = () => { const deployment = state.selectedDeployment; if (!deployment) { return deploymentsView(); } const shares = Array.isArray(deployment.shares) ? deployment.shares : []; return `
${deployment.status === "EXPIRED" ? `
This deployment is expired${deployment.errorMessage ? "" : " and has been stopped"}. Prolong its TTL to unlock start actions, or delete it when the data is no longer needed. ${deployment.errorMessage ? `
Stop warning: ${escapeHtml(deployment.errorMessage)}` : ""}
` : ""}

${escapeHtml(deployment.name)}

${statusBadge(deployment.status)}
${actionButton("start_deployments", "start", "Start", deployment.id, "spp-button-primary", deployment.status === "RUNNING" || deployment.status === "EXPIRED")} ${actionButton("stop_deployments", "stop", "Stop", deployment.id, "", deployment.status === "STOPPED" || deployment.status === "EXPIRED")} ${actionButton("refresh_deployment_ips", "refresh-ips", "Refresh IPs", deployment.id, "", deployment.status === "DELETED")} ${deployment.canDelete ? actionButton("delete_deployments", "delete", "Delete", deployment.id, "spp-button-danger", deployment.status === "DELETED") : ""}
Template${escapeHtml(deployment.templateName)} Requested by${escapeHtml(deployment.requestedByName)} Access${deployment.accessType ? escapeHtml(deployment.accessType) : "Owner"} Proxmox VM ID${deployment.proxmoxVmId || "Pending"} IP addresses${ipList(deployment.ipAddresses)} CPU${deployment.cpuCores} cores Memory${deployment.memoryMb} MB Disk${deployment.diskGb} GB Created${dateTime(deployment.createdAt)} Expires${dateTime(deployment.expiresAt)}
${can("prolong_deployments") ? `

Prolong TTL

` : ""} ${deployment.canShare ? `

Shared Access

${shares.length === 0 ? '

No users have shared access.

' : shares.map((share) => `
${escapeHtml(share.displayName || share.userLogin)} ${escapeHtml(share.userLogin)}${share.userEmail ? ` - ${escapeHtml(share.userEmail)}` : ""}
`).join("")}
` : ""}
`; }; const render = () => { root.innerHTML = ` ${header()} ${state.error ? `

${escapeHtml(state.error)}

` : ""} ${state.loading ? '

Loading...

' : ""} ${!state.loading && state.view === "deployments" ? deploymentsView() : ""} ${!state.loading && state.view === "templates" ? templatesView() : ""} ${!state.loading && state.view === "create" ? createView() : ""} ${!state.loading && state.view === "detail" ? detailView() : ""} `; root.querySelectorAll("[data-view]").forEach((button) => { button.addEventListener("click", () => { state = { ...state, view: button.dataset.view, selectedDeployment: null }; render(); }); }); root.querySelectorAll("[data-detail]").forEach((button) => { button.addEventListener("click", async () => { try { state.selectedDeployment = await request(`/deployments/${button.dataset.detail}`); state.view = "detail"; } catch (error) { state.error = error.message; } render(); }); }); root.querySelectorAll("[data-action]").forEach((button) => { button.addEventListener("click", () => lifecycle(Number(button.dataset.id), button.dataset.action)); }); root.querySelectorAll("[data-remove-share]").forEach((button) => { button.addEventListener("click", () => removeShare(Number(button.dataset.id), Number(button.dataset.removeShare))); }); const form = root.querySelector("#spp-create-form"); if (form) { form.addEventListener("submit", createDeployment); const neverExpire = form.querySelector('input[name="neverExpire"]'); const ttlHours = form.querySelector('input[name="ttlHours"]'); if (neverExpire && ttlHours) { neverExpire.addEventListener("change", () => { ttlHours.disabled = neverExpire.checked; if (neverExpire.checked) { ttlHours.value = ""; } }); } } root.querySelectorAll("[data-prolong-form]").forEach((form) => { form.addEventListener("submit", (event) => prolongDeployment(event, Number(form.dataset.prolongForm))); const neverExpire = form.querySelector('input[name="neverExpire"]'); const ttlHours = form.querySelector('input[name="ttlHours"]'); if (neverExpire && ttlHours) { neverExpire.addEventListener("change", () => { ttlHours.disabled = neverExpire.checked; if (neverExpire.checked) { ttlHours.value = ""; } }); } }); root.querySelectorAll("[data-share-form]").forEach((form) => { form.addEventListener("submit", (event) => shareDeployment(event, Number(form.dataset.shareForm))); }); }; const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (character) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[character])); load(); }); })();