D

Deploy Hub

Catalog every artifact the platform produces, then open a PR on AzDO or deploy to ephemeral, dev, staging, prod, or AWS (S3+CloudFront / Lambda / ECS).

Connecting…
Artifacts
Deployments
Live
Active previews
Targets
8
\n\n" }); el("create-modal").classList.remove("hidden"); } function closeCreate() { el("create-modal").classList.add("hidden"); } async function submitCreate(e) { e.preventDefault(); const form = e.currentTarget; const files = []; el("files-list").querySelectorAll("div.border").forEach(row => { const path = row.querySelector(".file-path").value.trim(); const content = row.querySelector(".file-content").value; if (path) files.push({ path, content }); }); if (files.length === 0) { toast("At least one file is required."); return; } const body = { title: form.title.value.trim(), description: form.description.value, kind: form.kind.value, created_by_role: form.created_by_role.value.trim() || null, scope_tags: form.scope_tags.value.split(",").map(s => s.trim()).filter(Boolean), tags: form.tags.value.split(",").map(s => s.trim()).filter(Boolean), files, }; try { const created = await api("POST", "/artifacts", body); toast(`Created ${created.artifact_id}`); closeCreate(); await refreshAll(); openDetail(created.artifact_id); } catch (e) { toast("Create failed: " + e.message); } } // ---------- Deploy modal ---------- let deployArtifactId = null; function openDeploy(artifactId) { deployArtifactId = artifactId; const radios = el("deploy-targets-radios"); radios.innerHTML = ""; state.targets.forEach((t, i) => { const row = document.createElement("label"); row.className = "flex items-start gap-2 border border-slate-200 rounded-md p-2 cursor-pointer hover:bg-slate-50"; row.innerHTML = `
${escapeHtml(t.name)}${escapeHtml(t.kind)}

${escapeHtml(t.description || "")}

${(t.requires_approval_roles || []).length > 0 ? `

Approval: ${t.requires_approval_roles.map(escapeHtml).join(", ")}

` : ""}
`; radios.appendChild(row); }); radios.querySelectorAll('input[name="target_id"]').forEach(r => { r.addEventListener("change", updateDeployForm); }); el("deploy-result").classList.add("hidden"); el("deploy-error").classList.add("hidden"); el("deploy-form").reset(); updateDeployForm(); el("deploy-modal").classList.remove("hidden"); } function updateDeployForm() { const sel = el("deploy-targets-radios").querySelector('input[name="target_id"]:checked'); if (!sel) return; const target = state.targets.find(t => t.target_id === sel.value); if (!target) return; const isEphemeral = target.kind === "ephemeral"; el("deploy-ttl-row").classList.toggle("hidden", !isEphemeral); const hint = el("deploy-approval-hint"); if ((target.requires_approval_roles || []).length > 0) { hint.textContent = `This target requires owner_role to be one of: ${target.requires_approval_roles.join(", ")}.`; hint.classList.remove("hidden"); } else { hint.classList.add("hidden"); } } function closeDeploy() { el("deploy-modal").classList.add("hidden"); deployArtifactId = null; } async function submitDeploy(e) { e.preventDefault(); if (!deployArtifactId) return; const form = e.currentTarget; const target_id = form.target_id.value; const owner_role = form.owner_role.value.trim() || null; const ttlRaw = form.ttl_minutes.value; const body = { target_id }; if (owner_role) body.owner_role = owner_role; const target = state.targets.find(t => t.target_id === target_id); if (target && target.kind === "ephemeral" && ttlRaw) { body.ttl_minutes = parseInt(ttlRaw, 10); } try { const dep = await api("POST", `/artifacts/${deployArtifactId}/deployments`, body); el("deploy-error").classList.add("hidden"); el("deploy-result").classList.remove("hidden"); el("deploy-result-url").textContent = dep.url || "(no url)"; el("deploy-result-logs").textContent = (dep.logs || []).join("\n"); const openLink = el("deploy-result-open"); if (dep.url) { openLink.href = dep.url; openLink.classList.remove("hidden"); } else { openLink.classList.add("hidden"); } toast(`Deployed ${dep.deployment_id}`); await refreshAll(); } catch (err) { const box = el("deploy-error"); box.textContent = err.message; box.classList.remove("hidden"); el("deploy-result").classList.add("hidden"); } } async function doRollback(depId) { if (!confirm(`Rollback ${depId}? This re-deploys the previous artifact for the same target and marks this deployment as rolled-back.`)) return; try { const newDep = await api("POST", `/deployments/${depId}/rollback`); toast(`Rolled back to ${newDep.deployment_id}`); await refreshAll(); } catch (err) { toast("Rollback failed: " + err.message); } } // ---------- Refreshers ---------- async function refreshArtifacts() { try { state.artifacts = await api("GET", "/artifacts"); renderArtifacts(); } catch (e) { setConn(false, e.message); throw e; } } async function refreshDeployments() { try { const arts = await api("GET", "/artifacts"); state.artifacts = arts; // Pull deployments for each artifact in parallel. Small fan-out is // fine — this is a v0 catalog, not a high-volume CI system. const all = []; const pending = arts.map(a => api("GET", `/artifacts/${a.artifact_id}/deployments`) .then(list => list.forEach(d => all.push(d))) .catch(() => {}) ); await Promise.all(pending); all.sort((a, b) => new Date(b.started_at) - new Date(a.started_at)); state.deployments = all.slice(0, 100); renderDeployments(); } catch (e) { setConn(false, e.message); } } async function refreshTargets() { state.targets = await api("GET", "/targets"); renderTargets(); } async function refreshStats() { try { state.stats = await api("GET", "/stats"); } catch (_) { state.stats = null; } renderStats(); } async function refreshAll() { try { await Promise.all([refreshArtifacts(), refreshTargets(), refreshStats()]); if (state.activeTab === "deployments") await refreshDeployments(); setConn(true); } catch (e) { setConn(false, e.message); } } // ---------- Wiring ---------- function wireFilters() { document.querySelectorAll(".filter-chip[data-filter]").forEach(btn => { btn.addEventListener("click", () => { const filter = btn.dataset.filter; const value = btn.dataset.value; state.filters[filter] = value; document.querySelectorAll(`.filter-chip[data-filter="${filter}"]`).forEach(b => { b.classList.toggle("active", b.dataset.value === value); }); renderArtifacts(); }); }); el("filter-scope-tag").addEventListener("input", e => { state.filters.scope_tag = e.target.value.trim(); renderArtifacts(); }); el("filter-owner-role").addEventListener("input", e => { state.filters.owner_role = e.target.value.trim(); renderArtifacts(); }); el("filter-clear-btn").addEventListener("click", () => { state.filters = { kind: "", status: "", scope_tag: "", owner_role: "" }; el("filter-scope-tag").value = ""; el("filter-owner-role").value = ""; document.querySelectorAll(".filter-chip[data-filter]").forEach(b => { b.classList.toggle("active", b.dataset.value === ""); }); renderArtifacts(); }); } function wireModals() { el("new-artifact-btn").addEventListener("click", openCreate); el("create-close-btn").addEventListener("click", closeCreate); el("create-cancel-btn").addEventListener("click", closeCreate); el("create-form").addEventListener("submit", submitCreate); el("add-file-btn").addEventListener("click", () => addFileRow()); el("detail-close-btn").addEventListener("click", closeDetail); el("detail-deploy-btn").addEventListener("click", () => { if (state.currentArtifact) { openDeploy(state.currentArtifact.artifact_id); } }); el("deploy-close-btn").addEventListener("click", closeDeploy); el("deploy-cancel-btn").addEventListener("click", closeDeploy); el("deploy-form").addEventListener("submit", submitDeploy); el("deploy-result-copy").addEventListener("click", () => { const txt = el("deploy-result-url").textContent; navigator.clipboard.writeText(txt).then(() => toast("Copied")); }); el("deployments-refresh-btn").addEventListener("click", refreshDeployments); } function wireTabs() { window.addEventListener("hashchange", () => setActiveTab(readHash())); setActiveTab(readHash()); } // ---------- Boot ---------- async function boot() { wireFilters(); wireModals(); wireTabs(); try { await refreshAll(); } catch (_) { /* setConn already called */ } } document.addEventListener("DOMContentLoaded", boot); })();