diff --git a/internal/cloudcp/portal/assets/portal.js b/internal/cloudcp/portal/assets/portal.js index 2f3b9f835..9c4fea09c 100644 --- a/internal/cloudcp/portal/assets/portal.js +++ b/internal/cloudcp/portal/assets/portal.js @@ -180,10 +180,12 @@ async function refreshBootstrap() { var response = await fetch(BOOTSTRAP_PATH, { headers: { 'Accept': 'application/json' } }); - if (!response.ok) return; + if (!response.ok) return false; var data = await response.json(); applyBootstrap(data); + return true; } catch (_) {} + return false; } function showToast(msg, isError) { @@ -569,8 +571,11 @@ async function createWorkspace(accountID) { showToast((err && err.error) || 'Failed to create workspace', true); return; } + if (!await refreshBootstrap()) { + window.location.href = PORTAL_PATH; + return; + } showToast('Workspace created!'); - setTimeout(function() { window.location.reload(); }, 800); } catch(e) { showToast('Network error. Please try again.', true); } finally { @@ -578,26 +583,30 @@ async function createWorkspace(accountID) { } } -function suspendOrDelete(evt, accountID, tenantID, state, name) { +async function suspendOrDelete(evt, accountID, tenantID, state, name) { evt.stopPropagation(); var action = state === 'active' ? 'Suspend' : 'Delete'; if (!confirm(action + ' workspace "' + name + '"?')) return; var method = state === 'active' ? 'PATCH' : 'DELETE'; var body = state === 'active' ? JSON.stringify({ state: 'suspended' }) : undefined; - fetch(ACCOUNT_API_BASE_PATH + '/' + accountID + '/tenants/' + tenantID, { - method: method, - headers: body ? { 'Content-Type': 'application/json' } : {}, - body: body - }).then(function(r) { - if (r.ok) { - showToast(action + 'd workspace.'); - setTimeout(function() { window.location.reload(); }, 800); - } else { + try { + var response = await fetch(ACCOUNT_API_BASE_PATH + '/' + accountID + '/tenants/' + tenantID, { + method: method, + headers: body ? { 'Content-Type': 'application/json' } : {}, + body: body + }); + if (!response.ok) { showToast('Failed to ' + action.toLowerCase() + ' workspace.', true); + return; } - }).catch(function() { + if (!await refreshBootstrap()) { + window.location.href = PORTAL_PATH; + return; + } + showToast(action + 'd workspace.'); + } catch(_) { showToast('Network error.', true); - }); + } } async function openBilling(accountID) { diff --git a/internal/cloudcp/portal/handlers_test.go b/internal/cloudcp/portal/handlers_test.go index d17baf081..b3c0678d5 100644 --- a/internal/cloudcp/portal/handlers_test.go +++ b/internal/cloudcp/portal/handlers_test.go @@ -809,6 +809,7 @@ func TestPortalPageTemplate_AccountServicesRendered(t *testing.T) { `fetch(LICENSE_API_BASE + '/v1/gdpr/export'`, `fetch(LICENSE_API_BASE + '/v1/gdpr/request-delete'`, `fetch(LICENSE_API_BASE + '/v1/gdpr/confirm-delete'`, + `if (!await refreshBootstrap())`, `href="https://pulserelay.pro/refund.html?email=owner%40example.com"`, "commercial account actions now live here", } @@ -820,6 +821,9 @@ func TestPortalPageTemplate_AccountServicesRendered(t *testing.T) { if strings.Contains(html, `const LICENSE_API_BASE = 'https://license.pulserelay.pro';`) { t.Errorf("expected commercial API base URL to be renderer-owned, not hardcoded in the asset") } + if strings.Contains(html, `window.location.reload()`) { + t.Errorf("expected workspace lifecycle to refresh the bootstrap contract instead of forcing a page reload") + } if strings.Contains(html, `await fetch('/auth/logout'`) { t.Errorf("expected portal paths to be renderer-owned, not hardcoded in the asset") }