@le-space/ui 0.1.59 → 0.1.60

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.59",
3
+ "version": "0.1.60",
4
4
  "description": "Floating Aleph sponsor relay UI components for Svelte and React.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/react/index.d.ts CHANGED
@@ -296,6 +296,7 @@ declare class SponsorRelayController {
296
296
  private stopWalletWatch;
297
297
  private props;
298
298
  private progressEmitter;
299
+ private runtimeCooldownByHash;
299
300
  constructor(props?: SponsorRelayProps);
300
301
  subscribeToDeploymentProgress(listener: DeploymentProgressListener): () => void;
301
302
  subscribe(subscriber: SponsorRelaySubscriber): () => void;
@@ -303,6 +304,9 @@ declare class SponsorRelayController {
303
304
  private emit;
304
305
  private patch;
305
306
  private emitProgress;
307
+ private syncLatestDeploymentProgress;
308
+ private canSkipRuntimeRefresh;
309
+ private noteRuntimeRefreshResult;
306
310
  init(): Promise<void>;
307
311
  destroy(): void;
308
312
  setOpen(open: boolean): void;
package/react/index.js CHANGED
@@ -32,6 +32,9 @@ var IDLE_DEPLOYMENT_PROGRESS = {
32
32
  };
33
33
  var REFRESH_INTERVAL_MS = 3e4;
34
34
  var RELAY_PING_INTERVAL_MS = 2e4;
35
+ var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
36
+ var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
37
+ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
35
38
 
36
39
  // src/shared/format.ts
37
40
  function shortHash(value, head = 8, tail = 6) {
@@ -1514,6 +1517,15 @@ async function inspectInstanceRuntime(args) {
1514
1517
  };
1515
1518
  }
1516
1519
  }
1520
+ function instanceTimestampMs(instance) {
1521
+ const value = instance.reception_time ?? instance.time;
1522
+ if (!value) return null;
1523
+ if (typeof value === "number") {
1524
+ return value > 0 && value < 1e10 ? value * 1e3 : value;
1525
+ }
1526
+ const parsed = Date.parse(value);
1527
+ return Number.isNaN(parsed) ? null : parsed;
1528
+ }
1517
1529
  function compatibleCrnsForTier(crns, state) {
1518
1530
  if (!state.pricingSummary.pricing || !state.pricingSummary.tier) {
1519
1531
  return [];
@@ -1575,6 +1587,9 @@ async function pingPeer(libp2p) {
1575
1587
  };
1576
1588
  }
1577
1589
  }
1590
+ function hasUsableRuntime(details) {
1591
+ return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1592
+ }
1578
1593
  var SponsorRelayController = class {
1579
1594
  state;
1580
1595
  subscribers = /* @__PURE__ */ new Set();
@@ -1584,6 +1599,7 @@ var SponsorRelayController = class {
1584
1599
  stopWalletWatch = null;
1585
1600
  props;
1586
1601
  progressEmitter = createDeploymentProgressEmitter();
1602
+ runtimeCooldownByHash = /* @__PURE__ */ new Map();
1587
1603
  constructor(props = {}) {
1588
1604
  this.props = props;
1589
1605
  this.state = defaultState(props);
@@ -1633,6 +1649,86 @@ var SponsorRelayController = class {
1633
1649
  });
1634
1650
  this.progressEmitter.emit(nextEvent);
1635
1651
  }
1652
+ syncLatestDeploymentProgress(instances) {
1653
+ const itemHash = this.state.lastDeploymentHash;
1654
+ if (!itemHash) {
1655
+ return;
1656
+ }
1657
+ const latest = instances.find((entry) => entry.instance.item_hash === itemHash);
1658
+ if (!latest) {
1659
+ return;
1660
+ }
1661
+ const status = latest.details.messageStatus;
1662
+ if (status === "rejected") {
1663
+ this.emitProgress({
1664
+ stage: "deployment-rejected",
1665
+ label: "Deployment rejected",
1666
+ progress: 100,
1667
+ status: "error",
1668
+ itemHash,
1669
+ detail: "Aleph rejected the deployment.",
1670
+ error: "Aleph rejected the deployment."
1671
+ });
1672
+ return;
1673
+ }
1674
+ if (status !== "processed") {
1675
+ this.emitProgress({
1676
+ stage: "waiting-for-aleph",
1677
+ label: "Waiting for Aleph",
1678
+ progress: 76,
1679
+ status: "info",
1680
+ itemHash,
1681
+ detail: "Deployment submitted. Waiting for Aleph to process the instance message."
1682
+ });
1683
+ return;
1684
+ }
1685
+ if (!hasUsableRuntime(latest.details)) {
1686
+ this.emitProgress({
1687
+ stage: "deployment-confirmed",
1688
+ label: "Waiting for runtime",
1689
+ progress: 88,
1690
+ status: "warning",
1691
+ itemHash,
1692
+ detail: "Aleph processed the deployment. Waiting for scheduler/runtime allocation details."
1693
+ });
1694
+ return;
1695
+ }
1696
+ this.emitProgress({
1697
+ stage: "completed",
1698
+ label: "Runtime ready",
1699
+ progress: 100,
1700
+ status: "success",
1701
+ itemHash,
1702
+ detail: "Deployment processed and runtime networking is available."
1703
+ });
1704
+ }
1705
+ canSkipRuntimeRefresh(instance) {
1706
+ if (instance.item_hash === this.state.lastDeploymentHash) {
1707
+ return false;
1708
+ }
1709
+ const cooldownUntil = this.runtimeCooldownByHash.get(instance.item_hash);
1710
+ if (!cooldownUntil) {
1711
+ return false;
1712
+ }
1713
+ return cooldownUntil > Date.now();
1714
+ }
1715
+ noteRuntimeRefreshResult(instance, details) {
1716
+ const hasRuntimeData = Boolean(details.crnUrl) || Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1717
+ if (hasRuntimeData || details.error || details.messageStatus !== "processed") {
1718
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1719
+ return;
1720
+ }
1721
+ const timestampMs = instanceTimestampMs(instance);
1722
+ const isRecent = timestampMs != null && Date.now() - timestampMs < RECENT_INSTANCE_RUNTIME_GRACE_MS;
1723
+ if (isRecent) {
1724
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1725
+ return;
1726
+ }
1727
+ this.runtimeCooldownByHash.set(
1728
+ instance.item_hash,
1729
+ Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
1730
+ );
1731
+ }
1636
1732
  async init() {
1637
1733
  this.stopWalletWatch = watchWallet(() => {
1638
1734
  void this.refreshWalletDerivedState();
@@ -1752,25 +1848,11 @@ var SponsorRelayController = class {
1752
1848
  statusText: "Refreshing relay sponsor data"
1753
1849
  });
1754
1850
  try {
1755
- this.emitProgress({
1756
- stage: "loading-manifest",
1757
- label: "Loading manifest",
1758
- progress: 8,
1759
- status: "info",
1760
- detail: this.state.manifestUrl
1761
- });
1762
1851
  const manifestState = await resolveManifest({
1763
1852
  manifestUrl: this.state.manifestUrl,
1764
1853
  manifestJson: this.state.manifestJson
1765
1854
  });
1766
1855
  const manifest = manifestState.manifest;
1767
- this.emitProgress({
1768
- stage: "loading-pricing",
1769
- label: "Loading pricing and CRNs",
1770
- progress: 18,
1771
- status: "info",
1772
- detail: "Fetching live Aleph pricing and compatible CRNs."
1773
- });
1774
1856
  const [pricingSummary, crns] = await Promise.all([
1775
1857
  fetchInstancePricing(this.client.apiHost),
1776
1858
  this.client.fetchCrns()
@@ -1778,39 +1860,43 @@ var SponsorRelayController = class {
1778
1860
  let balance = this.state.balance;
1779
1861
  let instances = [];
1780
1862
  if (this.state.wallet.address) {
1781
- this.emitProgress({
1782
- stage: "loading-balance",
1783
- label: "Loading wallet balance",
1784
- progress: 28,
1785
- status: "info",
1786
- detail: this.state.wallet.address
1787
- });
1788
1863
  const [nextBalance, rawInstances] = await Promise.all([
1789
1864
  this.client.fetchBalance(this.state.wallet.address),
1790
1865
  this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
1791
1866
  ]);
1792
1867
  balance = nextBalance;
1793
1868
  instances = await Promise.all(
1794
- rawInstances.map(async (instance) => ({
1795
- instance,
1796
- details: await inspectInstanceRuntime({
1869
+ rawInstances.map(async (instance) => {
1870
+ const details = this.canSkipRuntimeRefresh(instance) ? {
1871
+ messageStatus: String(
1872
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
1873
+ ).toLowerCase(),
1874
+ allocationSource: null,
1875
+ crnUrl: null,
1876
+ hostIpv4: null,
1877
+ ipv6: null,
1878
+ vmIpv4: null,
1879
+ webUrl: null,
1880
+ sshCommand: null,
1881
+ mappedPorts: [],
1882
+ execution: null,
1883
+ error: null
1884
+ } : await inspectInstanceRuntime({
1797
1885
  client: this.client,
1798
1886
  instance,
1799
1887
  crns
1800
- })
1801
- }))
1888
+ });
1889
+ this.noteRuntimeRefreshResult(instance, details);
1890
+ return {
1891
+ instance,
1892
+ details
1893
+ };
1894
+ })
1802
1895
  );
1803
1896
  }
1804
1897
  let rootfsVerified = false;
1805
1898
  let rootfsResolution = null;
1806
1899
  if (manifestState.valid && manifest) {
1807
- this.emitProgress({
1808
- stage: "verifying-rootfs",
1809
- label: "Verifying rootfs",
1810
- progress: 38,
1811
- status: "info",
1812
- detail: manifest.rootfsItemHash
1813
- });
1814
1900
  rootfsVerified = await verifyRootfsExists(
1815
1901
  manifest.rootfsItemHash,
1816
1902
  this.client.apiHost
@@ -1841,26 +1927,13 @@ var SponsorRelayController = class {
1841
1927
  statusText: "Relay sponsor data ready"
1842
1928
  });
1843
1929
  this.recomputePricingSummary();
1844
- this.emitProgress({
1845
- stage: "completed",
1846
- label: "Data ready",
1847
- progress: 100,
1848
- status: "success",
1849
- detail: "Manifest, pricing, and wallet state are ready."
1850
- });
1930
+ this.syncLatestDeploymentProgress(instances);
1851
1931
  } catch (error) {
1852
1932
  this.patch({
1853
1933
  busy: { refreshing: false },
1854
1934
  errorText: error instanceof Error ? error.message : String(error),
1855
1935
  statusText: "Refresh failed"
1856
1936
  });
1857
- this.emitProgress({
1858
- stage: "error",
1859
- label: "Refresh failed",
1860
- progress: 100,
1861
- status: "error",
1862
- error: error instanceof Error ? error.message : String(error)
1863
- });
1864
1937
  }
1865
1938
  }
1866
1939
  async refreshRelayPing() {
@@ -1952,15 +2025,6 @@ var SponsorRelayController = class {
1952
2025
  detail: "Reloading deployments and runtime state."
1953
2026
  });
1954
2027
  await this.refresh();
1955
- this.emitProgress({
1956
- stage: "completed",
1957
- label: "Deployment completed",
1958
- progress: 100,
1959
- status: result.status === "rejected" ? "error" : "success",
1960
- itemHash: result.itemHash,
1961
- detail: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : "Deployment submitted and synced back into the panel.",
1962
- error: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : null
1963
- });
1964
2028
  } catch (error) {
1965
2029
  this.patch({
1966
2030
  busy: { deploying: false },
@@ -2132,6 +2196,20 @@ function progressBadgeLabel(stage) {
2132
2196
  return "DEPLOY";
2133
2197
  }
2134
2198
  }
2199
+ var COMPLETED_PROGRESS_VISIBLE_MS = 12e3;
2200
+ function isDeploymentProgressVisible(state) {
2201
+ const stage = state.deploymentProgress.stage;
2202
+ if (stage === "idle") {
2203
+ return false;
2204
+ }
2205
+ if (stage === "error") {
2206
+ return true;
2207
+ }
2208
+ if (stage === "completed") {
2209
+ return Date.now() - state.deploymentProgress.timestamp < COMPLETED_PROGRESS_VISIBLE_MS;
2210
+ }
2211
+ return true;
2212
+ }
2135
2213
  function launcherIndicator(state) {
2136
2214
  if (state.deploymentProgress.stage !== "idle" && state.deploymentProgress.stage !== "completed") {
2137
2215
  return {
@@ -2140,7 +2218,7 @@ function launcherIndicator(state) {
2140
2218
  tone: state.deploymentProgress.status
2141
2219
  };
2142
2220
  }
2143
- if (state.deploymentProgress.stage === "completed") {
2221
+ if (state.deploymentProgress.stage === "completed" && isDeploymentProgressVisible(state)) {
2144
2222
  return {
2145
2223
  label: "DONE",
2146
2224
  detail: state.deploymentProgress.label,
@@ -2213,6 +2291,17 @@ function SponsorRelayFab(props) {
2213
2291
  state.deploymentProgress.status,
2214
2292
  state.deploymentProgress.timestamp
2215
2293
  ]);
2294
+ React.useEffect(() => {
2295
+ if (state.deploymentProgress.stage !== "completed") {
2296
+ return;
2297
+ }
2298
+ const timeout = window.setTimeout(() => {
2299
+ setSuccessFlash(false);
2300
+ }, COMPLETED_PROGRESS_VISIBLE_MS);
2301
+ return () => {
2302
+ window.clearTimeout(timeout);
2303
+ };
2304
+ }, [state.deploymentProgress.stage, state.deploymentProgress.timestamp]);
2216
2305
  React.useEffect(() => {
2217
2306
  if (launcherMode !== "inline" || !state.open) {
2218
2307
  return;
@@ -2258,6 +2347,7 @@ function SponsorRelayFab(props) {
2258
2347
  };
2259
2348
  }, [launcherMode]);
2260
2349
  const progressActive = state.deploymentProgress.stage !== "idle" && state.deploymentProgress.stage !== "completed" && state.deploymentProgress.stage !== "error";
2350
+ const deploymentProgressVisible = isDeploymentProgressVisible(state);
2261
2351
  const pulseScale = progressActive ? 1.03 : hovered ? 1.015 : 1;
2262
2352
  const pulseShadow = progressActive ? `0 0 0 4px ${progressToneColor(indicator.tone)}22, 0 12px 28px rgba(15, 23, 42, 0.22)` : successFlash ? `0 0 0 5px rgba(55, 214, 122, 0.22), 0 14px 32px rgba(55, 214, 122, 0.22)` : launcherMode === "inline" ? hovered ? "0 14px 30px rgba(79, 70, 229, 0.28)" : "0 10px 24px rgba(99, 102, 241, 0.22)" : void 0;
2263
2353
  const inlineButtonBackground = indicator.tone === "error" ? "linear-gradient(135deg, #b91c1c 0%, #dc2626 100%)" : indicator.tone === "warning" ? "linear-gradient(135deg, #7c3aed 0%, #8b5cf6 100%)" : indicator.tone === "success" ? "linear-gradient(135deg, #4f46e5 0%, #6366f1 100%)" : "linear-gradient(135deg, #4f46e5 0%, #6366f1 100%)";
@@ -2437,7 +2527,7 @@ function SponsorRelayFab(props) {
2437
2527
  ),
2438
2528
  /* @__PURE__ */ jsx("p", { style: { color: "#9fb2ca" }, children: state.statusText }),
2439
2529
  state.errorText ? /* @__PURE__ */ jsx("p", { style: { color: "#ffd9d9" }, children: state.errorText }) : null,
2440
- state.deploymentProgress.stage !== "idle" ? /* @__PURE__ */ jsxs(
2530
+ deploymentProgressVisible ? /* @__PURE__ */ jsxs(
2441
2531
  "div",
2442
2532
  {
2443
2533
  style: {
package/shared/index.d.ts CHANGED
@@ -293,6 +293,11 @@ declare const RELAY_PING_IDLE_STATE: RelayPingState;
293
293
  declare const IDLE_DEPLOYMENT_PROGRESS: DeploymentProgressEvent;
294
294
  declare const REFRESH_INTERVAL_MS = 30000;
295
295
  declare const RELAY_PING_INTERVAL_MS = 20000;
296
+ declare const INSTANCE_RUNTIME_READY_REFRESH_MS = 30000;
297
+ declare const INSTANCE_RUNTIME_PENDING_REFRESH_MS = 45000;
298
+ declare const INSTANCE_RUNTIME_STALE_REFRESH_MS: number;
299
+ declare const STALE_INSTANCE_ALLOCATION_COOLDOWN_MS: number;
300
+ declare const RECENT_INSTANCE_RUNTIME_GRACE_MS: number;
296
301
 
297
302
  declare function shortHash(value: string | null | undefined, head?: number, tail?: number): string;
298
303
  declare function formatNumber(value: number | null | undefined, digits?: number): string;
@@ -328,6 +333,7 @@ declare class SponsorRelayController {
328
333
  private stopWalletWatch;
329
334
  private props;
330
335
  private progressEmitter;
336
+ private runtimeCooldownByHash;
331
337
  constructor(props?: SponsorRelayProps);
332
338
  subscribeToDeploymentProgress(listener: DeploymentProgressListener): () => void;
333
339
  subscribe(subscriber: SponsorRelaySubscriber): () => void;
@@ -335,6 +341,9 @@ declare class SponsorRelayController {
335
341
  private emit;
336
342
  private patch;
337
343
  private emitProgress;
344
+ private syncLatestDeploymentProgress;
345
+ private canSkipRuntimeRefresh;
346
+ private noteRuntimeRefreshResult;
338
347
  init(): Promise<void>;
339
348
  destroy(): void;
340
349
  setOpen(open: boolean): void;
@@ -362,4 +371,4 @@ interface DeploymentProgressEmitter {
362
371
  }
363
372
  declare function createDeploymentProgressEmitter(): DeploymentProgressEmitter;
364
373
 
365
- export { type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, type DeploymentProgressEmitter, type EthereumProviderLike, IDLE_DEPLOYMENT_PROGRESS, REFRESH_INTERVAL_MS, RELAY_PING_IDLE_STATE, RELAY_PING_INTERVAL_MS, ROOTFS_MISSING_STATE, type RelayPingState, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayWalletState, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, getEthereumProvider, joinMappedPorts, personalSign, resolveManifestSource, shortHash, toChecksumAddress, watchWallet };
374
+ export { type CompactInstanceDetails, type CompactInstanceRecord, DEFAULT_INSTANCE_NAME, DEFAULT_MANIFEST_URL, DEFAULT_TIER_ID, type DeploymentProgressEmitter, type EthereumProviderLike, IDLE_DEPLOYMENT_PROGRESS, INSTANCE_RUNTIME_PENDING_REFRESH_MS, INSTANCE_RUNTIME_READY_REFRESH_MS, INSTANCE_RUNTIME_STALE_REFRESH_MS, RECENT_INSTANCE_RUNTIME_GRACE_MS, REFRESH_INTERVAL_MS, RELAY_PING_IDLE_STATE, RELAY_PING_INTERVAL_MS, ROOTFS_MISSING_STATE, type RelayPingState, STALE_INSTANCE_ALLOCATION_COOLDOWN_MS, SponsorRelayController, type SponsorRelayHealthTone, type SponsorRelayPricingSummary, type SponsorRelayProps, type SponsorRelayRootfsHealth, type SponsorRelayState, type SponsorRelaySubscriber, type SponsorRelayWalletState, buildSshCommand, connectWallet, createDeploymentProgressEmitter, createSponsorRelayController, formatDateTime, formatNumber, getEthereumProvider, joinMappedPorts, personalSign, resolveManifestSource, shortHash, toChecksumAddress, watchWallet };
package/shared/index.js CHANGED
@@ -29,6 +29,11 @@ var IDLE_DEPLOYMENT_PROGRESS = {
29
29
  };
30
30
  var REFRESH_INTERVAL_MS = 3e4;
31
31
  var RELAY_PING_INTERVAL_MS = 2e4;
32
+ var INSTANCE_RUNTIME_READY_REFRESH_MS = 3e4;
33
+ var INSTANCE_RUNTIME_PENDING_REFRESH_MS = 45e3;
34
+ var INSTANCE_RUNTIME_STALE_REFRESH_MS = 5 * 6e4;
35
+ var STALE_INSTANCE_ALLOCATION_COOLDOWN_MS = 5 * 60 * 1e3;
36
+ var RECENT_INSTANCE_RUNTIME_GRACE_MS = 10 * 60 * 1e3;
32
37
 
33
38
  // src/shared/format.ts
34
39
  function shortHash(value, head = 8, tail = 6) {
@@ -1511,6 +1516,15 @@ async function inspectInstanceRuntime(args) {
1511
1516
  };
1512
1517
  }
1513
1518
  }
1519
+ function instanceTimestampMs(instance) {
1520
+ const value = instance.reception_time ?? instance.time;
1521
+ if (!value) return null;
1522
+ if (typeof value === "number") {
1523
+ return value > 0 && value < 1e10 ? value * 1e3 : value;
1524
+ }
1525
+ const parsed = Date.parse(value);
1526
+ return Number.isNaN(parsed) ? null : parsed;
1527
+ }
1514
1528
  function compatibleCrnsForTier(crns, state) {
1515
1529
  if (!state.pricingSummary.pricing || !state.pricingSummary.tier) {
1516
1530
  return [];
@@ -1572,6 +1586,9 @@ async function pingPeer(libp2p) {
1572
1586
  };
1573
1587
  }
1574
1588
  }
1589
+ function hasUsableRuntime(details) {
1590
+ return Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1591
+ }
1575
1592
  var SponsorRelayController = class {
1576
1593
  state;
1577
1594
  subscribers = /* @__PURE__ */ new Set();
@@ -1581,6 +1598,7 @@ var SponsorRelayController = class {
1581
1598
  stopWalletWatch = null;
1582
1599
  props;
1583
1600
  progressEmitter = createDeploymentProgressEmitter();
1601
+ runtimeCooldownByHash = /* @__PURE__ */ new Map();
1584
1602
  constructor(props = {}) {
1585
1603
  this.props = props;
1586
1604
  this.state = defaultState(props);
@@ -1630,6 +1648,86 @@ var SponsorRelayController = class {
1630
1648
  });
1631
1649
  this.progressEmitter.emit(nextEvent);
1632
1650
  }
1651
+ syncLatestDeploymentProgress(instances) {
1652
+ const itemHash = this.state.lastDeploymentHash;
1653
+ if (!itemHash) {
1654
+ return;
1655
+ }
1656
+ const latest = instances.find((entry) => entry.instance.item_hash === itemHash);
1657
+ if (!latest) {
1658
+ return;
1659
+ }
1660
+ const status = latest.details.messageStatus;
1661
+ if (status === "rejected") {
1662
+ this.emitProgress({
1663
+ stage: "deployment-rejected",
1664
+ label: "Deployment rejected",
1665
+ progress: 100,
1666
+ status: "error",
1667
+ itemHash,
1668
+ detail: "Aleph rejected the deployment.",
1669
+ error: "Aleph rejected the deployment."
1670
+ });
1671
+ return;
1672
+ }
1673
+ if (status !== "processed") {
1674
+ this.emitProgress({
1675
+ stage: "waiting-for-aleph",
1676
+ label: "Waiting for Aleph",
1677
+ progress: 76,
1678
+ status: "info",
1679
+ itemHash,
1680
+ detail: "Deployment submitted. Waiting for Aleph to process the instance message."
1681
+ });
1682
+ return;
1683
+ }
1684
+ if (!hasUsableRuntime(latest.details)) {
1685
+ this.emitProgress({
1686
+ stage: "deployment-confirmed",
1687
+ label: "Waiting for runtime",
1688
+ progress: 88,
1689
+ status: "warning",
1690
+ itemHash,
1691
+ detail: "Aleph processed the deployment. Waiting for scheduler/runtime allocation details."
1692
+ });
1693
+ return;
1694
+ }
1695
+ this.emitProgress({
1696
+ stage: "completed",
1697
+ label: "Runtime ready",
1698
+ progress: 100,
1699
+ status: "success",
1700
+ itemHash,
1701
+ detail: "Deployment processed and runtime networking is available."
1702
+ });
1703
+ }
1704
+ canSkipRuntimeRefresh(instance) {
1705
+ if (instance.item_hash === this.state.lastDeploymentHash) {
1706
+ return false;
1707
+ }
1708
+ const cooldownUntil = this.runtimeCooldownByHash.get(instance.item_hash);
1709
+ if (!cooldownUntil) {
1710
+ return false;
1711
+ }
1712
+ return cooldownUntil > Date.now();
1713
+ }
1714
+ noteRuntimeRefreshResult(instance, details) {
1715
+ const hasRuntimeData = Boolean(details.crnUrl) || Boolean(details.hostIpv4) || Boolean(details.vmIpv4) || details.mappedPorts.length > 0 || Boolean(details.webUrl) || Boolean(details.execution);
1716
+ if (hasRuntimeData || details.error || details.messageStatus !== "processed") {
1717
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1718
+ return;
1719
+ }
1720
+ const timestampMs = instanceTimestampMs(instance);
1721
+ const isRecent = timestampMs != null && Date.now() - timestampMs < RECENT_INSTANCE_RUNTIME_GRACE_MS;
1722
+ if (isRecent) {
1723
+ this.runtimeCooldownByHash.delete(instance.item_hash);
1724
+ return;
1725
+ }
1726
+ this.runtimeCooldownByHash.set(
1727
+ instance.item_hash,
1728
+ Date.now() + STALE_INSTANCE_ALLOCATION_COOLDOWN_MS
1729
+ );
1730
+ }
1633
1731
  async init() {
1634
1732
  this.stopWalletWatch = watchWallet(() => {
1635
1733
  void this.refreshWalletDerivedState();
@@ -1749,25 +1847,11 @@ var SponsorRelayController = class {
1749
1847
  statusText: "Refreshing relay sponsor data"
1750
1848
  });
1751
1849
  try {
1752
- this.emitProgress({
1753
- stage: "loading-manifest",
1754
- label: "Loading manifest",
1755
- progress: 8,
1756
- status: "info",
1757
- detail: this.state.manifestUrl
1758
- });
1759
1850
  const manifestState = await resolveManifest({
1760
1851
  manifestUrl: this.state.manifestUrl,
1761
1852
  manifestJson: this.state.manifestJson
1762
1853
  });
1763
1854
  const manifest = manifestState.manifest;
1764
- this.emitProgress({
1765
- stage: "loading-pricing",
1766
- label: "Loading pricing and CRNs",
1767
- progress: 18,
1768
- status: "info",
1769
- detail: "Fetching live Aleph pricing and compatible CRNs."
1770
- });
1771
1855
  const [pricingSummary, crns] = await Promise.all([
1772
1856
  fetchInstancePricing(this.client.apiHost),
1773
1857
  this.client.fetchCrns()
@@ -1775,39 +1859,43 @@ var SponsorRelayController = class {
1775
1859
  let balance = this.state.balance;
1776
1860
  let instances = [];
1777
1861
  if (this.state.wallet.address) {
1778
- this.emitProgress({
1779
- stage: "loading-balance",
1780
- label: "Loading wallet balance",
1781
- progress: 28,
1782
- status: "info",
1783
- detail: this.state.wallet.address
1784
- });
1785
1862
  const [nextBalance, rawInstances] = await Promise.all([
1786
1863
  this.client.fetchBalance(this.state.wallet.address),
1787
1864
  this.state.showInstances ? this.client.fetchInstances(this.state.wallet.address) : Promise.resolve([])
1788
1865
  ]);
1789
1866
  balance = nextBalance;
1790
1867
  instances = await Promise.all(
1791
- rawInstances.map(async (instance) => ({
1792
- instance,
1793
- details: await inspectInstanceRuntime({
1868
+ rawInstances.map(async (instance) => {
1869
+ const details = this.canSkipRuntimeRefresh(instance) ? {
1870
+ messageStatus: String(
1871
+ instance.status ?? (instance.confirmed ? "processed" : "pending")
1872
+ ).toLowerCase(),
1873
+ allocationSource: null,
1874
+ crnUrl: null,
1875
+ hostIpv4: null,
1876
+ ipv6: null,
1877
+ vmIpv4: null,
1878
+ webUrl: null,
1879
+ sshCommand: null,
1880
+ mappedPorts: [],
1881
+ execution: null,
1882
+ error: null
1883
+ } : await inspectInstanceRuntime({
1794
1884
  client: this.client,
1795
1885
  instance,
1796
1886
  crns
1797
- })
1798
- }))
1887
+ });
1888
+ this.noteRuntimeRefreshResult(instance, details);
1889
+ return {
1890
+ instance,
1891
+ details
1892
+ };
1893
+ })
1799
1894
  );
1800
1895
  }
1801
1896
  let rootfsVerified = false;
1802
1897
  let rootfsResolution = null;
1803
1898
  if (manifestState.valid && manifest) {
1804
- this.emitProgress({
1805
- stage: "verifying-rootfs",
1806
- label: "Verifying rootfs",
1807
- progress: 38,
1808
- status: "info",
1809
- detail: manifest.rootfsItemHash
1810
- });
1811
1899
  rootfsVerified = await verifyRootfsExists(
1812
1900
  manifest.rootfsItemHash,
1813
1901
  this.client.apiHost
@@ -1838,26 +1926,13 @@ var SponsorRelayController = class {
1838
1926
  statusText: "Relay sponsor data ready"
1839
1927
  });
1840
1928
  this.recomputePricingSummary();
1841
- this.emitProgress({
1842
- stage: "completed",
1843
- label: "Data ready",
1844
- progress: 100,
1845
- status: "success",
1846
- detail: "Manifest, pricing, and wallet state are ready."
1847
- });
1929
+ this.syncLatestDeploymentProgress(instances);
1848
1930
  } catch (error) {
1849
1931
  this.patch({
1850
1932
  busy: { refreshing: false },
1851
1933
  errorText: error instanceof Error ? error.message : String(error),
1852
1934
  statusText: "Refresh failed"
1853
1935
  });
1854
- this.emitProgress({
1855
- stage: "error",
1856
- label: "Refresh failed",
1857
- progress: 100,
1858
- status: "error",
1859
- error: error instanceof Error ? error.message : String(error)
1860
- });
1861
1936
  }
1862
1937
  }
1863
1938
  async refreshRelayPing() {
@@ -1949,15 +2024,6 @@ var SponsorRelayController = class {
1949
2024
  detail: "Reloading deployments and runtime state."
1950
2025
  });
1951
2026
  await this.refresh();
1952
- this.emitProgress({
1953
- stage: "completed",
1954
- label: "Deployment completed",
1955
- progress: 100,
1956
- status: result.status === "rejected" ? "error" : "success",
1957
- itemHash: result.itemHash,
1958
- detail: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : "Deployment submitted and synced back into the panel.",
1959
- error: result.status === "rejected" ? result.rejectionReason ?? "Aleph rejected the deployment." : null
1960
- });
1961
2027
  } catch (error) {
1962
2028
  this.patch({
1963
2029
  busy: { deploying: false },
@@ -2047,10 +2113,15 @@ export {
2047
2113
  DEFAULT_MANIFEST_URL,
2048
2114
  DEFAULT_TIER_ID,
2049
2115
  IDLE_DEPLOYMENT_PROGRESS,
2116
+ INSTANCE_RUNTIME_PENDING_REFRESH_MS,
2117
+ INSTANCE_RUNTIME_READY_REFRESH_MS,
2118
+ INSTANCE_RUNTIME_STALE_REFRESH_MS,
2119
+ RECENT_INSTANCE_RUNTIME_GRACE_MS,
2050
2120
  REFRESH_INTERVAL_MS,
2051
2121
  RELAY_PING_IDLE_STATE,
2052
2122
  RELAY_PING_INTERVAL_MS,
2053
2123
  ROOTFS_MISSING_STATE,
2124
+ STALE_INSTANCE_ALLOCATION_COOLDOWN_MS,
2054
2125
  SponsorRelayController,
2055
2126
  buildSshCommand,
2056
2127
  connectWallet,