feat(launcher): promote control surface and isolate advanced integration panel

This commit is contained in:
Joshua 2026-02-18 10:06:26 -08:00
parent 06e48d7aa4
commit 9b989bd735
4 changed files with 394 additions and 192 deletions

View File

@ -21,17 +21,27 @@ Launcher never contains private kernel internals. It verifies and installs signe
## Local Harness (Current)
`app/index.html` is a local launcher shell harness for end-to-end API validation:
`app/index.html` now exposes a product-first control surface with advanced harness tooling preserved.
1. Wallet intent + verify
2. Membership quote + confirm
3. Member channel register/poll/ack/support
4. Governance install token/confirm/status
5. Lease heartbeat + offline renew
6. Injected wallet automation for intent signing and membership tx send
7. One-click `Run membership flow` path (intent -> verify -> quote -> tx -> confirm)
Top-level control surface:
Wallet automation shortcuts in the shell:
1. `Connect wallet`
2. `Activate membership`
3. `Refresh status + feed`
4. `Governance status`
5. Wallet/membership/designation/last-sync overview cards
6. Pull-first updates feed + support ticket action
Advanced integration controls (collapsible):
1. API/chain connection settings
2. Wallet intent + verify primitives
3. Membership quote + confirm primitives
4. Member channel register/poll primitives
5. Governance install + lease primitives
6. Raw response log for deterministic troubleshooting
Wallet automation helpers remain available in advanced controls:
1. `Connect wallet` fills address from `window.ethereum`.
2. `Sign intent (EIP-712)` signs the current intent payload and fills `walletSignature`.

View File

@ -6,6 +6,7 @@ const state = {
eventMap: new Map(),
lastIntent: null,
lastQuote: null,
channelReady: false,
};
function nowISO() {
@ -52,6 +53,25 @@ function logLine(label, payload) {
log.textContent = line + log.textContent;
}
function setSummary(id, value) {
const el = $(id);
if (!el) return;
el.textContent = value;
}
function refreshOverview(statusPayload) {
const currentWallet = wallet();
setSummary("summaryWallet", currentWallet || "not connected");
if (statusPayload && typeof statusPayload === "object") {
setSummary("summaryMembership", statusPayload.status || "unknown");
setSummary("summaryDesignation", statusPayload.designation_code || "-");
setSummary("summaryLastSync", nowISO());
return;
}
const designation = $("designationCode")?.value?.trim() || "-";
setSummary("summaryDesignation", designation);
}
function setFlowStatus(message) {
const el = $("flowStatus");
if (el) {
@ -211,6 +231,7 @@ async function onConnectWallet() {
provider_chain_id: providerChainID,
});
}
refreshOverview();
}
async function onSignIntent() {
@ -317,6 +338,7 @@ async function onIntent() {
$("designationCode").value = out.designation_code || "";
$("displayToken").value = out.display_token || "";
logLine("wallet intent", out);
refreshOverview();
}
async function onVerify() {
@ -330,6 +352,7 @@ async function onVerify() {
$("designationCode").value = out.designation_code;
}
logLine("wallet verify", out);
refreshOverview();
}
async function onStatus() {
@ -341,6 +364,7 @@ async function onStatus() {
$("designationCode").value = out.designation_code;
}
logLine("membership status", out);
refreshOverview(out);
return out;
}
@ -469,7 +493,9 @@ async function onRegisterChannel() {
app_version: $("appVersion").value.trim(),
push_provider: "none",
});
state.channelReady = true;
logLine("channel register", out);
return out;
}
async function onUnregisterChannel() {
@ -477,7 +503,9 @@ async function onUnregisterChannel() {
wallet: requireWallet(),
device_id: deviceID(),
});
state.channelReady = false;
logLine("channel unregister", out);
return out;
}
async function onPollEvents() {
@ -497,6 +525,7 @@ async function onPollEvents() {
}
renderEvents(out.events || []);
logLine("channel poll", out);
return out;
}
async function onSupportTicket() {
@ -552,6 +581,53 @@ async function onInstallStatus() {
});
const out = await request("GET", `/governance/install/status?${query.toString()}`);
logLine("install status", out);
return out;
}
async function ensureChannelBinding() {
if (state.channelReady) {
return;
}
await onRegisterChannel();
}
async function onQuickConnect() {
await onConnectWallet();
try {
await onStatus();
} catch (err) {
logLine("quick connect status warning", { error: String(err) });
}
}
async function onQuickActivate() {
await onRunMembershipFlow();
try {
await ensureChannelBinding();
await onPollEvents();
} catch (err) {
logLine("quick activate feed warning", { error: String(err) });
}
}
async function onQuickRefresh() {
const status = await onStatus();
if (String(status.status || "").toLowerCase() !== "active") {
setFlowStatus("membership inactive");
return;
}
try {
await ensureChannelBinding();
await onPollEvents();
setFlowStatus("status synced");
} catch (err) {
setFlowStatus("feed sync warning");
throw err;
}
}
async function onQuickInstallStatus() {
await onInstallStatus();
}
async function onLeaseHeartbeat() {
@ -578,7 +654,11 @@ async function onOfflineRenew() {
}
function bind(id, handler) {
$(id).addEventListener("click", async () => {
const el = $(id);
if (!el) {
return;
}
el.addEventListener("click", async () => {
try {
await handler();
} catch (err) {
@ -587,6 +667,10 @@ function bind(id, handler) {
});
}
bind("btnQuickConnect", onQuickConnect);
bind("btnQuickActivate", onQuickActivate);
bind("btnQuickRefresh", onQuickRefresh);
bind("btnQuickInstallStatus", onQuickInstallStatus);
bind("btnConnectWallet", onConnectWallet);
bind("btnRunMembershipFlow", onRunMembershipFlow);
bind("btnIntent", onIntent);
@ -611,3 +695,4 @@ logLine("launcher shell ready", {
api_base: baseURL(),
chain_id: chainID(),
});
refreshOverview();

View File

@ -3,17 +3,71 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>EDUT Launcher Shell</title>
<title>EDUT Launcher</title>
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<main class="shell">
<header class="hero">
<h1>EDUT Launcher</h1>
<p>Wallet-first onboarding shell (local integration harness)</p>
<p>Deterministic infrastructure control surface.</p>
</header>
<section class="panel">
<h2>Control Surface</h2>
<div class="actions">
<button id="btnQuickConnect">Connect wallet</button>
<button id="btnQuickActivate">Activate membership</button>
<button id="btnQuickRefresh">Refresh status + feed</button>
<button id="btnQuickInstallStatus">Governance status</button>
</div>
<p id="flowStatus" class="flow-status">ready</p>
<div class="grid two">
<article class="stat">
<h3>Wallet</h3>
<p id="summaryWallet">not connected</p>
</article>
<article class="stat">
<h3>Membership</h3>
<p id="summaryMembership">unknown</p>
</article>
<article class="stat">
<h3>Designation</h3>
<p id="summaryDesignation">-</p>
</article>
<article class="stat">
<h3>Last Sync</h3>
<p id="summaryLastSync">never</p>
</article>
</div>
</section>
<section class="panel">
<h2>Updates</h2>
<p class="note">Communication is pull-first. No broadcast notifications.</p>
<div id="eventList" class="event-list"></div>
</section>
<section class="panel">
<h2>Support</h2>
<label>
Support summary (owner only)
<input id="supportSummary" value="Need diagnostics snapshot." />
</label>
<div class="actions">
<button id="btnSupportTicket">Open support ticket</button>
</div>
</section>
<section class="panel">
<h2>Response Log</h2>
<pre id="log"></pre>
</section>
<details class="panel advanced">
<summary>Advanced Integration Controls</summary>
<div class="advanced-body">
<section class="subpanel">
<h2>Connection</h2>
<div class="grid two">
<label>
@ -27,8 +81,8 @@
</div>
</section>
<section class="panel">
<h2>Wallet Intent</h2>
<section class="subpanel">
<h2>Membership Flow Controls</h2>
<div class="actions">
<button id="btnRunMembershipFlow">Run membership flow</button>
<button id="btnConnectWallet">Connect wallet</button>
@ -37,7 +91,6 @@
<button id="btnVerify">Verify signature</button>
<button id="btnStatus">Membership status</button>
</div>
<p id="flowStatus" class="flow-status">flow idle</p>
<div class="grid three">
<label>
Wallet
@ -72,8 +125,8 @@
</label>
</section>
<section class="panel">
<h2>Membership Quote + Confirm</h2>
<section class="subpanel">
<h2>Quote + Confirm</h2>
<div class="actions">
<button id="btnQuote">Get quote</button>
<button id="btnSignPayerProof">Sign payer proof</button>
@ -114,7 +167,7 @@
</label>
</section>
<section class="panel">
<section class="subpanel">
<h2>Member Channel</h2>
<div class="grid three">
<label>
@ -151,10 +204,6 @@
</select>
</label>
</div>
<div class="actions">
<button id="btnRegisterChannel">Register channel</button>
<button id="btnUnregisterChannel">Unregister channel</button>
</div>
<div class="grid two">
<label>
Cursor
@ -166,19 +215,13 @@
</label>
</div>
<div class="actions">
<button id="btnRegisterChannel">Register channel</button>
<button id="btnUnregisterChannel">Unregister channel</button>
<button id="btnPollEvents">Poll events</button>
</div>
<div id="eventList" class="event-list"></div>
<label>
Support summary (owner only)
<input id="supportSummary" value="Need diagnostics snapshot." />
</label>
<div class="actions">
<button id="btnSupportTicket">Open support ticket</button>
</div>
</section>
<section class="panel">
<section class="subpanel">
<h2>Governance Install + Lease</h2>
<div class="actions">
<button id="btnInstallToken">Issue install token</button>
@ -208,11 +251,8 @@
<button id="btnOfflineRenew">Offline renew</button>
</div>
</section>
<section class="panel">
<h2>Response Log</h2>
<pre id="log"></pre>
</section>
</div>
</details>
</main>
<script src="./app.js"></script>
</body>

View File

@ -7,6 +7,7 @@
--muted: #8fa2c2;
--accent: #39c36b;
--warn: #ffcd57;
--accent-soft: #8bdba8;
}
* {
@ -52,6 +53,12 @@ body {
color: var(--accent);
}
.note {
margin: 0 0 10px;
color: var(--muted);
font-size: 12px;
}
.grid {
display: grid;
gap: 10px;
@ -107,6 +114,28 @@ textarea {
color: var(--warn);
}
.stat {
border: 1px solid var(--line);
border-radius: 8px;
background: #0d1625;
padding: 10px;
}
.stat h3 {
margin: 0 0 6px;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
}
.stat p {
margin: 0;
color: var(--accent-soft);
font-size: 12px;
word-break: break-word;
}
button {
border: 1px solid #2d7f4a;
background: #173325;
@ -166,6 +195,44 @@ button:active {
font-size: 11px;
}
.advanced {
padding: 0;
overflow: hidden;
}
.advanced > summary {
list-style: none;
cursor: pointer;
padding: 12px 14px;
border-bottom: 1px solid var(--line);
color: var(--muted);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.advanced > summary::-webkit-details-marker {
display: none;
}
.advanced-body {
padding: 12px;
display: grid;
gap: 10px;
}
.subpanel {
border: 1px solid var(--line);
border-radius: 8px;
padding: 10px;
background: #0b1523;
}
.subpanel h2 {
margin: 0 0 8px;
font-size: 12px;
}
@media (max-width: 720px) {
.shell {
margin-top: 12px;