Add launcher shell harness for membership and governance flows
This commit is contained in:
parent
b716b52f3f
commit
d0fcc2c119
19
README.md
19
README.md
@ -18,3 +18,22 @@ Free control-plane application for EDUT onboarding and entitlement-aware install
|
|||||||
## Boundary
|
## Boundary
|
||||||
|
|
||||||
Launcher never contains private kernel internals. It verifies and installs signed paid runtimes only after entitlement checks pass.
|
Launcher never contains private kernel internals. It verifies and installs signed paid runtimes only after entitlement checks pass.
|
||||||
|
|
||||||
|
## Local Harness (Current)
|
||||||
|
|
||||||
|
`app/index.html` is a local launcher shell harness for end-to-end API validation:
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
Run locally:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/vsg/Documents/VSG\ Codex/launcher/app
|
||||||
|
python3 -m http.server 4310
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://127.0.0.1:4310` and point API base URL at running `secretapi`.
|
||||||
|
|||||||
346
app/app.js
Normal file
346
app/app.js
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
/* global fetch */
|
||||||
|
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
eventMap: new Map(),
|
||||||
|
};
|
||||||
|
|
||||||
|
function nowISO() {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function baseURL() {
|
||||||
|
return $("apiBase").value.trim().replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function wallet() {
|
||||||
|
return $("walletAddress").value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function chainID() {
|
||||||
|
return Number($("chainId").value || 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function logLine(label, payload) {
|
||||||
|
const log = $("log");
|
||||||
|
const line = `[${nowISO()}] ${label}\n${JSON.stringify(payload, null, 2)}\n\n`;
|
||||||
|
log.textContent = line + log.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request(method, path, body) {
|
||||||
|
const opts = {
|
||||||
|
method,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
};
|
||||||
|
if (body !== undefined) {
|
||||||
|
opts.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
const res = await fetch(`${baseURL()}${path}`, opts);
|
||||||
|
const text = await res.text();
|
||||||
|
let json = {};
|
||||||
|
if (text.trim() !== "") {
|
||||||
|
try {
|
||||||
|
json = JSON.parse(text);
|
||||||
|
} catch {
|
||||||
|
json = { raw: text };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`${res.status} ${res.statusText}: ${JSON.stringify(json)}`);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireWallet() {
|
||||||
|
const value = wallet();
|
||||||
|
if (!value) {
|
||||||
|
throw new Error("wallet is required");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deviceID() {
|
||||||
|
return $("deviceId").value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function orgRootID() {
|
||||||
|
return $("orgRootId").value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function principalID() {
|
||||||
|
return $("principalId").value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function principalRole() {
|
||||||
|
return $("principalRole").value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEvents(events) {
|
||||||
|
const list = $("eventList");
|
||||||
|
list.innerHTML = "";
|
||||||
|
for (const evt of events) {
|
||||||
|
state.eventMap.set(evt.event_id, evt);
|
||||||
|
const card = document.createElement("article");
|
||||||
|
card.className = "event";
|
||||||
|
|
||||||
|
const title = document.createElement("h3");
|
||||||
|
title.textContent = `${evt.class} · ${evt.event_id}`;
|
||||||
|
card.appendChild(title);
|
||||||
|
|
||||||
|
const body = document.createElement("p");
|
||||||
|
body.textContent = evt.body || evt.title || "";
|
||||||
|
card.appendChild(body);
|
||||||
|
|
||||||
|
const meta = document.createElement("div");
|
||||||
|
meta.className = "meta";
|
||||||
|
meta.textContent = `${evt.created_at} · scope=${evt.visibility_scope} · ack=${evt.requires_ack}`;
|
||||||
|
card.appendChild(meta);
|
||||||
|
|
||||||
|
if (evt.requires_ack) {
|
||||||
|
const actions = document.createElement("div");
|
||||||
|
actions.className = "actions";
|
||||||
|
const button = document.createElement("button");
|
||||||
|
button.textContent = "Ack";
|
||||||
|
button.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
const out = await request("POST", `/member/channel/events/${evt.event_id}/ack`, {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
acknowledged_at: nowISO(),
|
||||||
|
});
|
||||||
|
logLine(`event ack ${evt.event_id}`, out);
|
||||||
|
} catch (err) {
|
||||||
|
logLine(`event ack ${evt.event_id} error`, { error: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.appendChild(button);
|
||||||
|
card.appendChild(actions);
|
||||||
|
}
|
||||||
|
list.appendChild(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onIntent() {
|
||||||
|
const payload = {
|
||||||
|
address: requireWallet(),
|
||||||
|
origin: $("walletOrigin").value.trim() || "https://edut.ai",
|
||||||
|
locale: $("walletLocale").value.trim() || "en",
|
||||||
|
chain_id: chainID(),
|
||||||
|
};
|
||||||
|
const out = await request("POST", "/secret/wallet/intent", payload);
|
||||||
|
$("intentId").value = out.intent_id || "";
|
||||||
|
$("designationCode").value = out.designation_code || "";
|
||||||
|
$("displayToken").value = out.display_token || "";
|
||||||
|
logLine("wallet intent", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onVerify() {
|
||||||
|
const out = await request("POST", "/secret/wallet/verify", {
|
||||||
|
intent_id: $("intentId").value.trim(),
|
||||||
|
address: requireWallet(),
|
||||||
|
chain_id: chainID(),
|
||||||
|
signature: $("walletSignature").value.trim(),
|
||||||
|
});
|
||||||
|
if (out.designation_code) {
|
||||||
|
$("designationCode").value = out.designation_code;
|
||||||
|
}
|
||||||
|
logLine("wallet verify", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onStatus() {
|
||||||
|
const out = await request(
|
||||||
|
"GET",
|
||||||
|
`/secret/membership/status?wallet=${encodeURIComponent(requireWallet())}`,
|
||||||
|
);
|
||||||
|
if (out.designation_code) {
|
||||||
|
$("designationCode").value = out.designation_code;
|
||||||
|
}
|
||||||
|
logLine("membership status", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onQuote() {
|
||||||
|
const payload = {
|
||||||
|
designation_code: $("designationCode").value.trim(),
|
||||||
|
address: requireWallet(),
|
||||||
|
chain_id: chainID(),
|
||||||
|
};
|
||||||
|
const payerWallet = $("payerWallet").value.trim();
|
||||||
|
const payerProof = $("payerProof").value.trim();
|
||||||
|
const sponsorOrgRoot = $("sponsorOrgRoot").value.trim();
|
||||||
|
if (payerWallet) {
|
||||||
|
payload.payer_wallet = payerWallet;
|
||||||
|
}
|
||||||
|
if (payerProof) {
|
||||||
|
payload.payer_proof = payerProof;
|
||||||
|
}
|
||||||
|
if (sponsorOrgRoot) {
|
||||||
|
payload.sponsor_org_root_id = sponsorOrgRoot;
|
||||||
|
}
|
||||||
|
const out = await request("POST", "/secret/membership/quote", payload);
|
||||||
|
$("quoteId").value = out.quote_id || "";
|
||||||
|
$("quoteValue").value = out.value || "";
|
||||||
|
$("quotePayer").value = out.payer_wallet || "";
|
||||||
|
logLine("membership quote", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onConfirmMembership() {
|
||||||
|
const out = await request("POST", "/secret/membership/confirm", {
|
||||||
|
designation_code: $("designationCode").value.trim(),
|
||||||
|
quote_id: $("quoteId").value.trim(),
|
||||||
|
tx_hash: $("confirmTxHash").value.trim(),
|
||||||
|
address: requireWallet(),
|
||||||
|
chain_id: chainID(),
|
||||||
|
});
|
||||||
|
logLine("membership confirm", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onRegisterChannel() {
|
||||||
|
const out = await request("POST", "/member/channel/device/register", {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
chain_id: chainID(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
platform: $("platform").value.trim(),
|
||||||
|
org_root_id: orgRootID(),
|
||||||
|
principal_id: principalID(),
|
||||||
|
principal_role: principalRole(),
|
||||||
|
app_version: $("appVersion").value.trim(),
|
||||||
|
push_provider: "none",
|
||||||
|
});
|
||||||
|
logLine("channel register", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onUnregisterChannel() {
|
||||||
|
const out = await request("POST", "/member/channel/device/unregister", {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
});
|
||||||
|
logLine("channel unregister", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onPollEvents() {
|
||||||
|
const cursor = $("eventCursor").value.trim();
|
||||||
|
const limit = $("eventLimit").value.trim() || "25";
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
wallet: requireWallet(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
if (cursor) {
|
||||||
|
query.set("cursor", cursor);
|
||||||
|
}
|
||||||
|
const out = await request("GET", `/member/channel/events?${query.toString()}`);
|
||||||
|
if (out.next_cursor) {
|
||||||
|
$("eventCursor").value = out.next_cursor;
|
||||||
|
}
|
||||||
|
renderEvents(out.events || []);
|
||||||
|
logLine("channel poll", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSupportTicket() {
|
||||||
|
const out = await request("POST", "/member/channel/support/ticket", {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
org_root_id: orgRootID(),
|
||||||
|
principal_id: principalID(),
|
||||||
|
category: "admin_support",
|
||||||
|
summary: $("supportSummary").value.trim(),
|
||||||
|
context: {
|
||||||
|
source: "launcher-shell",
|
||||||
|
requested_at: nowISO(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
logLine("support ticket", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onInstallToken() {
|
||||||
|
const out = await request("POST", "/governance/install/token", {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
org_root_id: orgRootID(),
|
||||||
|
principal_id: principalID(),
|
||||||
|
principal_role: principalRole(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
launcher_version: $("appVersion").value.trim(),
|
||||||
|
platform: $("platform").value.trim(),
|
||||||
|
});
|
||||||
|
$("installToken").value = out.install_token || "";
|
||||||
|
$("entitlementId").value = out.entitlement_id || "";
|
||||||
|
$("runtimeVersion").value = out.package?.runtime_version || "";
|
||||||
|
$("packageHash").value = out.package?.package_hash || "";
|
||||||
|
logLine("install token", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onInstallConfirm() {
|
||||||
|
const out = await request("POST", "/governance/install/confirm", {
|
||||||
|
install_token: $("installToken").value.trim(),
|
||||||
|
wallet: requireWallet(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
entitlement_id: $("entitlementId").value.trim(),
|
||||||
|
package_hash: $("packageHash").value.trim(),
|
||||||
|
runtime_version: $("runtimeVersion").value.trim(),
|
||||||
|
installed_at: nowISO(),
|
||||||
|
launcher_receipt_hash: `receipt-${Date.now()}`,
|
||||||
|
});
|
||||||
|
logLine("install confirm", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onInstallStatus() {
|
||||||
|
const query = new URLSearchParams({
|
||||||
|
wallet: requireWallet(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
});
|
||||||
|
const out = await request("GET", `/governance/install/status?${query.toString()}`);
|
||||||
|
logLine("install status", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onLeaseHeartbeat() {
|
||||||
|
const out = await request("POST", "/governance/lease/heartbeat", {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
org_root_id: orgRootID(),
|
||||||
|
principal_id: principalID(),
|
||||||
|
device_id: deviceID(),
|
||||||
|
});
|
||||||
|
logLine("lease heartbeat", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onOfflineRenew() {
|
||||||
|
const out = await request("POST", "/governance/lease/offline-renew", {
|
||||||
|
wallet: requireWallet(),
|
||||||
|
org_root_id: orgRootID(),
|
||||||
|
principal_id: principalID(),
|
||||||
|
renewal_bundle: {
|
||||||
|
bundle_id: `renew-${Date.now()}`,
|
||||||
|
source: "launcher-shell",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
logLine("offline renew", out);
|
||||||
|
}
|
||||||
|
|
||||||
|
function bind(id, handler) {
|
||||||
|
$(id).addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
await handler();
|
||||||
|
} catch (err) {
|
||||||
|
logLine(`${id} error`, { error: String(err) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bind("btnIntent", onIntent);
|
||||||
|
bind("btnVerify", onVerify);
|
||||||
|
bind("btnStatus", onStatus);
|
||||||
|
bind("btnQuote", onQuote);
|
||||||
|
bind("btnConfirmMembership", onConfirmMembership);
|
||||||
|
bind("btnRegisterChannel", onRegisterChannel);
|
||||||
|
bind("btnUnregisterChannel", onUnregisterChannel);
|
||||||
|
bind("btnPollEvents", onPollEvents);
|
||||||
|
bind("btnSupportTicket", onSupportTicket);
|
||||||
|
bind("btnInstallToken", onInstallToken);
|
||||||
|
bind("btnInstallConfirm", onInstallConfirm);
|
||||||
|
bind("btnInstallStatus", onInstallStatus);
|
||||||
|
bind("btnLeaseHeartbeat", onLeaseHeartbeat);
|
||||||
|
bind("btnOfflineRenew", onOfflineRenew);
|
||||||
|
|
||||||
|
logLine("launcher shell ready", {
|
||||||
|
api_base: baseURL(),
|
||||||
|
chain_id: chainID(),
|
||||||
|
});
|
||||||
217
app/index.html
Normal file
217
app/index.html
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>EDUT Launcher Shell</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>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Connection</h2>
|
||||||
|
<div class="grid two">
|
||||||
|
<label>
|
||||||
|
API base URL
|
||||||
|
<input id="apiBase" value="http://127.0.0.1:8080" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Chain ID
|
||||||
|
<input id="chainId" type="number" value="84532" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Wallet Intent</h2>
|
||||||
|
<div class="grid three">
|
||||||
|
<label>
|
||||||
|
Wallet
|
||||||
|
<input id="walletAddress" placeholder="0x..." />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Origin
|
||||||
|
<input id="walletOrigin" value="https://edut.ai" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Locale
|
||||||
|
<input id="walletLocale" value="en" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btnIntent">Create intent</button>
|
||||||
|
<button id="btnStatus">Membership status</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid three">
|
||||||
|
<label>
|
||||||
|
Intent ID
|
||||||
|
<input id="intentId" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Designation code
|
||||||
|
<input id="designationCode" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Display token
|
||||||
|
<input id="displayToken" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Signature (EIP-712)
|
||||||
|
<textarea id="walletSignature" rows="2" placeholder="0x..."></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btnVerify">Verify signature</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Membership Quote + Confirm</h2>
|
||||||
|
<div class="grid three">
|
||||||
|
<label>
|
||||||
|
Payer wallet (optional)
|
||||||
|
<input id="payerWallet" placeholder="0x..." />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Payer proof (optional)
|
||||||
|
<input id="payerProof" placeholder="0x..." />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Sponsor org root (optional)
|
||||||
|
<input id="sponsorOrgRoot" placeholder="org_root_id" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btnQuote">Get quote</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid three">
|
||||||
|
<label>
|
||||||
|
Quote ID
|
||||||
|
<input id="quoteId" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Tx value
|
||||||
|
<input id="quoteValue" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Payer used
|
||||||
|
<input id="quotePayer" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Confirm tx hash
|
||||||
|
<input id="confirmTxHash" placeholder="0x..." />
|
||||||
|
</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btnConfirmMembership">Confirm membership tx</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Member Channel</h2>
|
||||||
|
<div class="grid three">
|
||||||
|
<label>
|
||||||
|
Device ID
|
||||||
|
<input id="deviceId" value="desktop-local-01" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Platform
|
||||||
|
<select id="platform">
|
||||||
|
<option>desktop</option>
|
||||||
|
<option>ios</option>
|
||||||
|
<option>android</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
App version
|
||||||
|
<input id="appVersion" value="0.1.0" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="grid three">
|
||||||
|
<label>
|
||||||
|
Org root ID
|
||||||
|
<input id="orgRootId" value="org.local.root" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Principal ID
|
||||||
|
<input id="principalId" value="human.local" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Principal role
|
||||||
|
<select id="principalRole">
|
||||||
|
<option>org_root_owner</option>
|
||||||
|
<option>workspace_member</option>
|
||||||
|
</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
|
||||||
|
<input id="eventCursor" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Limit
|
||||||
|
<input id="eventLimit" type="number" value="25" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<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">
|
||||||
|
<h2>Governance Install + Lease</h2>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btnInstallToken">Issue install token</button>
|
||||||
|
<button id="btnInstallStatus">Install status</button>
|
||||||
|
</div>
|
||||||
|
<div class="grid three">
|
||||||
|
<label>
|
||||||
|
Install token
|
||||||
|
<input id="installToken" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Entitlement ID
|
||||||
|
<input id="entitlementId" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Runtime version
|
||||||
|
<input id="runtimeVersion" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label>
|
||||||
|
Package hash
|
||||||
|
<input id="packageHash" />
|
||||||
|
</label>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btnInstallConfirm">Confirm install</button>
|
||||||
|
<button id="btnLeaseHeartbeat">Lease heartbeat</button>
|
||||||
|
<button id="btnOfflineRenew">Offline renew</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<h2>Response Log</h2>
|
||||||
|
<pre id="log"></pre>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script src="./app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
167
app/style.css
Normal file
167
app/style.css
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
:root {
|
||||||
|
color-scheme: dark;
|
||||||
|
--bg: #0b1220;
|
||||||
|
--bg-soft: #121b2e;
|
||||||
|
--line: #24344f;
|
||||||
|
--text: #d8e2f2;
|
||||||
|
--muted: #8fa2c2;
|
||||||
|
--accent: #39c36b;
|
||||||
|
--warn: #ffcd57;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "IBM Plex Mono", "SF Mono", "Menlo", monospace;
|
||||||
|
background: radial-gradient(circle at top, #182642 0%, var(--bg) 55%);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: min(1180px, 94vw);
|
||||||
|
margin: 20px auto 42px;
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero,
|
||||||
|
.panel {
|
||||||
|
background: color-mix(in oklab, var(--bg-soft) 88%, black);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero h1 {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel h2 {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.two {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid.three {
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: grid;
|
||||||
|
gap: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #0f1828;
|
||||||
|
color: var(--text);
|
||||||
|
padding: 8px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: 1px solid #2d7f4a;
|
||||||
|
background: #173325;
|
||||||
|
color: #b8efcf;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event {
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #0d1625;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event h3 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event .meta {
|
||||||
|
margin-top: 7px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#log {
|
||||||
|
margin: 0;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #08111d;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.shell {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user