From 82141d8e222229246f016f9b5991691ad22de963 Mon Sep 17 00:00:00 2001 From: Joshua Date: Wed, 18 Feb 2026 14:23:37 -0800 Subject: [PATCH] Disable owner-only launcher actions until attested --- README.md | 1 + app/app.js | 19 +++++++++++++++++++ app/style.css | 6 ++++++ 3 files changed, 26 insertions(+) diff --git a/README.md b/README.md index 60752bb..2b403c1 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ Policy behavior in launcher shell: 1. Membership is required for all member-channel polling flows. 2. `onramp_attested` identity assurance is required for owner support-ticket and governance install-token actions. 3. Assurance state is displayed independently from membership state in the top summary cards. +4. Owner-only buttons are UI-disabled until both membership is active and assurance is `onramp_attested`. Run locally: diff --git a/app/app.js b/app/app.js index 5ccb890..0789d6c 100644 --- a/app/app.js +++ b/app/app.js @@ -83,6 +83,23 @@ function setSummary(id, value) { el.textContent = value; } +function setButtonDisabled(id, disabled) { + const el = $(id); + if (!el) return; + el.disabled = Boolean(disabled); +} + +function refreshActionLocks(statusPayload) { + const effective = statusPayload && typeof statusPayload === "object" ? statusPayload : state.lastStatus; + const membershipActive = String(effective?.status || "").toLowerCase() === "active"; + const attested = isOnrampAttested(effective?.identity_assurance_level); + const ownerActionReady = membershipActive && attested; + + setButtonDisabled("btnSupportTicket", !ownerActionReady); + setButtonDisabled("btnInstallToken", !ownerActionReady); + setButtonDisabled("btnQuickInstallStatus", !membershipActive); +} + function refreshOverview(statusPayload) { const currentWallet = wallet(); setSummary("summaryWallet", currentWallet || "not connected"); @@ -93,6 +110,7 @@ function refreshOverview(statusPayload) { setSummary("summaryAssurance", assurance); setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked"); setSummary("summaryLastSync", nowISO()); + refreshActionLocks(statusPayload); return; } const designation = $("designationCode")?.value?.trim() || "-"; @@ -100,6 +118,7 @@ function refreshOverview(statusPayload) { const assurance = assuranceDisplay(state.lastStatus?.identity_assurance_level); setSummary("summaryAssurance", assurance); setSummary("summaryAdminPolicy", isOnrampAttested(assurance) ? "ready" : "blocked"); + refreshActionLocks(); } function setFlowStatus(message) { diff --git a/app/style.css b/app/style.css index e48ff13..dd58061 100644 --- a/app/style.css +++ b/app/style.css @@ -153,6 +153,12 @@ button:active { transform: translateY(1px); } +button:disabled { + cursor: not-allowed; + filter: grayscale(0.5); + opacity: 0.55; +} + .event-list { display: grid; gap: 8px;