Files
DoGaMa-serv/internal/web/static/app.v1.js
T
codex 39a33556d6
CI / validate (pull_request) Successful in 26m0s
chore(code): improve source readability
2026-08-15 23:09:59 +02:00

118 lines
5.9 KiB
JavaScript

document.querySelectorAll(".menu-button:not([data-menu-bound])").forEach((button) => {
button.dataset.menuBound = "true";
button.addEventListener("click", () => {
const open = document.body.classList.toggle("nav-open");
button.setAttribute("aria-expanded", String(open));
});
});
const instanceSearch = document.querySelector("#instance-search");
if (instanceSearch) {
const cards = [...document.querySelectorAll("[data-instance-card]")];
const empty = document.querySelector("[data-search-empty]");
const filter = () => {
const query = instanceSearch.value.trim().toLocaleLowerCase();
let visible = 0;
cards.forEach((card) => {
const matches = card.dataset.search.toLocaleLowerCase().includes(query);
card.hidden = !matches;
visible += Number(matches);
});
empty.hidden = visible !== 0;
};
instanceSearch.addEventListener("input", filter);
document.addEventListener("keydown", (event) => {
if (event.key === "/" && document.activeElement !== instanceSearch) {
event.preventDefault();
instanceSearch.focus();
}
});
}
const deploymentForm = document.querySelector("[data-deployment-form]");
if (deploymentForm) {
deploymentForm.addEventListener("submit", (event) => {
event.preventDefault();
const submit = deploymentForm.querySelector("[data-deployment-submit]");
if (submit) {
submit.disabled = true;
submit.textContent = "…";
}
let progress = document.querySelector("[data-deployment-progress]");
if (!progress) {
progress = document.createElement("section");
progress.className = "panel deployment-progress";
progress.dataset.deploymentProgress = "";
deploymentForm.after(progress);
}
progress.replaceChildren();
const heading = document.createElement("h2");
heading.textContent = "Deployment progress";
const status = document.createElement("p");
status.dataset.operationStatus = "";
const list = document.createElement("ol");
list.dataset.operationSteps = "";
const error = document.createElement("p");
error.className = "error";
error.hidden = true;
const link = document.createElement("a");
link.hidden = true;
progress.append(heading, status, list, error, link);
fetch(deploymentForm.action || window.location.href, { method: "POST", body: new FormData(deploymentForm), credentials: "same-origin", headers: { Accept: "application/json" } }).then((response) => response.ok ? response.json() : Promise.reject(new Error("Deployment could not be queued."))).then(({ operation_id, instance_id }) => {
progress.hidden = false; deploymentForm.hidden = true;
const poll = () => fetch(`/api/v1/operations/${encodeURIComponent(operation_id)}`, { credentials: "same-origin" }).then((r) => r.ok ? r.json() : Promise.reject(new Error("Progress unavailable."))).then((operation) => {
status.textContent = operation.global_status; list.replaceChildren(...operation.steps.map((step) => { const item = document.createElement("li"); item.textContent = `${step.status === "success" ? "✓" : step.status === "failed" ? "✗" : step.status === "running" ? "▶" : "○"} ${step.id}`; return item; }));
if (operation.global_status === "failed") { error.hidden = false; error.textContent = `Deployment failed${operation.error_code ? `. Code: ${operation.error_code}` : "."}`; return; }
if (operation.global_status === "success") { link.hidden = false; link.href = `/instances/${encodeURIComponent(instance_id)}`; link.textContent = "Open instance"; return; }
window.setTimeout(poll, 800);
}).catch((reason) => { error.hidden = false; error.textContent = reason.message; }); poll();
}).catch((reason) => { if (submit) { submit.disabled = false; submit.textContent = "Go"; } alert(reason.message); });
});
}
// The backend, not a timer, is the source for this history. A 403 simply
// means the signed-in user is not an administrator and leaves no technical
// data in the DOM.
const detailMatch = window.location.pathname.match(/^\/instances\/([^/]+)$/);
if (detailMatch) {
fetch(`/api/v1/instances/${encodeURIComponent(detailMatch[1])}/diagnostics`, { credentials: "same-origin" })
.then((response) => response.ok ? response.json() : null)
.then((data) => {
if (!data || !data.operations || !data.operations.length) return;
const section = document.createElement("section");
section.className = "panel";
section.innerHTML = "<h2>Operation diagnostics</h2><p class=muted>Administrator-only technical history.</p>";
data.operations.forEach((operation) => {
const item = document.createElement("details");
const summary = document.createElement("summary");
summary.textContent = `${operation.created_at} · ${operation.kind} · ${operation.state}${operation.error_code ? ` · ${operation.error_code}` : ""} · ${operation.operation_id}`;
item.append(summary);
if (operation.diagnostic) { const detail = document.createElement("pre"); detail.textContent = operation.diagnostic.details; item.append(detail); }
section.append(item);
});
document.querySelector(".instance-detail-lower")?.append(section);
}).catch(() => {});
}
const catalogSearch = document.querySelector("#catalog-search");
if (catalogSearch) {
const cards = [...document.querySelectorAll("[data-catalog-card]")];
const empty = document.querySelector("[data-catalog-search-empty]");
const filter = () => {
const query = catalogSearch.value.trim().toLocaleLowerCase();
let visible = 0;
cards.forEach((card) => {
const matches = card.dataset.search.toLocaleLowerCase().includes(query);
card.hidden = !matches;
visible += Number(matches);
});
empty.hidden = visible !== 0;
};
catalogSearch.addEventListener("input", filter);
document.addEventListener("keydown", (event) => {
if (event.key === "/" && document.activeElement !== catalogSearch) {
event.preventDefault(); catalogSearch.focus();
}
});
}