@le-space/ui 0.6.44 → 0.6.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@le-space/ui",
3
- "version": "0.6.44",
3
+ "version": "0.6.46",
4
4
  "description": "Floating Aleph Relay Button UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,8 +44,8 @@
44
44
  "url": "https://github.com/NiKrause/relay-button/issues"
45
45
  },
46
46
  "dependencies": {
47
- "@le-space/browser": "0.6.44",
48
- "@le-space/core": "0.6.44",
47
+ "@le-space/browser": "0.6.46",
48
+ "@le-space/core": "0.6.46",
49
49
  "viem": "^2.53.1"
50
50
  },
51
51
  "peerDependencies": {
package/react/index.d.ts CHANGED
@@ -315,6 +315,12 @@ interface SponsorRelayState {
315
315
  balance: BalanceResponse | null;
316
316
  crns: Crn[];
317
317
  selectedCrn: Crn | null;
318
+ crnOptions: Array<{
319
+ hash: string;
320
+ name: string | null;
321
+ score: number | null;
322
+ }>;
323
+ crnPinnedByUser: boolean;
318
324
  instances: CompactInstanceRecord[];
319
325
  bootstrapRegistrations: CompactBootstrapRegistrationRecord[];
320
326
  orphanBootstrapRegistrations: CompactBootstrapRegistrationRecord[];
@@ -366,6 +372,8 @@ declare class SponsorRelayController {
366
372
  setUcanStoreBootstrapField(field: keyof SponsorRelayUcanStoreBootstrapInput, value: string): void;
367
373
  setTierId(tierId: string): void;
368
374
  private recomputePricingSummary;
375
+ /** Pin a specific CRN as the first deploy attempt (null = automatic). */
376
+ selectCrn(hash: string | null): void;
369
377
  private queueManifestRefresh;
370
378
  connectWallet(): Promise<void>;
371
379
  private refreshWalletDerivedState;
package/react/index.js CHANGED
@@ -43,7 +43,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
43
43
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
44
44
 
45
45
  // src/shared/package-version.ts
46
- var UI_PACKAGE_VERSION = "0.6.44";
46
+ var UI_PACKAGE_VERSION = "0.6.46";
47
47
 
48
48
  // src/shared/format.ts
49
49
  function shortHash(value, head = 8, tail = 6) {
@@ -3826,6 +3826,8 @@ function defaultState(props = {}) {
3826
3826
  balance: null,
3827
3827
  crns: [],
3828
3828
  selectedCrn: null,
3829
+ crnOptions: [],
3830
+ crnPinnedByUser: false,
3829
3831
  instances: [],
3830
3832
  bootstrapRegistrations: [],
3831
3833
  orphanBootstrapRegistrations: [],
@@ -4460,10 +4462,29 @@ var SponsorRelayController = class {
4460
4462
  detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
4461
4463
  });
4462
4464
  let latestRuntime = runtime;
4465
+ let activationAbsentStreak = 0;
4463
4466
  for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
4464
4467
  if (latestRuntime.webAccess?.active === true) {
4465
4468
  break;
4466
4469
  }
4470
+ if (attempt > 0 && attempt % 6 === 0) {
4471
+ const absent = await crnExecutionAbsent({
4472
+ crnUrl: args.crnUrl,
4473
+ itemHash: args.itemHash,
4474
+ fetch: asJsonFetch
4475
+ });
4476
+ activationAbsentStreak = absent ? activationAbsentStreak + 1 : 0;
4477
+ if (activationAbsentStreak >= 4) {
4478
+ this.trace("deploy:crn-execution-absent-abort", {
4479
+ itemHash: args.itemHash,
4480
+ crnUrl: args.crnUrl ?? null,
4481
+ phase: "https-activation"
4482
+ });
4483
+ throw new Error(
4484
+ "The CRN never scheduled the relay VM (execution absent while waiting for HTTPS activation)."
4485
+ );
4486
+ }
4487
+ }
4467
4488
  await new Promise(
4468
4489
  (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
4469
4490
  );
@@ -5238,15 +5259,26 @@ var SponsorRelayController = class {
5238
5259
  const balance = this.state.balance;
5239
5260
  const quote = pricing && tier && balance ? buildPaymentQuote(tier, pricing, balance) : null;
5240
5261
  const spec = pricing && tier ? tierSpec(pricing, tier) : null;
5241
- const selectedCrn = compatibleCrnsForTier(this.state.crns, {
5262
+ const compatibleList = compatibleCrnsForTier(this.state.crns, {
5242
5263
  ...this.state,
5243
5264
  pricingSummary: {
5244
5265
  ...this.state.pricingSummary,
5245
5266
  pricing,
5246
5267
  tier
5247
5268
  }
5248
- })[0] ?? null;
5269
+ });
5270
+ const pinned = this.state.crnPinnedByUser ? compatibleList.find(
5271
+ (crn) => crn.hash === this.state.selectedCrn?.hash
5272
+ ) ?? null : null;
5273
+ const selectedCrn = pinned ?? compatibleList[0] ?? null;
5274
+ const crnOptions = compatibleList.slice(0, 15).map((crn) => ({
5275
+ hash: crn.hash,
5276
+ name: crn.name ?? null,
5277
+ score: typeof crn.score === "number" ? crn.score : null
5278
+ }));
5249
5279
  this.patch({
5280
+ crnOptions,
5281
+ crnPinnedByUser: this.state.crnPinnedByUser && pinned != null,
5250
5282
  pricingSummary: {
5251
5283
  pricing,
5252
5284
  tier,
@@ -5259,6 +5291,17 @@ var SponsorRelayController = class {
5259
5291
  selectedCrn
5260
5292
  });
5261
5293
  }
5294
+ /** Pin a specific CRN as the first deploy attempt (null = automatic). */
5295
+ selectCrn(hash) {
5296
+ if (!hash) {
5297
+ this.patch({ crnPinnedByUser: false });
5298
+ this.recomputePricingSummary();
5299
+ return;
5300
+ }
5301
+ const match = this.state.crns.find((crn) => crn.hash === hash) ?? null;
5302
+ if (!match) return;
5303
+ this.patch({ selectedCrn: match, crnPinnedByUser: true });
5304
+ }
5262
5305
  queueManifestRefresh() {
5263
5306
  if (!this.state.ready) {
5264
5307
  return;
@@ -7375,7 +7418,22 @@ function SponsorRelayFab(props) {
7375
7418
  " credits required"
7376
7419
  ] }),
7377
7420
  /* @__PURE__ */ jsx("div", { children: state.rootfsHealth.label }),
7378
- /* @__PURE__ */ jsx("div", { children: state.selectedCrn?.name ?? shortHash(state.selectedCrn?.hash) }),
7421
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsxs(
7422
+ "select",
7423
+ {
7424
+ "aria-label": "Select CRN",
7425
+ value: state.crnPinnedByUser ? state.selectedCrn?.hash ?? "" : "",
7426
+ onChange: (event) => controller.selectCrn(event.currentTarget.value || null),
7427
+ children: [
7428
+ /* @__PURE__ */ jsxs("option", { value: "", children: [
7429
+ "Auto (",
7430
+ state.crnOptions[0]?.name ?? "best score",
7431
+ ")"
7432
+ ] }),
7433
+ state.crnOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.hash, children: (option.name ?? option.hash.slice(0, 8)) + (option.score != null ? ` \xB7 ${option.score.toFixed(2)}` : "") }, option.hash))
7434
+ ]
7435
+ }
7436
+ ) }),
7379
7437
  /* @__PURE__ */ jsxs("div", { children: [
7380
7438
  "Ports",
7381
7439
  " ",
package/shared/index.d.ts CHANGED
@@ -313,6 +313,12 @@ interface SponsorRelayState {
313
313
  balance: BalanceResponse | null;
314
314
  crns: Crn[];
315
315
  selectedCrn: Crn | null;
316
+ crnOptions: Array<{
317
+ hash: string;
318
+ name: string | null;
319
+ score: number | null;
320
+ }>;
321
+ crnPinnedByUser: boolean;
316
322
  instances: CompactInstanceRecord[];
317
323
  bootstrapRegistrations: CompactBootstrapRegistrationRecord[];
318
324
  orphanBootstrapRegistrations: CompactBootstrapRegistrationRecord[];
@@ -341,7 +347,7 @@ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
341
347
  declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
342
348
  declare const DEPLOYMENT_PENDING_WARNING_MS: number;
343
349
 
344
- declare const UI_PACKAGE_VERSION = "0.6.44";
350
+ declare const UI_PACKAGE_VERSION = "0.6.46";
345
351
 
346
352
  interface RootfsRequiredPortForward {
347
353
  port: number;
@@ -425,6 +431,8 @@ declare class SponsorRelayController {
425
431
  setUcanStoreBootstrapField(field: keyof SponsorRelayUcanStoreBootstrapInput, value: string): void;
426
432
  setTierId(tierId: string): void;
427
433
  private recomputePricingSummary;
434
+ /** Pin a specific CRN as the first deploy attempt (null = automatic). */
435
+ selectCrn(hash: string | null): void;
428
436
  private queueManifestRefresh;
429
437
  connectWallet(): Promise<void>;
430
438
  private refreshWalletDerivedState;
package/shared/index.js CHANGED
@@ -42,7 +42,7 @@ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
42
42
  var DEPLOYMENT_PENDING_WARNING_MS = 3 * 60 * 1e3;
43
43
 
44
44
  // src/shared/package-version.ts
45
- var UI_PACKAGE_VERSION = "0.6.44";
45
+ var UI_PACKAGE_VERSION = "0.6.46";
46
46
 
47
47
  // src/shared/format.ts
48
48
  function shortHash(value, head = 8, tail = 6) {
@@ -3825,6 +3825,8 @@ function defaultState(props = {}) {
3825
3825
  balance: null,
3826
3826
  crns: [],
3827
3827
  selectedCrn: null,
3828
+ crnOptions: [],
3829
+ crnPinnedByUser: false,
3828
3830
  instances: [],
3829
3831
  bootstrapRegistrations: [],
3830
3832
  orphanBootstrapRegistrations: [],
@@ -4459,10 +4461,29 @@ var SponsorRelayController = class {
4459
4461
  detail: "The relay received a 2n6 hostname, but it is not active yet. Waiting before handing HTTPS config to the guest."
4460
4462
  });
4461
4463
  let latestRuntime = runtime;
4464
+ let activationAbsentStreak = 0;
4462
4465
  for (let attempt = 0; attempt < UI_RUNTIME_WAIT_ATTEMPTS; attempt += 1) {
4463
4466
  if (latestRuntime.webAccess?.active === true) {
4464
4467
  break;
4465
4468
  }
4469
+ if (attempt > 0 && attempt % 6 === 0) {
4470
+ const absent = await crnExecutionAbsent({
4471
+ crnUrl: args.crnUrl,
4472
+ itemHash: args.itemHash,
4473
+ fetch: asJsonFetch
4474
+ });
4475
+ activationAbsentStreak = absent ? activationAbsentStreak + 1 : 0;
4476
+ if (activationAbsentStreak >= 4) {
4477
+ this.trace("deploy:crn-execution-absent-abort", {
4478
+ itemHash: args.itemHash,
4479
+ crnUrl: args.crnUrl ?? null,
4480
+ phase: "https-activation"
4481
+ });
4482
+ throw new Error(
4483
+ "The CRN never scheduled the relay VM (execution absent while waiting for HTTPS activation)."
4484
+ );
4485
+ }
4486
+ }
4466
4487
  await new Promise(
4467
4488
  (resolve) => globalThis.setTimeout(resolve, UI_RUNTIME_WAIT_DELAY_MS)
4468
4489
  );
@@ -5237,15 +5258,26 @@ var SponsorRelayController = class {
5237
5258
  const balance = this.state.balance;
5238
5259
  const quote = pricing && tier && balance ? buildPaymentQuote(tier, pricing, balance) : null;
5239
5260
  const spec = pricing && tier ? tierSpec(pricing, tier) : null;
5240
- const selectedCrn = compatibleCrnsForTier(this.state.crns, {
5261
+ const compatibleList = compatibleCrnsForTier(this.state.crns, {
5241
5262
  ...this.state,
5242
5263
  pricingSummary: {
5243
5264
  ...this.state.pricingSummary,
5244
5265
  pricing,
5245
5266
  tier
5246
5267
  }
5247
- })[0] ?? null;
5268
+ });
5269
+ const pinned = this.state.crnPinnedByUser ? compatibleList.find(
5270
+ (crn) => crn.hash === this.state.selectedCrn?.hash
5271
+ ) ?? null : null;
5272
+ const selectedCrn = pinned ?? compatibleList[0] ?? null;
5273
+ const crnOptions = compatibleList.slice(0, 15).map((crn) => ({
5274
+ hash: crn.hash,
5275
+ name: crn.name ?? null,
5276
+ score: typeof crn.score === "number" ? crn.score : null
5277
+ }));
5248
5278
  this.patch({
5279
+ crnOptions,
5280
+ crnPinnedByUser: this.state.crnPinnedByUser && pinned != null,
5249
5281
  pricingSummary: {
5250
5282
  pricing,
5251
5283
  tier,
@@ -5258,6 +5290,17 @@ var SponsorRelayController = class {
5258
5290
  selectedCrn
5259
5291
  });
5260
5292
  }
5293
+ /** Pin a specific CRN as the first deploy attempt (null = automatic). */
5294
+ selectCrn(hash) {
5295
+ if (!hash) {
5296
+ this.patch({ crnPinnedByUser: false });
5297
+ this.recomputePricingSummary();
5298
+ return;
5299
+ }
5300
+ const match = this.state.crns.find((crn) => crn.hash === hash) ?? null;
5301
+ if (!match) return;
5302
+ this.patch({ selectedCrn: match, crnPinnedByUser: true });
5303
+ }
5261
5304
  queueManifestRefresh() {
5262
5305
  if (!this.state.ready) {
5263
5306
  return;
@@ -260,7 +260,19 @@ export let apiHosts = undefined
260
260
  </div>
261
261
  <div class="metric-card">
262
262
  <span>CRN</span>
263
- <strong>{state.selectedCrn?.name ?? shortHash(state.selectedCrn?.hash)}</strong>
263
+ <select
264
+ class="crn-select"
265
+ aria-label="Select CRN"
266
+ value={state.crnPinnedByUser ? (state.selectedCrn?.hash ?? '') : ''}
267
+ on:change={(event) => controller.selectCrn(event.currentTarget.value || null)}
268
+ >
269
+ <option value="">Auto ({state.crnOptions[0]?.name ?? 'best score'})</option>
270
+ {#each state.crnOptions as option (option.hash)}
271
+ <option value={option.hash}>
272
+ {option.name ?? option.hash.slice(0, 8)}{option.score != null ? ` · ${option.score.toFixed(2)}` : ''}
273
+ </option>
274
+ {/each}
275
+ </select>
264
276
  <small>{state.selectedCrn?.address ?? 'Auto-picked best compatible CRN'}</small>
265
277
  </div>
266
278
  <div class="metric-card">