@xh/hoist 71.0.0-SNAPSHOT.1735844948971 → 71.0.0-SNAPSHOT.1736119965537

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/admin/AppModel.ts +15 -8
  3. package/admin/tabs/cluster/ClusterTab.ts +14 -63
  4. package/admin/tabs/cluster/{BaseInstanceModel.ts → instances/BaseInstanceModel.ts} +2 -2
  5. package/admin/tabs/cluster/instances/InstancesTab.ts +73 -0
  6. package/admin/tabs/cluster/{ClusterTabModel.ts → instances/InstancesTabModel.ts} +16 -16
  7. package/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorModel.ts +1 -1
  8. package/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorPanel.ts +1 -1
  9. package/admin/tabs/cluster/{environment → instances/environment}/ServerEnvModel.ts +1 -1
  10. package/admin/tabs/cluster/{environment → instances/environment}/ServerEnvPanel.ts +1 -1
  11. package/admin/tabs/cluster/{logs → instances/logs}/LogViewer.ts +1 -1
  12. package/admin/tabs/cluster/{logs → instances/logs}/LogViewerModel.ts +1 -1
  13. package/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialog.ts +1 -1
  14. package/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialogModel.ts +1 -1
  15. package/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorModel.ts +1 -1
  16. package/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorPanel.ts +1 -1
  17. package/admin/tabs/cluster/{services → instances/services}/DetailsPanel.ts +1 -1
  18. package/admin/tabs/cluster/{services → instances/services}/ServiceModel.ts +3 -3
  19. package/admin/tabs/cluster/{services → instances/services}/ServicePanel.ts +1 -1
  20. package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketModel.ts +1 -1
  21. package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketPanel.ts +1 -1
  22. package/admin/tabs/cluster/objects/ClusterObjects.scss +25 -0
  23. package/admin/tabs/cluster/objects/ClusterObjectsModel.ts +427 -0
  24. package/admin/tabs/cluster/objects/ClusterObjectsPanel.ts +114 -0
  25. package/admin/tabs/cluster/objects/DetailModel.ts +158 -0
  26. package/admin/tabs/cluster/objects/DetailPanel.ts +51 -0
  27. package/build/types/admin/tabs/cluster/ClusterTab.d.ts +1 -4
  28. package/build/types/admin/tabs/cluster/{BaseInstanceModel.d.ts → instances/BaseInstanceModel.d.ts} +2 -2
  29. package/build/types/admin/tabs/cluster/instances/InstancesTab.d.ts +4 -0
  30. package/build/types/admin/tabs/cluster/{ClusterTabModel.d.ts → instances/InstancesTabModel.d.ts} +2 -1
  31. package/build/types/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorModel.d.ts +1 -1
  32. package/build/types/admin/tabs/cluster/{connpool → instances/connpool}/ConnPoolMonitorPanel.d.ts +1 -1
  33. package/build/types/admin/tabs/cluster/{environment → instances/environment}/ServerEnvModel.d.ts +1 -1
  34. package/build/types/admin/tabs/cluster/{environment → instances/environment}/ServerEnvPanel.d.ts +1 -1
  35. package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogViewerModel.d.ts +1 -1
  36. package/build/types/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialog.d.ts +1 -1
  37. package/build/types/admin/tabs/cluster/{logs → instances/logs}/levels/LogLevelDialogModel.d.ts +1 -1
  38. package/build/types/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorModel.d.ts +1 -1
  39. package/build/types/admin/tabs/cluster/{memory → instances/memory}/MemoryMonitorPanel.d.ts +1 -1
  40. package/build/types/admin/tabs/cluster/{services → instances/services}/DetailsPanel.d.ts +1 -1
  41. package/build/types/admin/tabs/cluster/{services → instances/services}/ServiceModel.d.ts +1 -1
  42. package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketModel.d.ts +1 -1
  43. package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketPanel.d.ts +1 -1
  44. package/build/types/admin/tabs/cluster/objects/ClusterObjectsModel.d.ts +30 -0
  45. package/build/types/admin/tabs/cluster/objects/ClusterObjectsPanel.d.ts +3 -0
  46. package/build/types/admin/tabs/cluster/objects/DetailModel.d.ts +19 -0
  47. package/build/types/admin/tabs/cluster/objects/DetailPanel.d.ts +3 -0
  48. package/build/types/cmp/viewmanager/ViewManagerModel.d.ts +3 -3
  49. package/cmp/viewmanager/ViewManagerModel.ts +3 -3
  50. package/desktop/cmp/viewmanager/dialog/ViewPanelModel.ts +5 -11
  51. package/package.json +1 -1
  52. package/tsconfig.tsbuildinfo +1 -1
  53. package/admin/tabs/cluster/distobjects/DistributedObjectsModel.ts +0 -199
  54. package/admin/tabs/cluster/distobjects/DistributedObjectsPanel.ts +0 -99
  55. package/build/types/admin/tabs/cluster/distobjects/DistributedObjectsModel.d.ts +0 -16
  56. package/build/types/admin/tabs/cluster/distobjects/DistributedObjectsPanel.d.ts +0 -2
  57. /package/admin/tabs/cluster/{logs → instances/logs}/LogDisplay.ts +0 -0
  58. /package/admin/tabs/cluster/{logs → instances/logs}/LogDisplayModel.ts +0 -0
  59. /package/admin/tabs/cluster/{logs → instances/logs}/LogViewer.scss +0 -0
  60. /package/admin/tabs/cluster/{services → instances/services}/DetailsModel.ts +0 -0
  61. /package/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketColumns.ts +0 -0
  62. /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogDisplay.d.ts +0 -0
  63. /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogDisplayModel.d.ts +0 -0
  64. /package/build/types/admin/tabs/cluster/{logs → instances/logs}/LogViewer.d.ts +0 -0
  65. /package/build/types/admin/tabs/cluster/{services → instances/services}/DetailsModel.d.ts +0 -0
  66. /package/build/types/admin/tabs/cluster/{services → instances/services}/ServicePanel.d.ts +0 -0
  67. /package/build/types/admin/tabs/cluster/{websocket → instances/websocket}/WebSocketColumns.d.ts +0 -0
@@ -0,0 +1,51 @@
1
+ /*
2
+ * This file belongs to Hoist, an application development toolkit
3
+ * developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
4
+ *
5
+ * Copyright © 2025 Extremely Heavy Industries Inc.
6
+ */
7
+ import {grid} from '@xh/hoist/cmp/grid';
8
+ import {placeholder} from '@xh/hoist/cmp/layout';
9
+ import {creates, hoistCmp} from '@xh/hoist/core';
10
+ import {jsonInput} from '@xh/hoist/desktop/cmp/input';
11
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
12
+ import {Icon} from '@xh/hoist/icon';
13
+ import {DetailModel} from './DetailModel';
14
+ import './ClusterObjects.scss';
15
+
16
+ export const detailPanel = hoistCmp.factory({
17
+ model: creates(DetailModel),
18
+
19
+ render({model}) {
20
+ const {instanceName, selectedAdminStats, objectName, objectType} = model;
21
+ if (!objectName) return placeholder(Icon.grip(), 'Select an object');
22
+
23
+ return panel({
24
+ title: `${objectType} - ${objectName}`,
25
+ icon: Icon.diff(),
26
+ compactHeader: true,
27
+ items: [
28
+ grid({flex: 1}),
29
+ panel({
30
+ title: `Instance - ${instanceName}`,
31
+ omit: !instanceName,
32
+ compactHeader: true,
33
+ modelConfig: {
34
+ side: 'bottom',
35
+ defaultSize: '80%',
36
+ collapsible: false
37
+ },
38
+ item: jsonInput({
39
+ readonly: true,
40
+ flex: 1,
41
+ width: '100%',
42
+ height: '100%',
43
+ showFullscreenButton: false,
44
+ editorProps: {lineNumbers: false},
45
+ value: model.fmtStats(selectedAdminStats)
46
+ })
47
+ })
48
+ ]
49
+ });
50
+ }
51
+ });
@@ -1,4 +1 @@
1
- import { ClusterTabModel } from './ClusterTabModel';
2
- export declare const clusterTab: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ClusterTabModel>>;
3
- export declare const detailPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ClusterTabModel>>;
4
- export declare const failedConnectionMask: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ClusterTabModel>>;
1
+ export declare const clusterTab: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<import("@xh/hoist/core").HoistModel>>;
@@ -1,9 +1,9 @@
1
1
  /// <reference types="react" />
2
- import { ClusterTabModel } from '@xh/hoist/admin/tabs/cluster/ClusterTabModel';
2
+ import { InstancesTabModel } from '@xh/hoist/admin/tabs/cluster/instances/InstancesTabModel';
3
3
  import { HoistModel, LoadSpec, PlainObject } from '@xh/hoist/core';
4
4
  export declare class BaseInstanceModel extends HoistModel {
5
5
  viewRef: import("react").RefObject<HTMLElement>;
6
- parent: ClusterTabModel;
6
+ parent: InstancesTabModel;
7
7
  get instanceName(): string;
8
8
  fmtStats(stats: PlainObject): string;
9
9
  handleLoadException(e: unknown, loadSpec: LoadSpec): void;
@@ -0,0 +1,4 @@
1
+ import { InstancesTabModel } from '@xh/hoist/admin/tabs/cluster/instances/InstancesTabModel';
2
+ export declare const instancesTab: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<InstancesTabModel>>;
3
+ export declare const detailPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<InstancesTabModel>>;
4
+ export declare const failedConnectionMask: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<InstancesTabModel>>;
@@ -4,7 +4,7 @@ import { HoistModel, LoadSpec, PlainObject } from '@xh/hoist/core';
4
4
  import { RecordActionSpec } from '@xh/hoist/data';
5
5
  import { Timer } from '@xh/hoist/utils/async';
6
6
  import { ReactNode } from 'react';
7
- export declare class ClusterTabModel extends HoistModel {
7
+ export declare class InstancesTabModel extends HoistModel {
8
8
  persistWith: {
9
9
  localStorageKey: string;
10
10
  };
@@ -16,6 +16,7 @@ export declare class ClusterTabModel extends HoistModel {
16
16
  get instance(): PlainObject;
17
17
  get instanceName(): string;
18
18
  get isMultiInstance(): boolean;
19
+ get instanceNames(): string[];
19
20
  doLoadAsync(loadSpec: LoadSpec): Promise<void>;
20
21
  constructor();
21
22
  formatInstance(instance: PlainObject): ReactNode;
@@ -1,4 +1,4 @@
1
- import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel';
1
+ import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
2
2
  import { ChartModel } from '@xh/hoist/cmp/chart';
3
3
  import { GridModel } from '@xh/hoist/cmp/grid';
4
4
  import { LoadSpec, PlainObject } from '@xh/hoist/core';
@@ -1,2 +1,2 @@
1
- import { ConnPoolMonitorModel } from '@xh/hoist/admin/tabs/cluster/connpool/ConnPoolMonitorModel';
1
+ import { ConnPoolMonitorModel } from '@xh/hoist/admin/tabs/cluster/instances/connpool/ConnPoolMonitorModel';
2
2
  export declare const connPoolMonitorPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ConnPoolMonitorModel>>;
@@ -1,4 +1,4 @@
1
- import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel';
1
+ import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
2
2
  import { GridModel } from '@xh/hoist/cmp/grid';
3
3
  import { LoadSpec } from '@xh/hoist/core';
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { ServerEnvModel } from '@xh/hoist/admin/tabs/cluster/environment/ServerEnvModel';
1
+ import { ServerEnvModel } from '@xh/hoist/admin/tabs/cluster/instances/environment/ServerEnvModel';
2
2
  export declare const serverEnvPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ServerEnvModel>>;
@@ -1,4 +1,4 @@
1
- import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel';
1
+ import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
2
2
  import { GridModel } from '@xh/hoist/cmp/grid';
3
3
  import { LoadSpec } from '@xh/hoist/core';
4
4
  import { RecordActionSpec } from '@xh/hoist/data';
@@ -1,2 +1,2 @@
1
- import { LogLevelDialogModel } from '@xh/hoist/admin/tabs/cluster/logs/levels/LogLevelDialogModel';
1
+ import { LogLevelDialogModel } from '@xh/hoist/admin/tabs/cluster/instances/logs/levels/LogLevelDialogModel';
2
2
  export declare const logLevelDialog: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<LogLevelDialogModel>>;
@@ -1,4 +1,4 @@
1
- import { LogViewerModel } from '@xh/hoist/admin/tabs/cluster/logs/LogViewerModel';
1
+ import { LogViewerModel } from '@xh/hoist/admin/tabs/cluster/instances/logs/LogViewerModel';
2
2
  import { HoistModel, LoadSpec } from '@xh/hoist/core';
3
3
  import { RestGridModel } from '@xh/hoist/desktop/cmp/rest';
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel';
1
+ import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
2
2
  import { ChartModel } from '@xh/hoist/cmp/chart';
3
3
  import { ColumnSpec, GridModel } from '@xh/hoist/cmp/grid';
4
4
  import { LoadSpec } from '@xh/hoist/core';
@@ -1,2 +1,2 @@
1
- import { MemoryMonitorModel } from '@xh/hoist/admin/tabs/cluster/memory/MemoryMonitorModel';
1
+ import { MemoryMonitorModel } from '@xh/hoist/admin/tabs/cluster/instances/memory/MemoryMonitorModel';
2
2
  export declare const memoryMonitorPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<MemoryMonitorModel>>;
@@ -1,2 +1,2 @@
1
- import { DetailsModel } from '@xh/hoist/admin/tabs/cluster/services/DetailsModel';
1
+ import { DetailsModel } from '@xh/hoist/admin/tabs/cluster/instances/services/DetailsModel';
2
2
  export declare const detailsPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<DetailsModel>>;
@@ -1,4 +1,4 @@
1
- import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel';
1
+ import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
2
2
  import { GridModel } from '@xh/hoist/cmp/grid';
3
3
  import { LoadSpec } from '@xh/hoist/core';
4
4
  import { FilterTestFn, RecordActionSpec } from '@xh/hoist/data';
@@ -1,4 +1,4 @@
1
- import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel';
1
+ import { BaseInstanceModel } from '@xh/hoist/admin/tabs/cluster/instances/BaseInstanceModel';
2
2
  import { GridModel } from '@xh/hoist/cmp/grid';
3
3
  import { LoadSpec } from '@xh/hoist/core';
4
4
  import { RecordActionSpec } from '@xh/hoist/data';
@@ -1,2 +1,2 @@
1
- import { WebSocketModel } from '@xh/hoist/admin/tabs/cluster/websocket/WebSocketModel';
1
+ import { WebSocketModel } from '@xh/hoist/admin/tabs/cluster/instances/websocket/WebSocketModel';
2
2
  export declare const webSocketPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<WebSocketModel>>;
@@ -0,0 +1,30 @@
1
+ /// <reference types="react" />
2
+ import { GridModel } from '@xh/hoist/cmp/grid';
3
+ import { HoistModel, LoadSpec } from '@xh/hoist/core';
4
+ import { FilterTestFn, RecordActionSpec, StoreRecord } from '@xh/hoist/data';
5
+ export declare class ClusterObjectsModel extends HoistModel {
6
+ viewRef: import("react").RefObject<HTMLElement>;
7
+ startTimestamp: Date;
8
+ runDurationMs: number;
9
+ hideUnchecked: boolean;
10
+ textFilter: FilterTestFn;
11
+ clearHibernateCachesAction: RecordActionSpec;
12
+ gridModel: GridModel;
13
+ get selectedRecord(): StoreRecord;
14
+ get isSingleInstance(): boolean;
15
+ get counts(): {
16
+ passed: number;
17
+ failed: number;
18
+ unchecked: number;
19
+ };
20
+ constructor();
21
+ clearHibernateCachesAsync(): Promise<void>;
22
+ clearAllHibernateCachesAsync(): Promise<void>;
23
+ doLoadAsync(loadSpec: LoadSpec): Promise<void>;
24
+ get isVisible(): boolean;
25
+ private applyFilters;
26
+ private processReport;
27
+ private createParentRecord;
28
+ private deriveParent;
29
+ private deriveDisplayName;
30
+ }
@@ -0,0 +1,3 @@
1
+ import { ClusterObjectsModel } from './ClusterObjectsModel';
2
+ import './ClusterObjects.scss';
3
+ export declare const clusterObjectsPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<ClusterObjectsModel>>;
@@ -0,0 +1,19 @@
1
+ import { ClusterObjectsModel } from '@xh/hoist/admin/tabs/cluster/objects/ClusterObjectsModel';
2
+ import { GridModel } from '@xh/hoist/cmp/grid';
3
+ import { HoistModel, PlainObject } from '@xh/hoist/core';
4
+ import { StoreRecord } from '@xh/hoist/data';
5
+ export declare class DetailModel extends HoistModel {
6
+ parent: ClusterObjectsModel;
7
+ gridModel: GridModel;
8
+ get selectedObject(): StoreRecord;
9
+ get objectName(): string;
10
+ get objectType(): string;
11
+ get instanceName(): string;
12
+ get selectedAdminStats(): any;
13
+ constructor();
14
+ fmtStats(stats: PlainObject): string;
15
+ private updateGridModel;
16
+ private createGridModel;
17
+ private createColSpec;
18
+ private processTimestamps;
19
+ }
@@ -0,0 +1,3 @@
1
+ import { DetailModel } from './DetailModel';
2
+ import './ClusterObjects.scss';
3
+ export declare const detailPanel: import("@xh/hoist/core").ElementFactory<import("@xh/hoist/core").DefaultHoistProps<DetailModel>>;
@@ -11,9 +11,9 @@ export interface ViewCreateSpec {
11
11
  value: PlainObject;
12
12
  }
13
13
  export interface ViewUpdateSpec {
14
- name: string;
15
- group: string;
16
- description: string;
14
+ name?: string;
15
+ group?: string;
16
+ description?: string;
17
17
  isShared?: boolean;
18
18
  isDefaultPinned?: boolean;
19
19
  }
@@ -37,9 +37,9 @@ export interface ViewCreateSpec {
37
37
  }
38
38
 
39
39
  export interface ViewUpdateSpec {
40
- name: string;
41
- group: string;
42
- description: string;
40
+ name?: string;
41
+ group?: string;
42
+ description?: string;
43
43
  isShared?: boolean;
44
44
  isDefaultPinned?: boolean;
45
45
  }
@@ -8,7 +8,7 @@
8
8
  import {FormModel} from '@xh/hoist/cmp/form';
9
9
  import {fragment, p, strong} from '@xh/hoist/cmp/layout';
10
10
  import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
11
- import {capitalize} from 'lodash';
11
+ import {capitalize, isUndefined} from 'lodash';
12
12
  import {ManageDialogModel} from './ManageDialogModel';
13
13
  import {makeObservable} from '@xh/hoist/mobx';
14
14
  import {ViewInfo} from '@xh/hoist/cmp/viewmanager';
@@ -55,14 +55,14 @@ export class ViewPanelModel extends HoistModel {
55
55
 
56
56
  async saveAsync() {
57
57
  const {parent, view, formModel} = this,
58
- {name, group, description, isDefaultPinned, isShared} = formModel.getData(),
58
+ updates = formModel.getData(true),
59
59
  isValid = await formModel.validateAsync(),
60
60
  isDirty = formModel.isDirty;
61
61
 
62
62
  if (!isValid || !isDirty) return;
63
63
 
64
- if (view.isOwned && view.isShared != isShared) {
65
- const msg: ReactNode = !isShared
64
+ if (view.isOwned && !isUndefined(updates.isShared)) {
65
+ const msg: ReactNode = !updates.isShared
66
66
  ? `Your ${view.typedName} will no longer be visible to all other ${XH.appName} users.`
67
67
  : `Your ${view.typedName} will become visible to all other ${XH.appName} users.`;
68
68
  const msgs = [msg, strong('Are you sure you want to proceed?')];
@@ -79,13 +79,7 @@ export class ViewPanelModel extends HoistModel {
79
79
  if (!confirmed) return;
80
80
  }
81
81
 
82
- await parent.updateAsync(view, {
83
- name,
84
- group,
85
- description,
86
- isShared,
87
- isDefaultPinned
88
- });
82
+ await parent.updateAsync(view, updates);
89
83
  }
90
84
 
91
85
  //------------------------
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xh/hoist",
3
- "version": "71.0.0-SNAPSHOT.1735844948971",
3
+ "version": "71.0.0-SNAPSHOT.1736119965537",
4
4
  "description": "Hoist add-on for building and deploying React Applications.",
5
5
  "repository": "github:xh/hoist-react",
6
6
  "homepage": "https://xh.io",