biz-a-cli 2.3.80-15375 → 2.3.80-15381

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/bin/app.js CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  import { validateTenancyDeclaration } from "../engine/orm/tenancyDeclaration.js";
56
56
  import { logger } from "../logger.js";
57
57
  import { findRawColorsInSource } from "../engine/domain/styleColors.js";
58
+ import { resolveMenuLayout } from "../engine/domain/domainNavigation.js";
58
59
 
59
60
  const getKeyFolderPath = () => {
60
61
  const scriptPath =
@@ -922,13 +923,39 @@ const parseApplicationConfigData = (rawConfig) => {
922
923
  }
923
924
  };
924
925
 
925
- const resolveAppDisplayNameFromApplicationConfig = async ({
926
+ /*
927
+ * What the stored APPLICATION_CONFIG contributes to the SYS$APPS metadata: the display name (which
928
+ * also keys the row) and the menu layout the client shell draws.
929
+ */
930
+ const resolveAppMetadataFromApplicationConfig = async ({
926
931
  server,
927
932
  apiPort,
928
933
  dbIndex,
929
934
  sub,
930
935
  }) => {
931
- /* A PostgreSQL tenant reads SYS$CONFIG directly. This must not silently return "": the
936
+ const applicationConfig = await readStoredApplicationConfig({
937
+ server,
938
+ apiPort,
939
+ dbIndex,
940
+ sub,
941
+ });
942
+ const displayName = applicationConfig?.metadata?.name;
943
+ const declaredLayout = applicationConfig?.navigation?.layout;
944
+ const menuLayout = resolveMenuLayout(applicationConfig);
945
+ if (declaredLayout != null && menuLayout == null) {
946
+ logger.warn(
947
+ `[Add] navigation.layout ${JSON.stringify(declaredLayout)} is not one of ` +
948
+ `"horizontal" or "vertical"; the client keeps its default menu.`,
949
+ );
950
+ }
951
+ return {
952
+ displayName: typeof displayName === "string" ? displayName.trim() : "",
953
+ menuLayout,
954
+ };
955
+ };
956
+
957
+ const readStoredApplicationConfig = async ({ server, apiPort, dbIndex, sub }) => {
958
+ /* A PostgreSQL tenant reads SYS$CONFIG directly. This must not silently come back empty: the
932
959
  display name keys the SYS$APPS row, so falling back to the random bundle name would insert
933
960
  a fresh app row on every publish instead of updating the existing one. */
934
961
  if (isPostgresIndex(dbIndex)) {
@@ -937,15 +964,14 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
937
964
  buildPostgresApplicationConfigLookupSql(),
938
965
  dbIndex,
939
966
  );
940
- const displayName = parseApplicationConfigData(rows?.[0]?.data)?.metadata?.name;
941
- return typeof displayName === "string" ? displayName.trim() : "";
967
+ return parseApplicationConfigData(rows?.[0]?.data);
942
968
  } catch {
943
- return "";
969
+ return null;
944
970
  }
945
971
  }
946
972
 
947
973
  if (typeof axios.request !== "function") {
948
- return "";
974
+ return null;
949
975
  }
950
976
 
951
977
  const baseUrl = "fina/rest/TOrmMethod/%22list%22";
@@ -989,18 +1015,16 @@ const resolveAppDisplayNameFromApplicationConfig = async ({
989
1015
  return name === "APPLICATION_CONFIG";
990
1016
  });
991
1017
  if (!applicationConfigRow) {
992
- return "";
1018
+ return null;
993
1019
  }
994
1020
 
995
- const applicationConfig = parseApplicationConfigData(
1021
+ return parseApplicationConfigData(
996
1022
  applicationConfigRow?.["SYS$CONFIG.DATA"] ??
997
1023
  applicationConfigRow?.DATA ??
998
1024
  applicationConfigRow?.data,
999
1025
  );
1000
- const displayName = applicationConfig?.metadata?.name;
1001
- return typeof displayName === "string" ? displayName.trim() : "";
1002
1026
  } catch {
1003
- return "";
1027
+ return null;
1004
1028
  }
1005
1029
  };
1006
1030
 
@@ -1161,8 +1185,8 @@ async function addApp({
1161
1185
  : `${server}:${apiPort}/${baseUrl}`;
1162
1186
 
1163
1187
  const headers = { "Content-Type": "text/plain" };
1164
- const appDisplayName =
1165
- await resolveAppDisplayNameFromApplicationConfig({
1188
+ const { displayName: appDisplayName, menuLayout } =
1189
+ await resolveAppMetadataFromApplicationConfig({
1166
1190
  server,
1167
1191
  apiPort,
1168
1192
  dbIndex,
@@ -1172,6 +1196,11 @@ async function addApp({
1172
1196
  if (useApplicationConfigName && appDisplayName.length > 0) {
1173
1197
  keys.metadata["name"] = appDisplayName;
1174
1198
  }
1199
+ /* navigation.layout -> how the client shell draws this app's menu. Only a value it
1200
+ understands is copied; without one the metadata stays exactly as before. */
1201
+ if (useApplicationConfigName && menuLayout) {
1202
+ keys.metadata["menuLayout"] = menuLayout;
1203
+ }
1175
1204
  const uploadAppName = normalizeAppName(
1176
1205
  (useApplicationConfigName ? appDisplayName : "") || bundleName,
1177
1206
  );
@@ -6,6 +6,8 @@ import { CLIENT_ROOM } from "../message-broker/clientEmitter.js";
6
6
  import { ClientEmitter } from "../message-broker/clientEmitter.js";
7
7
  import { joinExtRooms } from "../engine/ext/esb.js";
8
8
  import { extClientListener } from "../engine/ext/socketListener.js";
9
+ import { io as ioClient } from "socket.io-client";
10
+
9
11
  import { prefixedLogger } from "../logger.js";
10
12
  import { APP_URL, TEST_URL, ADMIN_URL, TEST_ADMIN_URL } from "../envs/hosts.js";
11
13
 
@@ -14,28 +16,99 @@ const logger = prefixedLogger("[DirectHub]");
14
16
  export async function localhostTunnel(port, notifier) {
15
17
  let tunnelUrl = "";
16
18
  let qt = QuickTunnel.quick("127.0.0.1:" + port);
19
+ let isReconnecting = false;
20
+ let loopbackSocket = null;
21
+
22
+ function destroyLoopback() {
23
+ if (loopbackSocket) {
24
+ loopbackSocket.disconnect();
25
+ loopbackSocket.close();
26
+ loopbackSocket = null;
27
+ }
28
+ }
29
+
30
+ const gracefulProcessShutdown = () => {
31
+ destroyLoopback();
32
+ if (qt && typeof qt.stop === "function") {
33
+ try {
34
+ qt.stop();
35
+ } catch (e) {}
36
+ }
37
+ };
38
+ process.once("SIGINT", gracefulProcessShutdown);
39
+ process.once("SIGTERM", gracefulProcessShutdown);
40
+
17
41
  function reconnectTunnel() {
42
+ process.off("SIGINT", gracefulProcessShutdown);
43
+ process.off("SIGTERM", gracefulProcessShutdown);
44
+ destroyLoopback();
45
+
46
+ if (isReconnecting) return;
47
+ isReconnecting = true;
48
+
49
+ if (qt && typeof qt.stop === "function") {
50
+ try {
51
+ qt.stop();
52
+ } catch (e) {}
53
+ }
54
+
18
55
  setTimeout(() => {
19
56
  qt = localhostTunnel(port, notifier);
20
57
  }, RECONNECT_SOCKET_DELAY);
21
58
  }
59
+
60
+ function startDirectHubHeartbeat() {
61
+ if (loopbackSocket && loopbackSocket.connected) return;
62
+
63
+ if (!tunnelUrl || !tunnelUrl.includes("trycloudflare.com")) {
64
+ logger.warn(`Invalid remote URL detected. Heartbeat aborted.`);
65
+ return;
66
+ }
67
+
68
+ destroyLoopback();
69
+
70
+ loopbackSocket = ioClient(tunnelUrl, {
71
+ query: { isLoopback: true },
72
+ transports: ["websocket"],
73
+ reconnectionDelay: 5000,
74
+ reconnectionAttempts: 3,
75
+ });
76
+
77
+ loopbackSocket.on("connect", () => {
78
+ logger.debug(
79
+ `Heartbeat established via public internet. Cloudflare idle-drop prevented.`,
80
+ );
81
+ });
82
+
83
+ loopbackSocket.io.on("reconnect_failed", () => {
84
+ logger.warn(
85
+ `Heartbeat failed to reach Edge. Ghost tunnel confirmed. Terminating...`,
86
+ );
87
+ notifier("");
88
+ destroyLoopback();
89
+ if (qt && typeof qt.stop === "function")
90
+ try {
91
+ qt.stop();
92
+ } catch (e) {}
93
+ });
94
+ }
95
+
22
96
  qt.on("url", (qtUrl) => {
23
97
  tunnelUrl = qtUrl;
24
98
  })
25
99
  .on("connected", (conn) => {
26
100
  notifier(tunnelUrl);
27
101
  logger.info(`Connected to Direct Hub at public URL ${tunnelUrl}`);
102
+ startDirectHubHeartbeat();
28
103
  })
29
104
  .on("disconnected", (conn) => {
30
105
  notifier("");
31
- qt.stop();
32
106
  logger.info(
33
107
  `Disconnected from edge node. Awaiting native reconnect...`,
34
108
  );
35
109
  })
36
110
  .on("error", (err) => {
37
111
  notifier("");
38
- qt.stop();
39
112
  logger.error(
40
113
  `Tunnel error: ${err.message || err}. Awaiting native reconnect...`,
41
114
  );
@@ -91,7 +164,10 @@ export function directHubEvent(serverSocket, argv) {
91
164
  if (clientInstanceId) {
92
165
  clientSocket.join(clientInstanceId);
93
166
  clientSocket.on("ESB_ACK", (msgId) => {
94
- ClientEmitter.acknowledgeMessage(msgId, clientInstanceId);
167
+ ClientEmitter.acknowledgeMessage(
168
+ msgId,
169
+ clientInstanceId,
170
+ );
95
171
  });
96
172
  }
97
173
 
@@ -108,6 +184,8 @@ export function directHubEvent(serverSocket, argv) {
108
184
  } else if (clientSocket.handshake.query.isDeploy) {
109
185
  setConnectListeners(clientSocket);
110
186
  deploymentListenerForVSCode(clientSocket, argv);
187
+ } else if (clientSocket.handshake.query.isLoopback) {
188
+ setConnectListeners(clientSocket);
111
189
  } else {
112
190
  clientSocket.disconnect();
113
191
  }
@@ -18,6 +18,23 @@
18
18
  /* The route shape the client's menu component renders, identical to a hand-declared leaf. */
19
19
  const linkFor = (entry) => ["./form", String(entry?.entryPoint ?? "").trim(), null];
20
20
 
21
+ /* The menu layouts the client shell can draw. */
22
+ const MENU_LAYOUTS = ["horizontal", "vertical"];
23
+
24
+ /*
25
+ * How the client shell draws this app's menu: `navigation.layout` in application.js.
26
+ *
27
+ * It reaches the browser as SYS$APPS.METADATA.menuLayout, beside the display name. Answers null when
28
+ * nothing usable is declared, so an app that never mentions it keeps its metadata exactly as it was
29
+ * and the client keeps the horizontal bar.
30
+ */
31
+ export const resolveMenuLayout = (applicationConfig) => {
32
+ const raw = applicationConfig?.navigation?.layout;
33
+ if (typeof raw !== "string") return null;
34
+ const layout = raw.trim().toLowerCase();
35
+ return MENU_LAYOUTS.includes(layout) ? layout : null;
36
+ };
37
+
21
38
  /*
22
39
  * Which of a domain's declared entries are live right now.
23
40
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biz-a-cli",
3
- "version": "2.3.80-15375",
3
+ "version": "2.3.80-15381",
4
4
  "description": "",
5
5
  "main": "bin/index.js",
6
6
  "type": "module",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "axios": "^1.7.8",
48
- "cloudflared": "^0.6.0",
48
+ "cloudflared": "^0.7.2",
49
49
  "compression": "^1.7.5",
50
50
  "cors": "^2.8.5",
51
51
  "dayjs": "^1.11.10",