@le-space/ui 0.1.62 → 0.2.0

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.1.62",
3
+ "version": "0.2.0",
4
4
  "description": "Floating Aleph sponsor relay UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -36,8 +36,8 @@
36
36
  "access": "public"
37
37
  },
38
38
  "dependencies": {
39
- "@le-space/browser": "0.1.57",
40
- "@le-space/core": "0.1.52",
39
+ "@le-space/browser": "0.2.0",
40
+ "@le-space/core": "0.2.0",
41
41
  "viem": "^2.38.5"
42
42
  },
43
43
  "peerDependencies": {
package/react/index.d.ts CHANGED
@@ -190,6 +190,7 @@ type DeploymentProgressListener = (event: DeploymentProgressEvent) => void;
190
190
  type SponsorRelayHealthTone = 'ok' | 'caution' | 'error' | 'idle';
191
191
  interface SponsorRelayProps {
192
192
  libp2p?: unknown;
193
+ debug?: boolean;
193
194
  manifestUrl?: string;
194
195
  manifestJson?: string;
195
196
  sshPublicKey?: string;
@@ -297,16 +298,23 @@ declare class SponsorRelayController {
297
298
  private props;
298
299
  private progressEmitter;
299
300
  private runtimeCooldownByHash;
301
+ private runtimeDetailsByHash;
302
+ private debugEnabled;
303
+ private lastRuntimeReadyHash;
300
304
  constructor(props?: SponsorRelayProps);
301
305
  subscribeToDeploymentProgress(listener: DeploymentProgressListener): () => void;
302
306
  subscribe(subscriber: SponsorRelaySubscriber): () => void;
303
307
  getState(): SponsorRelayState;
308
+ updateProps(props: Partial<SponsorRelayProps>): void;
304
309
  private emit;
310
+ private trace;
305
311
  private patch;
306
312
  private emitProgress;
307
313
  private syncLatestDeploymentProgress;
308
314
  private canSkipRuntimeRefresh;
309
315
  private noteRuntimeRefreshResult;
316
+ private rememberRuntimeDetails;
317
+ private mergeRememberedRuntimeDetails;
310
318
  init(): Promise<void>;
311
319
  destroy(): void;
312
320
  setOpen(open: boolean): void;
package/react/index.js CHANGED
@@ -1985,6 +1985,16 @@ function defaultState(props = {}) {
1985
1985
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
1986
1986
  };
1987
1987
  }
1988
+ function isDebugEnabled(props) {
1989
+ if (props.debug) {
1990
+ return true;
1991
+ }
1992
+ try {
1993
+ return globalThis.localStorage?.getItem("LE_SPACE_UI_DEBUG") === "1";
1994
+ } catch {
1995
+ return false;
1996
+ }
1997
+ }
1988
1998
  async function sha256Hex(payload) {
1989
1999
  const digest = await globalThis.crypto.subtle.digest(
1990
2000
  "SHA-256",
@@ -2000,6 +2010,12 @@ function mappedPorts(execution) {
2000
2010
  })
2001
2011
  );
2002
2012
  }
2013
+ function runtimeMappedPorts(mappedPortsRecord) {
2014
+ return Object.entries(mappedPortsRecord ?? {}).map(([port, mapping]) => ({
2015
+ label: `${port}/${mapping.udp ? "udp" : "tcp"}`,
2016
+ hostPort: mapping.host ?? null
2017
+ }));
2018
+ }
2003
2019
  function rootfsHealth(args) {
2004
2020
  if (!args.manifestState.valid || !args.manifestState.manifest) {
2005
2021
  return {
@@ -2070,8 +2086,18 @@ async function inspectInstanceRuntime(args) {
2070
2086
  execution: null,
2071
2087
  error: null
2072
2088
  };
2073
- if (details.messageStatus !== "processed") {
2074
- return details;
2089
+ const lookup = {
2090
+ allocationFound: false,
2091
+ allocationSource: null,
2092
+ crnUrl: null,
2093
+ webUrl: null,
2094
+ executionPayloadFound: false,
2095
+ executionLookupBlocked: false,
2096
+ executionLookupRequestUrl: null,
2097
+ executionLookupVersion: null
2098
+ };
2099
+ if (details.messageStatus === "rejected" || details.messageStatus === "removing") {
2100
+ return { details, lookup };
2075
2101
  }
2076
2102
  try {
2077
2103
  const allocation = await args.client.fetchSchedulerAllocation(args.instance.item_hash) ?? (() => {
@@ -2086,26 +2112,35 @@ async function inspectInstanceRuntime(args) {
2086
2112
  period: null
2087
2113
  } : null;
2088
2114
  })();
2115
+ lookup.allocationFound = Boolean(allocation);
2116
+ lookup.allocationSource = allocation?.source ?? null;
2089
2117
  details.allocationSource = allocation?.source ?? null;
2090
2118
  details.crnUrl = allocation?.crnUrl ?? null;
2091
2119
  details.ipv6 = allocation?.vmIpv6 ?? null;
2092
2120
  details.webUrl = await args.client.fetch2n6WebAccessUrl(
2093
2121
  args.instance.item_hash
2094
2122
  );
2123
+ lookup.crnUrl = details.crnUrl;
2124
+ lookup.webUrl = details.webUrl;
2095
2125
  if (!allocation?.crnUrl) {
2096
- return details;
2126
+ return { details, lookup };
2097
2127
  }
2098
2128
  const executionLookup = await args.client.fetchCrnExecutionMap(
2099
2129
  allocation.crnUrl
2100
2130
  );
2131
+ lookup.executionLookupBlocked = executionLookup.blocked;
2132
+ lookup.executionLookupRequestUrl = executionLookup.requestUrl ?? null;
2133
+ lookup.executionLookupVersion = executionLookup.version ?? null;
2101
2134
  const executionPayload = executionLookup.payload?.[args.instance.item_hash];
2135
+ lookup.executionPayloadFound = Boolean(executionPayload);
2102
2136
  if (!executionPayload) {
2103
- return details;
2137
+ return { details, lookup };
2104
2138
  }
2105
2139
  const execution = normalizeExecution(executionPayload, allocation.crnUrl);
2106
2140
  if (!execution.networking.proxy_url && details.webUrl) {
2107
2141
  execution.networking.proxy_url = details.webUrl;
2108
2142
  }
2143
+ details.messageStatus = "processed";
2109
2144
  details.execution = execution;
2110
2145
  details.hostIpv4 = execution.networking.host_ipv4 ?? execution.networking.ipv4 ?? null;
2111
2146
  details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
@@ -2113,11 +2148,14 @@ async function inspectInstanceRuntime(args) {
2113
2148
  details.webUrl = execution.networking.proxy_url ?? details.webUrl;
2114
2149
  details.mappedPorts = mappedPorts(execution);
2115
2150
  details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
2116
- return details;
2151
+ return { details, lookup };
2117
2152
  } catch (error) {
2118
2153
  return {
2119
- ...details,
2120
- error: error instanceof Error ? error.message : String(error)
2154
+ details: {
2155
+ ...details,
2156
+ error: error instanceof Error ? error.message : String(error)
2157
+ },
2158
+ lookup
2121
2159
  };
2122
2160
  }
2123
2161
  }
@@ -2226,9 +2264,13 @@ var SponsorRelayController = class {
2226
2264
  props;
2227
2265
  progressEmitter = createDeploymentProgressEmitter();
2228
2266
  runtimeCooldownByHash = /* @__PURE__ */ new Map();
2267
+ runtimeDetailsByHash = /* @__PURE__ */ new Map();
2268
+ debugEnabled;
2269
+ lastRuntimeReadyHash = null;
2229
2270
  constructor(props = {}) {
2230
2271
  this.props = props;
2231
2272
  this.state = defaultState(props);
2273
+ this.debugEnabled = isDebugEnabled(props);
2232
2274
  this.client = createAlephBrowserClient({
2233
2275
  apiHost: props.apiHost,
2234
2276
  crnListUrl: props.crnListUrl,
@@ -2248,10 +2290,24 @@ var SponsorRelayController = class {
2248
2290
  getState() {
2249
2291
  return this.state;
2250
2292
  }
2293
+ updateProps(props) {
2294
+ this.props = { ...this.props, ...props };
2295
+ this.debugEnabled = isDebugEnabled(this.props);
2296
+ }
2251
2297
  emit() {
2252
2298
  const next = this.state;
2253
2299
  this.subscribers.forEach((subscriber) => subscriber(next));
2254
2300
  }
2301
+ trace(message, data) {
2302
+ if (!this.debugEnabled) {
2303
+ return;
2304
+ }
2305
+ if (data === void 0) {
2306
+ console.debug("[le-space/ui]", message);
2307
+ return;
2308
+ }
2309
+ console.debug("[le-space/ui]", message, data);
2310
+ }
2255
2311
  patch(patch) {
2256
2312
  this.state = {
2257
2313
  ...this.state,
@@ -2268,6 +2324,14 @@ var SponsorRelayController = class {
2268
2324
  ...event,
2269
2325
  timestamp: Date.now()
2270
2326
  };
2327
+ this.trace(`progress:${nextEvent.stage}`, {
2328
+ label: nextEvent.label,
2329
+ progress: nextEvent.progress,
2330
+ status: nextEvent.status,
2331
+ itemHash: nextEvent.itemHash ?? null,
2332
+ detail: nextEvent.detail ?? null,
2333
+ error: nextEvent.error ?? null
2334
+ });
2271
2335
  this.patch({
2272
2336
  deploymentProgress: nextEvent,
2273
2337
  statusText: event.error ?? event.detail ?? event.label,
@@ -2286,6 +2350,9 @@ var SponsorRelayController = class {
2286
2350
  if (!latest) {
2287
2351
  return;
2288
2352
  }
2353
+ const currentProgress = this.state.deploymentProgress;
2354
+ const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
2355
+ const keepTerminalSuccess = this.lastRuntimeReadyHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
2289
2356
  const status = latest.details.messageStatus;
2290
2357
  if (status === "rejected") {
2291
2358
  this.emitProgress({
@@ -2300,6 +2367,14 @@ var SponsorRelayController = class {
2300
2367
  return;
2301
2368
  }
2302
2369
  if (status !== "processed") {
2370
+ if (keepTerminalSuccess) {
2371
+ this.trace("progress:retain-completed", {
2372
+ reason: "latest refresh still reports non-processed status",
2373
+ itemHash,
2374
+ status
2375
+ });
2376
+ return;
2377
+ }
2303
2378
  const submittedAtMs = instanceTimestampMs(latest.instance);
2304
2379
  const pendingTooLong = submittedAtMs != null && Date.now() - submittedAtMs >= DEPLOYMENT_PENDING_WARNING_MS;
2305
2380
  this.emitProgress({
@@ -2313,6 +2388,13 @@ var SponsorRelayController = class {
2313
2388
  return;
2314
2389
  }
2315
2390
  if (!hasUsableRuntime(latest.details)) {
2391
+ if (keepTerminalSuccess) {
2392
+ this.trace("progress:retain-completed", {
2393
+ reason: "latest refresh lacks runtime details after completed state",
2394
+ itemHash
2395
+ });
2396
+ return;
2397
+ }
2316
2398
  this.emitProgress({
2317
2399
  stage: "deployment-confirmed",
2318
2400
  label: "Waiting for runtime",
@@ -2359,8 +2441,42 @@ var SponsorRelayController = class {
2359
2441
  Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
2360
2442
  );
2361
2443
  }
2444
+ rememberRuntimeDetails(itemHash, details) {
2445
+ if (!hasUsableRuntime(details)) {
2446
+ return;
2447
+ }
2448
+ this.runtimeDetailsByHash.set(itemHash, {
2449
+ ...details,
2450
+ mappedPorts: [...details.mappedPorts]
2451
+ });
2452
+ }
2453
+ mergeRememberedRuntimeDetails(itemHash, details) {
2454
+ const remembered = this.runtimeDetailsByHash.get(itemHash);
2455
+ if (!remembered) {
2456
+ return details;
2457
+ }
2458
+ if (details.messageStatus === "processed" && hasUsableRuntime(details)) {
2459
+ return details;
2460
+ }
2461
+ this.trace("runtime:retain-known-details", {
2462
+ itemHash,
2463
+ currentStatus: details.messageStatus,
2464
+ rememberedStatus: remembered.messageStatus,
2465
+ currentHasRuntime: hasUsableRuntime(details)
2466
+ });
2467
+ return {
2468
+ ...details,
2469
+ ...remembered,
2470
+ error: details.error ?? remembered.error
2471
+ };
2472
+ }
2362
2473
  async init() {
2474
+ this.trace("init:start", {
2475
+ launcherMode: this.props.launcherMode ?? "floating",
2476
+ manifestUrl: this.state.manifestUrl
2477
+ });
2363
2478
  this.stopWalletWatch = watchWallet(() => {
2479
+ this.trace("wallet:changed");
2364
2480
  void this.refreshWalletDerivedState();
2365
2481
  });
2366
2482
  await this.refresh();
@@ -2372,6 +2488,7 @@ var SponsorRelayController = class {
2372
2488
  }, RELAY_PING_INTERVAL_MS);
2373
2489
  await this.refreshRelayPing();
2374
2490
  this.patch({ ready: true });
2491
+ this.trace("init:ready");
2375
2492
  }
2376
2493
  destroy() {
2377
2494
  if (this.refreshTimer) clearInterval(this.refreshTimer);
@@ -2472,6 +2589,11 @@ var SponsorRelayController = class {
2472
2589
  }
2473
2590
  }
2474
2591
  async refresh() {
2592
+ this.trace("refresh:start", {
2593
+ wallet: this.state.wallet.address,
2594
+ manifestUrl: this.state.manifestUrl,
2595
+ showInstances: this.state.showInstances
2596
+ });
2475
2597
  this.patch({
2476
2598
  busy: { refreshing: true },
2477
2599
  errorText: null,
@@ -2497,29 +2619,58 @@ var SponsorRelayController = class {
2497
2619
  balance = nextBalance;
2498
2620
  instances = await Promise.all(
2499
2621
  rawInstances.map(async (instance) => {
2500
- const details = this.canSkipRuntimeRefresh(instance) ? {
2501
- messageStatus: String(
2502
- instance.status ?? (instance.confirmed ? "processed" : "pending")
2503
- ).toLowerCase(),
2504
- allocationSource: null,
2505
- crnUrl: null,
2506
- hostIpv4: null,
2507
- ipv6: null,
2508
- vmIpv4: null,
2509
- webUrl: null,
2510
- sshCommand: null,
2511
- mappedPorts: [],
2512
- execution: null,
2513
- error: null
2622
+ const inspected = this.canSkipRuntimeRefresh(instance) ? {
2623
+ details: {
2624
+ messageStatus: String(
2625
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
2626
+ ).toLowerCase(),
2627
+ allocationSource: null,
2628
+ crnUrl: null,
2629
+ hostIpv4: null,
2630
+ ipv6: null,
2631
+ vmIpv4: null,
2632
+ webUrl: null,
2633
+ sshCommand: null,
2634
+ mappedPorts: [],
2635
+ execution: null,
2636
+ error: null
2637
+ },
2638
+ lookup: {
2639
+ allocationFound: false,
2640
+ allocationSource: null,
2641
+ crnUrl: null,
2642
+ webUrl: null,
2643
+ executionPayloadFound: false,
2644
+ executionLookupBlocked: false,
2645
+ executionLookupRequestUrl: null,
2646
+ executionLookupVersion: null
2647
+ }
2514
2648
  } : await inspectInstanceRuntime({
2515
2649
  client: this.client,
2516
2650
  instance,
2517
2651
  crns
2518
2652
  });
2519
- this.noteRuntimeRefreshResult(instance, details);
2653
+ const details = inspected.details;
2654
+ const mergedDetails = this.mergeRememberedRuntimeDetails(
2655
+ instance.item_hash,
2656
+ details
2657
+ );
2658
+ if (this.debugEnabled && instance.item_hash === this.state.lastDeploymentHash) {
2659
+ this.trace("runtime:inspect-result", {
2660
+ itemHash: instance.item_hash,
2661
+ messageStatus: details.messageStatus,
2662
+ hostIpv4: details.hostIpv4,
2663
+ vmIpv4: details.vmIpv4,
2664
+ mappedPorts: details.mappedPorts,
2665
+ error: details.error,
2666
+ lookup: inspected.lookup
2667
+ });
2668
+ }
2669
+ this.rememberRuntimeDetails(instance.item_hash, mergedDetails);
2670
+ this.noteRuntimeRefreshResult(instance, mergedDetails);
2520
2671
  return {
2521
2672
  instance,
2522
- details
2673
+ details: mergedDetails
2523
2674
  };
2524
2675
  })
2525
2676
  );
@@ -2556,9 +2707,15 @@ var SponsorRelayController = class {
2556
2707
  busy: { refreshing: false },
2557
2708
  statusText: "Relay sponsor data ready"
2558
2709
  });
2710
+ this.trace("refresh:success", {
2711
+ manifestValid: manifestState.valid,
2712
+ rootfsVerified,
2713
+ instances: instances.length
2714
+ });
2559
2715
  this.recomputePricingSummary();
2560
2716
  this.syncLatestDeploymentProgress(instances);
2561
2717
  } catch (error) {
2718
+ this.trace("refresh:error", error);
2562
2719
  this.patch({
2563
2720
  busy: { refreshing: false },
2564
2721
  errorText: error instanceof Error ? error.message : String(error),
@@ -2569,8 +2726,17 @@ var SponsorRelayController = class {
2569
2726
  async refreshRelayPing() {
2570
2727
  const relayPing = await pingPeer(this.props.libp2p);
2571
2728
  this.patch({ relayPing });
2729
+ this.trace("libp2p:relay-ping", relayPing);
2572
2730
  }
2573
2731
  async deploy() {
2732
+ this.trace("deploy:start", {
2733
+ wallet: this.state.wallet.address,
2734
+ instanceName: this.state.instanceName,
2735
+ tierId: this.state.tierId,
2736
+ selectedCrn: this.state.selectedCrn?.name ?? null,
2737
+ manifestRootfsItemHash: this.state.manifest?.rootfsItemHash ?? null,
2738
+ requiredPortForwards: this.state.manifest?.requiredPortForwards ?? []
2739
+ });
2574
2740
  if (!this.state.wallet.address) {
2575
2741
  this.patch({ errorText: "Connect MetaMask before deploying." });
2576
2742
  return;
@@ -2635,6 +2801,8 @@ var SponsorRelayController = class {
2635
2801
  statusText: `Deployment submitted: ${result.itemHash}`,
2636
2802
  lastDeploymentHash: result.itemHash
2637
2803
  });
2804
+ this.trace("deploy:broadcasted", result);
2805
+ this.lastRuntimeReadyHash = null;
2638
2806
  const inspection = await waitForDeploymentResult2(result.itemHash, {
2639
2807
  rootfsRef: this.state.manifest.rootfsItemHash,
2640
2808
  apiHost: this.client.apiHost,
@@ -2680,6 +2848,7 @@ var SponsorRelayController = class {
2680
2848
  inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
2681
2849
  );
2682
2850
  }
2851
+ this.trace("deploy:aleph-processed", inspection);
2683
2852
  if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
2684
2853
  this.emitProgress({
2685
2854
  stage: "refreshing-instances",
@@ -2699,8 +2868,18 @@ var SponsorRelayController = class {
2699
2868
  apiHost: this.client.apiHost,
2700
2869
  sync: true
2701
2870
  });
2871
+ this.trace("deploy:port-forwards-published", {
2872
+ itemHash: result.itemHash,
2873
+ requiredPortForwards: this.state.manifest.requiredPortForwards
2874
+ });
2702
2875
  }
2703
2876
  if (this.state.selectedCrn?.address) {
2877
+ this.trace("deploy:notifying-crn", {
2878
+ itemHash: result.itemHash,
2879
+ crnName: this.state.selectedCrn.name,
2880
+ crnHash: this.state.selectedCrn.hash,
2881
+ crnUrl: this.state.selectedCrn.address
2882
+ });
2704
2883
  await notifyCrnAllocationWithRetry({
2705
2884
  crnUrl: this.state.selectedCrn.address,
2706
2885
  itemHash: result.itemHash,
@@ -2753,6 +2932,21 @@ var SponsorRelayController = class {
2753
2932
  runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
2754
2933
  );
2755
2934
  }
2935
+ this.trace("deploy:runtime-ready", runtime);
2936
+ this.lastRuntimeReadyHash = result.itemHash;
2937
+ this.rememberRuntimeDetails(result.itemHash, {
2938
+ messageStatus: "processed",
2939
+ allocationSource: runtime.allocation?.source ?? null,
2940
+ crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
2941
+ hostIpv4: runtime.hostIpv4 ?? null,
2942
+ ipv6: runtime.ipv6 ?? null,
2943
+ vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
2944
+ webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
2945
+ sshCommand: runtime.sshCommand ?? null,
2946
+ mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
2947
+ execution: runtime.execution ?? null,
2948
+ error: null
2949
+ });
2756
2950
  this.patch({
2757
2951
  busy: { deploying: false }
2758
2952
  });
@@ -2766,6 +2960,7 @@ var SponsorRelayController = class {
2766
2960
  });
2767
2961
  await this.refresh();
2768
2962
  } catch (error) {
2963
+ this.trace("deploy:error", error);
2769
2964
  this.patch({
2770
2965
  busy: { deploying: false },
2771
2966
  errorText: error instanceof Error ? error.message : String(error),
@@ -2781,6 +2976,7 @@ var SponsorRelayController = class {
2781
2976
  }
2782
2977
  }
2783
2978
  async deleteInstance(instanceHash) {
2979
+ this.trace("delete:start", { instanceHash });
2784
2980
  if (!this.state.wallet.address) {
2785
2981
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
2786
2982
  return;
@@ -2828,6 +3024,7 @@ var SponsorRelayController = class {
2828
3024
  detail: "Reloading current deployments."
2829
3025
  });
2830
3026
  await this.refresh();
3027
+ this.trace("delete:submitted", { instanceHash });
2831
3028
  this.emitProgress({
2832
3029
  stage: "completed",
2833
3030
  label: "Delete completed",
@@ -2838,6 +3035,7 @@ var SponsorRelayController = class {
2838
3035
  error: null
2839
3036
  });
2840
3037
  } catch (error) {
3038
+ this.trace("delete:error", error);
2841
3039
  this.patch({
2842
3040
  busy: { deletingInstanceHash: null },
2843
3041
  errorText: error instanceof Error ? error.message : String(error),
@@ -2858,8 +3056,8 @@ function useSponsorRelayController(props) {
2858
3056
  [
2859
3057
  props.apiHost,
2860
3058
  props.crnListUrl,
3059
+ props.debug,
2861
3060
  props.instanceName,
2862
- props.libp2p,
2863
3061
  props.manifestJson,
2864
3062
  props.manifestUrl,
2865
3063
  props.openByDefault,
@@ -2869,6 +3067,12 @@ function useSponsorRelayController(props) {
2869
3067
  props.twoN6ApiHost
2870
3068
  ]
2871
3069
  );
3070
+ useEffect(() => {
3071
+ controller.updateProps({
3072
+ libp2p: props.libp2p,
3073
+ debug: props.debug
3074
+ });
3075
+ }, [controller, props.debug, props.libp2p]);
2872
3076
  useEffect(() => {
2873
3077
  void controller.init();
2874
3078
  return () => {
package/shared/index.d.ts CHANGED
@@ -188,6 +188,7 @@ type DeploymentProgressListener = (event: DeploymentProgressEvent) => void;
188
188
  type SponsorRelayHealthTone = 'ok' | 'caution' | 'error' | 'idle';
189
189
  interface SponsorRelayProps {
190
190
  libp2p?: unknown;
191
+ debug?: boolean;
191
192
  manifestUrl?: string;
192
193
  manifestJson?: string;
193
194
  sshPublicKey?: string;
@@ -343,16 +344,23 @@ declare class SponsorRelayController {
343
344
  private props;
344
345
  private progressEmitter;
345
346
  private runtimeCooldownByHash;
347
+ private runtimeDetailsByHash;
348
+ private debugEnabled;
349
+ private lastRuntimeReadyHash;
346
350
  constructor(props?: SponsorRelayProps);
347
351
  subscribeToDeploymentProgress(listener: DeploymentProgressListener): () => void;
348
352
  subscribe(subscriber: SponsorRelaySubscriber): () => void;
349
353
  getState(): SponsorRelayState;
354
+ updateProps(props: Partial<SponsorRelayProps>): void;
350
355
  private emit;
356
+ private trace;
351
357
  private patch;
352
358
  private emitProgress;
353
359
  private syncLatestDeploymentProgress;
354
360
  private canSkipRuntimeRefresh;
355
361
  private noteRuntimeRefreshResult;
362
+ private rememberRuntimeDetails;
363
+ private mergeRememberedRuntimeDetails;
356
364
  init(): Promise<void>;
357
365
  destroy(): void;
358
366
  setOpen(open: boolean): void;
package/shared/index.js CHANGED
@@ -1984,6 +1984,16 @@ function defaultState(props = {}) {
1984
1984
  deploymentProgress: IDLE_DEPLOYMENT_PROGRESS
1985
1985
  };
1986
1986
  }
1987
+ function isDebugEnabled(props) {
1988
+ if (props.debug) {
1989
+ return true;
1990
+ }
1991
+ try {
1992
+ return globalThis.localStorage?.getItem("LE_SPACE_UI_DEBUG") === "1";
1993
+ } catch {
1994
+ return false;
1995
+ }
1996
+ }
1987
1997
  async function sha256Hex(payload) {
1988
1998
  const digest = await globalThis.crypto.subtle.digest(
1989
1999
  "SHA-256",
@@ -1999,6 +2009,12 @@ function mappedPorts(execution) {
1999
2009
  })
2000
2010
  );
2001
2011
  }
2012
+ function runtimeMappedPorts(mappedPortsRecord) {
2013
+ return Object.entries(mappedPortsRecord ?? {}).map(([port, mapping]) => ({
2014
+ label: `${port}/${mapping.udp ? "udp" : "tcp"}`,
2015
+ hostPort: mapping.host ?? null
2016
+ }));
2017
+ }
2002
2018
  function rootfsHealth(args) {
2003
2019
  if (!args.manifestState.valid || !args.manifestState.manifest) {
2004
2020
  return {
@@ -2069,8 +2085,18 @@ async function inspectInstanceRuntime(args) {
2069
2085
  execution: null,
2070
2086
  error: null
2071
2087
  };
2072
- if (details.messageStatus !== "processed") {
2073
- return details;
2088
+ const lookup = {
2089
+ allocationFound: false,
2090
+ allocationSource: null,
2091
+ crnUrl: null,
2092
+ webUrl: null,
2093
+ executionPayloadFound: false,
2094
+ executionLookupBlocked: false,
2095
+ executionLookupRequestUrl: null,
2096
+ executionLookupVersion: null
2097
+ };
2098
+ if (details.messageStatus === "rejected" || details.messageStatus === "removing") {
2099
+ return { details, lookup };
2074
2100
  }
2075
2101
  try {
2076
2102
  const allocation = await args.client.fetchSchedulerAllocation(args.instance.item_hash) ?? (() => {
@@ -2085,26 +2111,35 @@ async function inspectInstanceRuntime(args) {
2085
2111
  period: null
2086
2112
  } : null;
2087
2113
  })();
2114
+ lookup.allocationFound = Boolean(allocation);
2115
+ lookup.allocationSource = allocation?.source ?? null;
2088
2116
  details.allocationSource = allocation?.source ?? null;
2089
2117
  details.crnUrl = allocation?.crnUrl ?? null;
2090
2118
  details.ipv6 = allocation?.vmIpv6 ?? null;
2091
2119
  details.webUrl = await args.client.fetch2n6WebAccessUrl(
2092
2120
  args.instance.item_hash
2093
2121
  );
2122
+ lookup.crnUrl = details.crnUrl;
2123
+ lookup.webUrl = details.webUrl;
2094
2124
  if (!allocation?.crnUrl) {
2095
- return details;
2125
+ return { details, lookup };
2096
2126
  }
2097
2127
  const executionLookup = await args.client.fetchCrnExecutionMap(
2098
2128
  allocation.crnUrl
2099
2129
  );
2130
+ lookup.executionLookupBlocked = executionLookup.blocked;
2131
+ lookup.executionLookupRequestUrl = executionLookup.requestUrl ?? null;
2132
+ lookup.executionLookupVersion = executionLookup.version ?? null;
2100
2133
  const executionPayload = executionLookup.payload?.[args.instance.item_hash];
2134
+ lookup.executionPayloadFound = Boolean(executionPayload);
2101
2135
  if (!executionPayload) {
2102
- return details;
2136
+ return { details, lookup };
2103
2137
  }
2104
2138
  const execution = normalizeExecution(executionPayload, allocation.crnUrl);
2105
2139
  if (!execution.networking.proxy_url && details.webUrl) {
2106
2140
  execution.networking.proxy_url = details.webUrl;
2107
2141
  }
2142
+ details.messageStatus = "processed";
2108
2143
  details.execution = execution;
2109
2144
  details.hostIpv4 = execution.networking.host_ipv4 ?? execution.networking.ipv4 ?? null;
2110
2145
  details.ipv6 = execution.networking.ipv6_ip ?? execution.networking.ipv6 ?? details.ipv6;
@@ -2112,11 +2147,14 @@ async function inspectInstanceRuntime(args) {
2112
2147
  details.webUrl = execution.networking.proxy_url ?? details.webUrl;
2113
2148
  details.mappedPorts = mappedPorts(execution);
2114
2149
  details.sshCommand = buildSshCommand(details.hostIpv4, details.mappedPorts);
2115
- return details;
2150
+ return { details, lookup };
2116
2151
  } catch (error) {
2117
2152
  return {
2118
- ...details,
2119
- error: error instanceof Error ? error.message : String(error)
2153
+ details: {
2154
+ ...details,
2155
+ error: error instanceof Error ? error.message : String(error)
2156
+ },
2157
+ lookup
2120
2158
  };
2121
2159
  }
2122
2160
  }
@@ -2225,9 +2263,13 @@ var SponsorRelayController = class {
2225
2263
  props;
2226
2264
  progressEmitter = createDeploymentProgressEmitter();
2227
2265
  runtimeCooldownByHash = /* @__PURE__ */ new Map();
2266
+ runtimeDetailsByHash = /* @__PURE__ */ new Map();
2267
+ debugEnabled;
2268
+ lastRuntimeReadyHash = null;
2228
2269
  constructor(props = {}) {
2229
2270
  this.props = props;
2230
2271
  this.state = defaultState(props);
2272
+ this.debugEnabled = isDebugEnabled(props);
2231
2273
  this.client = createAlephBrowserClient({
2232
2274
  apiHost: props.apiHost,
2233
2275
  crnListUrl: props.crnListUrl,
@@ -2247,10 +2289,24 @@ var SponsorRelayController = class {
2247
2289
  getState() {
2248
2290
  return this.state;
2249
2291
  }
2292
+ updateProps(props) {
2293
+ this.props = { ...this.props, ...props };
2294
+ this.debugEnabled = isDebugEnabled(this.props);
2295
+ }
2250
2296
  emit() {
2251
2297
  const next = this.state;
2252
2298
  this.subscribers.forEach((subscriber) => subscriber(next));
2253
2299
  }
2300
+ trace(message, data) {
2301
+ if (!this.debugEnabled) {
2302
+ return;
2303
+ }
2304
+ if (data === void 0) {
2305
+ console.debug("[le-space/ui]", message);
2306
+ return;
2307
+ }
2308
+ console.debug("[le-space/ui]", message, data);
2309
+ }
2254
2310
  patch(patch) {
2255
2311
  this.state = {
2256
2312
  ...this.state,
@@ -2267,6 +2323,14 @@ var SponsorRelayController = class {
2267
2323
  ...event,
2268
2324
  timestamp: Date.now()
2269
2325
  };
2326
+ this.trace(`progress:${nextEvent.stage}`, {
2327
+ label: nextEvent.label,
2328
+ progress: nextEvent.progress,
2329
+ status: nextEvent.status,
2330
+ itemHash: nextEvent.itemHash ?? null,
2331
+ detail: nextEvent.detail ?? null,
2332
+ error: nextEvent.error ?? null
2333
+ });
2270
2334
  this.patch({
2271
2335
  deploymentProgress: nextEvent,
2272
2336
  statusText: event.error ?? event.detail ?? event.label,
@@ -2285,6 +2349,9 @@ var SponsorRelayController = class {
2285
2349
  if (!latest) {
2286
2350
  return;
2287
2351
  }
2352
+ const currentProgress = this.state.deploymentProgress;
2353
+ const currentProgressIsForLatestHash = currentProgress.itemHash === itemHash || currentProgress.itemHash == null && currentProgress.stage !== "idle" && currentProgress.stage !== "error";
2354
+ const keepTerminalSuccess = this.lastRuntimeReadyHash === itemHash || currentProgressIsForLatestHash && currentProgress.stage === "completed" && currentProgress.status === "success";
2288
2355
  const status = latest.details.messageStatus;
2289
2356
  if (status === "rejected") {
2290
2357
  this.emitProgress({
@@ -2299,6 +2366,14 @@ var SponsorRelayController = class {
2299
2366
  return;
2300
2367
  }
2301
2368
  if (status !== "processed") {
2369
+ if (keepTerminalSuccess) {
2370
+ this.trace("progress:retain-completed", {
2371
+ reason: "latest refresh still reports non-processed status",
2372
+ itemHash,
2373
+ status
2374
+ });
2375
+ return;
2376
+ }
2302
2377
  const submittedAtMs = instanceTimestampMs(latest.instance);
2303
2378
  const pendingTooLong = submittedAtMs != null && Date.now() - submittedAtMs >= DEPLOYMENT_PENDING_WARNING_MS;
2304
2379
  this.emitProgress({
@@ -2312,6 +2387,13 @@ var SponsorRelayController = class {
2312
2387
  return;
2313
2388
  }
2314
2389
  if (!hasUsableRuntime(latest.details)) {
2390
+ if (keepTerminalSuccess) {
2391
+ this.trace("progress:retain-completed", {
2392
+ reason: "latest refresh lacks runtime details after completed state",
2393
+ itemHash
2394
+ });
2395
+ return;
2396
+ }
2315
2397
  this.emitProgress({
2316
2398
  stage: "deployment-confirmed",
2317
2399
  label: "Waiting for runtime",
@@ -2358,8 +2440,42 @@ var SponsorRelayController = class {
2358
2440
  Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
2359
2441
  );
2360
2442
  }
2443
+ rememberRuntimeDetails(itemHash, details) {
2444
+ if (!hasUsableRuntime(details)) {
2445
+ return;
2446
+ }
2447
+ this.runtimeDetailsByHash.set(itemHash, {
2448
+ ...details,
2449
+ mappedPorts: [...details.mappedPorts]
2450
+ });
2451
+ }
2452
+ mergeRememberedRuntimeDetails(itemHash, details) {
2453
+ const remembered = this.runtimeDetailsByHash.get(itemHash);
2454
+ if (!remembered) {
2455
+ return details;
2456
+ }
2457
+ if (details.messageStatus === "processed" && hasUsableRuntime(details)) {
2458
+ return details;
2459
+ }
2460
+ this.trace("runtime:retain-known-details", {
2461
+ itemHash,
2462
+ currentStatus: details.messageStatus,
2463
+ rememberedStatus: remembered.messageStatus,
2464
+ currentHasRuntime: hasUsableRuntime(details)
2465
+ });
2466
+ return {
2467
+ ...details,
2468
+ ...remembered,
2469
+ error: details.error ?? remembered.error
2470
+ };
2471
+ }
2361
2472
  async init() {
2473
+ this.trace("init:start", {
2474
+ launcherMode: this.props.launcherMode ?? "floating",
2475
+ manifestUrl: this.state.manifestUrl
2476
+ });
2362
2477
  this.stopWalletWatch = watchWallet(() => {
2478
+ this.trace("wallet:changed");
2363
2479
  void this.refreshWalletDerivedState();
2364
2480
  });
2365
2481
  await this.refresh();
@@ -2371,6 +2487,7 @@ var SponsorRelayController = class {
2371
2487
  }, RELAY_PING_INTERVAL_MS);
2372
2488
  await this.refreshRelayPing();
2373
2489
  this.patch({ ready: true });
2490
+ this.trace("init:ready");
2374
2491
  }
2375
2492
  destroy() {
2376
2493
  if (this.refreshTimer) clearInterval(this.refreshTimer);
@@ -2471,6 +2588,11 @@ var SponsorRelayController = class {
2471
2588
  }
2472
2589
  }
2473
2590
  async refresh() {
2591
+ this.trace("refresh:start", {
2592
+ wallet: this.state.wallet.address,
2593
+ manifestUrl: this.state.manifestUrl,
2594
+ showInstances: this.state.showInstances
2595
+ });
2474
2596
  this.patch({
2475
2597
  busy: { refreshing: true },
2476
2598
  errorText: null,
@@ -2496,29 +2618,58 @@ var SponsorRelayController = class {
2496
2618
  balance = nextBalance;
2497
2619
  instances = await Promise.all(
2498
2620
  rawInstances.map(async (instance) => {
2499
- const details = this.canSkipRuntimeRefresh(instance) ? {
2500
- messageStatus: String(
2501
- instance.status ?? (instance.confirmed ? "processed" : "pending")
2502
- ).toLowerCase(),
2503
- allocationSource: null,
2504
- crnUrl: null,
2505
- hostIpv4: null,
2506
- ipv6: null,
2507
- vmIpv4: null,
2508
- webUrl: null,
2509
- sshCommand: null,
2510
- mappedPorts: [],
2511
- execution: null,
2512
- error: null
2621
+ const inspected = this.canSkipRuntimeRefresh(instance) ? {
2622
+ details: {
2623
+ messageStatus: String(
2624
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
2625
+ ).toLowerCase(),
2626
+ allocationSource: null,
2627
+ crnUrl: null,
2628
+ hostIpv4: null,
2629
+ ipv6: null,
2630
+ vmIpv4: null,
2631
+ webUrl: null,
2632
+ sshCommand: null,
2633
+ mappedPorts: [],
2634
+ execution: null,
2635
+ error: null
2636
+ },
2637
+ lookup: {
2638
+ allocationFound: false,
2639
+ allocationSource: null,
2640
+ crnUrl: null,
2641
+ webUrl: null,
2642
+ executionPayloadFound: false,
2643
+ executionLookupBlocked: false,
2644
+ executionLookupRequestUrl: null,
2645
+ executionLookupVersion: null
2646
+ }
2513
2647
  } : await inspectInstanceRuntime({
2514
2648
  client: this.client,
2515
2649
  instance,
2516
2650
  crns
2517
2651
  });
2518
- this.noteRuntimeRefreshResult(instance, details);
2652
+ const details = inspected.details;
2653
+ const mergedDetails = this.mergeRememberedRuntimeDetails(
2654
+ instance.item_hash,
2655
+ details
2656
+ );
2657
+ if (this.debugEnabled && instance.item_hash === this.state.lastDeploymentHash) {
2658
+ this.trace("runtime:inspect-result", {
2659
+ itemHash: instance.item_hash,
2660
+ messageStatus: details.messageStatus,
2661
+ hostIpv4: details.hostIpv4,
2662
+ vmIpv4: details.vmIpv4,
2663
+ mappedPorts: details.mappedPorts,
2664
+ error: details.error,
2665
+ lookup: inspected.lookup
2666
+ });
2667
+ }
2668
+ this.rememberRuntimeDetails(instance.item_hash, mergedDetails);
2669
+ this.noteRuntimeRefreshResult(instance, mergedDetails);
2519
2670
  return {
2520
2671
  instance,
2521
- details
2672
+ details: mergedDetails
2522
2673
  };
2523
2674
  })
2524
2675
  );
@@ -2555,9 +2706,15 @@ var SponsorRelayController = class {
2555
2706
  busy: { refreshing: false },
2556
2707
  statusText: "Relay sponsor data ready"
2557
2708
  });
2709
+ this.trace("refresh:success", {
2710
+ manifestValid: manifestState.valid,
2711
+ rootfsVerified,
2712
+ instances: instances.length
2713
+ });
2558
2714
  this.recomputePricingSummary();
2559
2715
  this.syncLatestDeploymentProgress(instances);
2560
2716
  } catch (error) {
2717
+ this.trace("refresh:error", error);
2561
2718
  this.patch({
2562
2719
  busy: { refreshing: false },
2563
2720
  errorText: error instanceof Error ? error.message : String(error),
@@ -2568,8 +2725,17 @@ var SponsorRelayController = class {
2568
2725
  async refreshRelayPing() {
2569
2726
  const relayPing = await pingPeer(this.props.libp2p);
2570
2727
  this.patch({ relayPing });
2728
+ this.trace("libp2p:relay-ping", relayPing);
2571
2729
  }
2572
2730
  async deploy() {
2731
+ this.trace("deploy:start", {
2732
+ wallet: this.state.wallet.address,
2733
+ instanceName: this.state.instanceName,
2734
+ tierId: this.state.tierId,
2735
+ selectedCrn: this.state.selectedCrn?.name ?? null,
2736
+ manifestRootfsItemHash: this.state.manifest?.rootfsItemHash ?? null,
2737
+ requiredPortForwards: this.state.manifest?.requiredPortForwards ?? []
2738
+ });
2573
2739
  if (!this.state.wallet.address) {
2574
2740
  this.patch({ errorText: "Connect MetaMask before deploying." });
2575
2741
  return;
@@ -2634,6 +2800,8 @@ var SponsorRelayController = class {
2634
2800
  statusText: `Deployment submitted: ${result.itemHash}`,
2635
2801
  lastDeploymentHash: result.itemHash
2636
2802
  });
2803
+ this.trace("deploy:broadcasted", result);
2804
+ this.lastRuntimeReadyHash = null;
2637
2805
  const inspection = await waitForDeploymentResult2(result.itemHash, {
2638
2806
  rootfsRef: this.state.manifest.rootfsItemHash,
2639
2807
  apiHost: this.client.apiHost,
@@ -2679,6 +2847,7 @@ var SponsorRelayController = class {
2679
2847
  inspection.rejectionReason ?? `Deployment ${result.itemHash} stayed ${inspection.status} on Aleph.`
2680
2848
  );
2681
2849
  }
2850
+ this.trace("deploy:aleph-processed", inspection);
2682
2851
  if ((this.state.manifest.requiredPortForwards?.length ?? 0) > 0) {
2683
2852
  this.emitProgress({
2684
2853
  stage: "refreshing-instances",
@@ -2698,8 +2867,18 @@ var SponsorRelayController = class {
2698
2867
  apiHost: this.client.apiHost,
2699
2868
  sync: true
2700
2869
  });
2870
+ this.trace("deploy:port-forwards-published", {
2871
+ itemHash: result.itemHash,
2872
+ requiredPortForwards: this.state.manifest.requiredPortForwards
2873
+ });
2701
2874
  }
2702
2875
  if (this.state.selectedCrn?.address) {
2876
+ this.trace("deploy:notifying-crn", {
2877
+ itemHash: result.itemHash,
2878
+ crnName: this.state.selectedCrn.name,
2879
+ crnHash: this.state.selectedCrn.hash,
2880
+ crnUrl: this.state.selectedCrn.address
2881
+ });
2703
2882
  await notifyCrnAllocationWithRetry({
2704
2883
  crnUrl: this.state.selectedCrn.address,
2705
2884
  itemHash: result.itemHash,
@@ -2752,6 +2931,21 @@ var SponsorRelayController = class {
2752
2931
  runtime.diagnostics?.reason ?? "Deployment was processed, but runtime networking never exposed mapped ports."
2753
2932
  );
2754
2933
  }
2934
+ this.trace("deploy:runtime-ready", runtime);
2935
+ this.lastRuntimeReadyHash = result.itemHash;
2936
+ this.rememberRuntimeDetails(result.itemHash, {
2937
+ messageStatus: "processed",
2938
+ allocationSource: runtime.allocation?.source ?? null,
2939
+ crnUrl: runtime.allocation?.crnUrl ?? runtime.execution?.crnUrl ?? null,
2940
+ hostIpv4: runtime.hostIpv4 ?? null,
2941
+ ipv6: runtime.ipv6 ?? null,
2942
+ vmIpv4: runtime.execution?.networking?.ipv4_ip ?? null,
2943
+ webUrl: runtime.webAccessUrl ?? runtime.proxyUrl ?? runtime.execution?.networking?.proxy_url ?? null,
2944
+ sshCommand: runtime.sshCommand ?? null,
2945
+ mappedPorts: runtime.execution != null ? mappedPorts(runtime.execution) : runtimeMappedPorts(runtime.mappedPorts),
2946
+ execution: runtime.execution ?? null,
2947
+ error: null
2948
+ });
2755
2949
  this.patch({
2756
2950
  busy: { deploying: false }
2757
2951
  });
@@ -2765,6 +2959,7 @@ var SponsorRelayController = class {
2765
2959
  });
2766
2960
  await this.refresh();
2767
2961
  } catch (error) {
2962
+ this.trace("deploy:error", error);
2768
2963
  this.patch({
2769
2964
  busy: { deploying: false },
2770
2965
  errorText: error instanceof Error ? error.message : String(error),
@@ -2780,6 +2975,7 @@ var SponsorRelayController = class {
2780
2975
  }
2781
2976
  }
2782
2977
  async deleteInstance(instanceHash) {
2978
+ this.trace("delete:start", { instanceHash });
2783
2979
  if (!this.state.wallet.address) {
2784
2980
  this.patch({ errorText: "Connect MetaMask before deleting instances." });
2785
2981
  return;
@@ -2827,6 +3023,7 @@ var SponsorRelayController = class {
2827
3023
  detail: "Reloading current deployments."
2828
3024
  });
2829
3025
  await this.refresh();
3026
+ this.trace("delete:submitted", { instanceHash });
2830
3027
  this.emitProgress({
2831
3028
  stage: "completed",
2832
3029
  label: "Delete completed",
@@ -2837,6 +3034,7 @@ var SponsorRelayController = class {
2837
3034
  error: null
2838
3035
  });
2839
3036
  } catch (error) {
3037
+ this.trace("delete:error", error);
2840
3038
  this.patch({
2841
3039
  busy: { deletingInstanceHash: null },
2842
3040
  errorText: error instanceof Error ? error.message : String(error),