cyberia 3.2.9 → 3.2.22

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 (184) hide show
  1. package/.github/workflows/engine-cyberia.cd.yml +7 -0
  2. package/.github/workflows/engine-cyberia.ci.yml +14 -2
  3. package/.github/workflows/ghpkg.ci.yml +1 -0
  4. package/.github/workflows/npmpkg.ci.yml +10 -5
  5. package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
  6. package/.github/workflows/release.cd.yml +1 -0
  7. package/.vscode/extensions.json +9 -9
  8. package/.vscode/settings.json +20 -4
  9. package/CHANGELOG.md +363 -1
  10. package/CLI-HELP.md +975 -1061
  11. package/README.md +190 -348
  12. package/bin/build.js +102 -125
  13. package/bin/build.template.js +33 -0
  14. package/bin/cyberia.js +238 -56
  15. package/bin/deploy.js +16 -3
  16. package/bin/index.js +238 -56
  17. package/bump.config.js +26 -0
  18. package/conf.js +131 -24
  19. package/deployment.yaml +76 -2
  20. package/hardhat/package-lock.json +113 -144
  21. package/hardhat/package.json +4 -3
  22. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -2
  23. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
  24. package/manifests/deployment/dd-cyberia-development/deployment.yaml +76 -2
  25. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  26. package/manifests/kind-config-dev.yaml +8 -0
  27. package/manifests/lxd/lxd-admin-profile.yaml +12 -3
  28. package/manifests/mongodb/pv-pvc.yaml +44 -8
  29. package/manifests/mongodb/statefulset.yaml +55 -68
  30. package/manifests/mongodb-4.4/headless-service.yaml +10 -0
  31. package/manifests/mongodb-4.4/kustomization.yaml +3 -1
  32. package/manifests/mongodb-4.4/mongodb-nodeport.yaml +17 -0
  33. package/manifests/mongodb-4.4/pv-pvc.yaml +10 -14
  34. package/manifests/mongodb-4.4/statefulset.yaml +79 -0
  35. package/manifests/mongodb-4.4/storage-class.yaml +9 -0
  36. package/manifests/valkey/statefulset.yaml +1 -1
  37. package/manifests/valkey/valkey-nodeport.yaml +17 -0
  38. package/package.json +31 -19
  39. package/scripts/ipxe-setup.sh +52 -49
  40. package/scripts/k3s-node-setup.sh +81 -46
  41. package/scripts/link-local-underpost-cli.sh +6 -0
  42. package/scripts/lxd-vm-setup.sh +193 -8
  43. package/scripts/maas-nat-firewalld.sh +145 -0
  44. package/scripts/test-monitor.sh +250 -0
  45. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +38 -33
  46. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +16 -16
  47. package/src/api/core/core.router.js +19 -14
  48. package/src/api/core/core.service.js +5 -5
  49. package/src/api/crypto/crypto.router.js +18 -12
  50. package/src/api/crypto/crypto.service.js +3 -3
  51. package/src/api/cyberia-action/cyberia-action.model.js +1 -1
  52. package/src/api/cyberia-action/cyberia-action.router.js +22 -18
  53. package/src/api/cyberia-action/cyberia-action.service.js +5 -5
  54. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +74 -0
  55. package/src/api/cyberia-client-hints/cyberia-client-hints.model.js +99 -0
  56. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +98 -0
  57. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +152 -0
  58. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +25 -20
  59. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +6 -6
  60. package/src/api/cyberia-entity/cyberia-entity.router.js +22 -18
  61. package/src/api/cyberia-entity/cyberia-entity.service.js +5 -5
  62. package/src/api/cyberia-instance/cyberia-fallback-world.js +79 -4
  63. package/src/api/cyberia-instance/cyberia-instance.router.js +57 -52
  64. package/src/api/cyberia-instance/cyberia-instance.service.js +10 -10
  65. package/src/api/cyberia-instance/cyberia-world-generator.js +3 -3
  66. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +14 -48
  67. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +22 -18
  68. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +5 -5
  69. package/src/api/cyberia-map/cyberia-map.router.js +35 -30
  70. package/src/api/cyberia-map/cyberia-map.service.js +7 -7
  71. package/src/api/cyberia-quest/cyberia-quest.model.js +1 -1
  72. package/src/api/cyberia-quest/cyberia-quest.router.js +22 -18
  73. package/src/api/cyberia-quest/cyberia-quest.service.js +5 -5
  74. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +22 -18
  75. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +5 -5
  76. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +458 -0
  77. package/src/api/default/default.router.js +22 -18
  78. package/src/api/default/default.service.js +5 -5
  79. package/src/api/document/document.router.js +28 -23
  80. package/src/api/document/document.service.js +100 -23
  81. package/src/api/file/file.router.js +19 -13
  82. package/src/api/file/file.service.js +9 -7
  83. package/src/api/instance/instance.router.js +29 -24
  84. package/src/api/instance/instance.service.js +6 -6
  85. package/src/api/ipfs/ipfs.router.js +21 -16
  86. package/src/api/ipfs/ipfs.service.js +8 -8
  87. package/src/api/object-layer/object-layer.router.js +512 -507
  88. package/src/api/object-layer/object-layer.service.js +17 -14
  89. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +22 -18
  90. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +5 -5
  91. package/src/api/test/test.router.js +17 -12
  92. package/src/api/types.js +24 -0
  93. package/src/api/user/guest.service.js +5 -4
  94. package/src/api/user/user.router.js +297 -288
  95. package/src/api/user/user.service.js +100 -35
  96. package/src/cli/baremetal.js +132 -101
  97. package/src/cli/cluster.js +700 -232
  98. package/src/cli/db.js +59 -60
  99. package/src/cli/deploy.js +291 -294
  100. package/src/cli/env.js +1 -4
  101. package/src/cli/fs.js +13 -3
  102. package/src/cli/image.js +58 -4
  103. package/src/cli/index.js +127 -15
  104. package/src/cli/ipfs.js +4 -6
  105. package/src/cli/kubectl.js +4 -1
  106. package/src/cli/lxd.js +1099 -223
  107. package/src/cli/monitor.js +396 -9
  108. package/src/cli/release.js +355 -146
  109. package/src/cli/repository.js +169 -30
  110. package/src/cli/run.js +347 -117
  111. package/src/cli/secrets.js +11 -2
  112. package/src/cli/test.js +9 -3
  113. package/src/client/Default.index.js +9 -3
  114. package/src/client/components/core/Auth.js +5 -0
  115. package/src/client/components/core/ClientEvents.js +76 -0
  116. package/src/client/components/core/EventBus.js +4 -0
  117. package/src/client/components/core/Modal.js +82 -41
  118. package/src/client/components/core/PanelForm.js +14 -10
  119. package/src/client/components/core/Worker.js +162 -363
  120. package/src/client/components/cyberia/MapEngineCyberia.js +1 -1
  121. package/src/client/components/cyberia/SharedDefaultsCyberia.js +330 -0
  122. package/src/client/public/cyberia-docs/ACTION-SYSTEM.md +55 -1
  123. package/src/client/public/cyberia-docs/ARCHITECTURE.md +223 -361
  124. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +114 -327
  125. package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +200 -222
  126. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +212 -185
  127. package/src/client/public/cyberia-docs/CYBERIA.md +259 -0
  128. package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +2 -2
  129. package/src/client/public/cyberia-docs/QUEST-SYSTEM.md +23 -1
  130. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  131. package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +106 -0
  132. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  133. package/src/client/services/cyberia-client-hints/cyberia-client-hints.service.js +99 -0
  134. package/src/client/ssr/views/CyberiaServerMetrics.js +982 -0
  135. package/src/client/sw/core.sw.js +174 -112
  136. package/src/db/DataBaseProvider.js +115 -15
  137. package/src/db/mariadb/MariaDB.js +2 -1
  138. package/src/db/mongo/MongoBootstrap.js +657 -0
  139. package/src/db/mongo/MongooseDB.js +130 -21
  140. package/src/grpc/cyberia/grpc-server.js +25 -57
  141. package/src/index.js +1 -1
  142. package/src/runtime/cyberia-client/Dockerfile +10 -7
  143. package/src/runtime/cyberia-client/Dockerfile.dev +67 -0
  144. package/src/runtime/cyberia-server/Dockerfile +11 -6
  145. package/src/runtime/cyberia-server/Dockerfile.dev +47 -0
  146. package/src/runtime/express/Express.js +2 -2
  147. package/src/runtime/wp/Dockerfile +3 -3
  148. package/src/runtime/wp/Wp.js +8 -5
  149. package/src/server/auth.js +2 -2
  150. package/src/server/catalog-underpost.js +61 -0
  151. package/src/server/catalog.js +77 -0
  152. package/src/server/client-build-docs.js +1 -1
  153. package/src/server/client-build.js +94 -129
  154. package/src/server/conf.js +496 -135
  155. package/src/server/ipfs-client.js +5 -3
  156. package/src/server/process.js +180 -19
  157. package/src/server/proxy.js +9 -2
  158. package/src/server/runtime-status.js +235 -0
  159. package/src/server/runtime.js +1 -1
  160. package/src/server/start.js +44 -11
  161. package/src/server/valkey.js +2 -0
  162. package/src/ws/IoInterface.js +16 -16
  163. package/src/ws/core/channels/core.ws.chat.js +11 -11
  164. package/src/ws/core/channels/core.ws.mailer.js +29 -29
  165. package/src/ws/core/channels/core.ws.stream.js +19 -19
  166. package/src/ws/core/core.ws.connection.js +8 -8
  167. package/src/ws/core/core.ws.server.js +6 -5
  168. package/src/ws/default/channels/default.ws.main.js +10 -10
  169. package/src/ws/default/default.ws.connection.js +4 -4
  170. package/src/ws/default/default.ws.server.js +4 -3
  171. package/test/deploy-monitor.test.js +251 -0
  172. package/bin/file.js +0 -202
  173. package/bin/vs.js +0 -74
  174. package/bin/zed.js +0 -84
  175. package/manifests/deployment/dd-test-development/deployment.yaml +0 -254
  176. package/manifests/deployment/dd-test-development/proxy.yaml +0 -102
  177. package/src/api/cyberia-instance-conf/cyberia-instance-conf.defaults.js +0 -574
  178. package/src/client/components/cyberia-portal/CommonCyberiaPortal.js +0 -467
  179. package/src/client/ssr/email/DefaultRecoverEmail.js +0 -21
  180. package/src/client/ssr/email/DefaultVerifyEmail.js +0 -17
  181. package/src/client/ssr/pages/CyberiaServerMetrics.js +0 -461
  182. /package/src/client/ssr/{offline → views}/Maintenance.js +0 -0
  183. /package/src/client/ssr/{offline → views}/NoNetworkConnection.js +0 -0
  184. /package/src/client/ssr/{pages → views}/Test.js +0 -0
@@ -26,17 +26,26 @@ class UnderpostSecret {
26
26
  * @memberof UnderpostSecret
27
27
  */
28
28
  underpost: {
29
- createFromEnvFile(envPath) {
29
+ /**
30
+ * @method createFromEnvFile
31
+ * @description Reads application secrets from a .env file and writes them to the underpost .env file. Used for local development and testing.
32
+ * @param {string} envPath - The path to the .env file to read secrets from. Defaults to './.env'.
33
+ * @memberof UnderpostSecret
34
+ */
35
+ createFromEnvFile(envPath = './.env') {
30
36
  Underpost.env.clean();
31
37
  const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
32
38
  for (const key of Object.keys(envObj)) {
33
39
  Underpost.env.set(key, envObj[key]);
34
40
  }
35
41
  },
36
- /** Reads application secrets from process.env (injected via envFrom: secretRef)
42
+ /**
43
+ * @method createFromContainerEnv
44
+ * @description Reads application secrets from process.env (injected via envFrom: secretRef)
37
45
  * and writes them to the underpost .env file, filtering out known system and
38
46
  * Kubernetes-injected environment variables. Replaces the fragile shell-based
39
47
  * `printenv | grep -vE` pattern with a maintainable Node.js blocklist.
48
+ * @memberof UnderpostSecret
40
49
  */
41
50
  createFromContainerEnv() {
42
51
  Underpost.env.clean();
package/src/cli/test.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * @namespace UnderpostTest
5
5
  */
6
6
 
7
+ import fs from 'fs-extra';
7
8
  import { timer } from '../client/components/core/CommonJs.js';
8
9
  import { MariaDB } from '../db/mariadb/MariaDB.js';
9
10
  import { getNpmRootPath } from '../server/conf.js';
@@ -42,7 +43,13 @@ class UnderpostTest {
42
43
  */
43
44
  run() {
44
45
  actionInitLog();
45
- shellExec(`cd ${getNpmRootPath()}/underpost && npm run test`);
46
+ const underpostTestPath = `${getNpmRootPath()}/underpost`;
47
+ if (fs.existsSync(underpostTestPath)) {
48
+ shellExec(`cd ${underpostTestPath} && npm run test`);
49
+ } else {
50
+ logger.warn(`Global underpost not found at ${underpostTestPath}, running local npm test instead`);
51
+ shellExec('npm test');
52
+ }
46
53
  },
47
54
  /**
48
55
  * @method callback
@@ -148,8 +155,7 @@ class UnderpostTest {
148
155
  const pods = Underpost.kubectl.get(podName, kindType);
149
156
  let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
150
157
  logger.info(
151
- `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
152
- index + 1
158
+ `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${index + 1
153
159
  }/${maxAttempts}`,
154
160
  pods[0] ? pods[0].STATUS : 'Not found kind object',
155
161
  );
@@ -19,6 +19,12 @@ const DefaultTemplate = async () => {
19
19
  });
20
20
  });
21
21
  return html`
22
+ <style>
23
+ .feature-icon {
24
+ font-size: 2.5rem;
25
+ margin-bottom: 1rem;
26
+ }
27
+ </style>
22
28
  <div class="landing-container">
23
29
  <div class="content-wrapper">
24
30
  <h1 class="animated-text">
@@ -27,17 +33,17 @@ const DefaultTemplate = async () => {
27
33
  </h1>
28
34
  <div class="features">
29
35
  <div class="feature-card">
30
- <i class="icon">🚀</i>
36
+ <i class="fas fa-rocket feature-icon"></i>
31
37
  <h3>Fast &amp; Reliable</h3>
32
38
  <p>Lightning-fast performance with 99.9% uptime</p>
33
39
  </div>
34
40
  <div class="feature-card">
35
- <i class="icon">🎨</i>
41
+ <i class="fas fa-palette feature-icon"></i>
36
42
  <h3>Beautiful UI</h3>
37
43
  <p>Modern and intuitive user interface</p>
38
44
  </div>
39
45
  <div class="feature-card">
40
- <i class="icon">⚡</i>
46
+ <i class="fas fa-bolt feature-icon"></i>
41
47
  <h3>Powerful Features</h3>
42
48
  <p>Everything you need in one place</p>
43
49
  </div>
@@ -319,6 +319,11 @@ class Auth {
319
319
  // Close any open login/signup modals
320
320
  if (s(`.modal-log-in`)) s(`.btn-close-modal-log-in`).click();
321
321
  if (s(`.modal-sign-up`)) s(`.btn-close-modal-sign-up`).click();
322
+ if (!s(`.main-body-btn-ui-open`).classList.contains('hide')) s(`.main-body-btn-ui-open`).click();
323
+ if (!s(`.main-body-btn-ui-bar-custom-open`).classList.contains('hide')) {
324
+ SearchBox.Data.skipOpen = true;
325
+ s(`.main-body-btn-ui-bar-custom-open`).click();
326
+ }
322
327
  });
323
328
  }
324
329
 
@@ -45,6 +45,79 @@ const AppointmentEventType = {
45
45
  submitted: 'appointment:submitted',
46
46
  };
47
47
 
48
+ const ModalEventType = {
49
+ close: 'modal:close',
50
+ menu: 'modal:menu',
51
+ collapseMenu: 'modal:collapse-menu',
52
+ extendMenu: 'modal:extend-menu',
53
+ dragEnd: 'modal:drag-end',
54
+ observer: 'modal:observer',
55
+ click: 'modal:click',
56
+ expandUi: 'modal:expand-ui',
57
+ barUiOpen: 'modal:bar-ui-open',
58
+ barUiClose: 'modal:bar-ui-close',
59
+ reload: 'modal:reload',
60
+ home: 'modal:home',
61
+ };
62
+
63
+ const ModalListenerChannels = {
64
+ onCloseListener: ModalEventType.close,
65
+ onMenuListener: ModalEventType.menu,
66
+ onCollapseMenuListener: ModalEventType.collapseMenu,
67
+ onExtendMenuListener: ModalEventType.extendMenu,
68
+ onDragEndListener: ModalEventType.dragEnd,
69
+ onObserverListener: ModalEventType.observer,
70
+ onClickListener: ModalEventType.click,
71
+ onExpandUiListener: ModalEventType.expandUi,
72
+ onBarUiOpen: ModalEventType.barUiOpen,
73
+ onBarUiClose: ModalEventType.barUiClose,
74
+ onReloadModalListener: ModalEventType.reload,
75
+ onHome: ModalEventType.home,
76
+ };
77
+
78
+ const createModalEventChannel = (bus, type) => {
79
+ const busKey = (key) => `${type}::${key}`;
80
+ return new Proxy(
81
+ {},
82
+ {
83
+ get(_target, prop) {
84
+ if (typeof prop === 'symbol') return undefined;
85
+ const key = busKey(prop);
86
+ if (!bus.has(key)) return undefined;
87
+ return (detail) => bus.emitKey(key, detail);
88
+ },
89
+ set(_target, prop, value) {
90
+ if (typeof prop !== 'symbol' && typeof value === 'function') bus.on(type, value, { key: busKey(prop) });
91
+ return true;
92
+ },
93
+ deleteProperty(_target, prop) {
94
+ if (typeof prop !== 'symbol') bus.off(busKey(prop));
95
+ return true;
96
+ },
97
+ has(_target, prop) {
98
+ return typeof prop !== 'symbol' && bus.has(busKey(prop));
99
+ },
100
+ ownKeys() {
101
+ const prefix = `${type}::`;
102
+ return bus.keysOf(type).map((key) => String(key).slice(prefix.length));
103
+ },
104
+ getOwnPropertyDescriptor(_target, prop) {
105
+ if (typeof prop !== 'symbol' && bus.has(busKey(prop)))
106
+ return { enumerable: true, configurable: true, writable: true, value: undefined };
107
+ return undefined;
108
+ },
109
+ },
110
+ );
111
+ };
112
+
113
+ // One EventBus per modal id, surfaced through the legacy channel names.
114
+ const createModalEvents = () => {
115
+ const bus = new EventBus();
116
+ const channels = {};
117
+ for (const [name, type] of Object.entries(ModalListenerChannels)) channels[name] = createModalEventChannel(bus, type);
118
+ return { bus, channels };
119
+ };
120
+
48
121
  const authLoginEvents = new EventBus();
49
122
  const authLogoutEvents = new EventBus();
50
123
  const authSignupEvents = new EventBus();
@@ -70,6 +143,9 @@ export {
70
143
  KeyboardEventType,
71
144
  AccountEventType,
72
145
  AppointmentEventType,
146
+ ModalEventType,
147
+ ModalListenerChannels,
148
+ createModalEvents,
73
149
  authLoginEvents,
74
150
  authLogoutEvents,
75
151
  authSignupEvents,
@@ -69,6 +69,10 @@ class EventBus {
69
69
  return this.listeners.has(key);
70
70
  }
71
71
 
72
+ keysOf(type) {
73
+ return [...(this.typeKeys.get(type) ?? [])];
74
+ }
75
+
72
76
  async emit(type, detail) {
73
77
  if (!(this.typeKeys.get(type)?.size > 0)) return;
74
78
 
@@ -36,6 +36,7 @@ import { Worker } from './Worker.js';
36
36
  import { Scroll } from './Scroll.js';
37
37
  import { windowGetH, windowGetW } from './windowGetDimensions.js';
38
38
  import { SearchBox } from './SearchBox.js';
39
+ import { createModalEvents } from './ClientEvents.js';
39
40
 
40
41
  const logger = loggerFactory(import.meta, { trace: true });
41
42
 
@@ -93,6 +94,11 @@ const logger = loggerFactory(import.meta, { trace: true });
93
94
  /**
94
95
  * @typedef {object} ModalDataEntry
95
96
  * @property {ModalRenderOptions} options - Original render options.
97
+ * @property {import('./EventBus.js').EventBus} events - Per-modal event bus backing the listener channels below.
98
+ *
99
+ * The `onX` channels are EventBus-backed proxies that preserve the historical
100
+ * `{ [key]: listener }` map API: assign to register, read+call to invoke a single
101
+ * listener, `delete` to remove, and `Object.keys` to enumerate registered keys.
96
102
  * @property {Object.<string, Function>} onCloseListener - Close event listeners keyed by id.
97
103
  * @property {Object.<string, Function>} onMenuListener - Menu button event listeners.
98
104
  * @property {Object.<string, Function>} onCollapseMenuListener - Collapse menu listeners.
@@ -123,6 +129,69 @@ class Modal {
123
129
  /** @type {Object.<string, ModalDataEntry>} */
124
130
  static Data = {};
125
131
 
132
+ /**
133
+ * Bottom offset of the slide-menu area for the given options.
134
+ * @param {ModalRenderOptions} options
135
+ * @param {number} [heightDefaultBottomBar=0]
136
+ * @returns {number}
137
+ */
138
+ static getModalTop(options, heightDefaultBottomBar = 0) {
139
+ return windowGetH() - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar);
140
+ }
141
+
142
+ /**
143
+ * Available modal height, accounting for the top/bottom bars when the UI is expanded.
144
+ * @param {ModalRenderOptions} options
145
+ * @param {number} [heightDefaultTopBar=50]
146
+ * @param {number} [heightDefaultBottomBar=0]
147
+ * @returns {number}
148
+ */
149
+ static getModalHeight(options, heightDefaultTopBar = 50, heightDefaultBottomBar = 0) {
150
+ const barsVisible = s(`.main-body-btn-ui-close`) && !s(`.main-body-btn-ui-close`).classList.contains('hide');
151
+ return (
152
+ windowGetH() -
153
+ (barsVisible
154
+ ? (options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) +
155
+ (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
156
+ : 0)
157
+ );
158
+ }
159
+
160
+ /**
161
+ * Left CSS value for a slide menu in its open/closed state.
162
+ * @param {ModalRenderOptions} options
163
+ * @param {{ originSlideMenuWidth: number, collapseSlideMenuWidth: number }} widths
164
+ * @param {{ open: boolean }} [ops={ open: false }]
165
+ * @returns {string}
166
+ */
167
+ static getModalMenuLeftStyle(options, { originSlideMenuWidth, collapseSlideMenuWidth }, ops = { open: false }) {
168
+ return `${
169
+ options.mode === 'slide-menu-right'
170
+ ? `${
171
+ windowGetW() +
172
+ (ops?.open
173
+ ? -1 * originSlideMenuWidth +
174
+ (options.mode === 'slide-menu-right' && s(`.btn-icon-menu-mode-right`).classList.contains('hide')
175
+ ? originSlideMenuWidth - collapseSlideMenuWidth
176
+ : 0)
177
+ : originSlideMenuWidth)
178
+ }px`
179
+ : `-${ops?.open ? '0px' : originSlideMenuWidth}px`
180
+ }`;
181
+ }
182
+
183
+ /**
184
+ * Viewport-centered top/left CSS values for a modal of the given size.
185
+ * @param {{ width: number, height: number }} param0
186
+ * @returns {{ top: string, left: string }}
187
+ */
188
+ static getModalCenter({ width, height }) {
189
+ return {
190
+ top: `${windowGetH() / 2 - height / 2}px`,
191
+ left: `${windowGetW() / 2 - width / 2}px`,
192
+ };
193
+ }
194
+
126
195
  /**
127
196
  * Create or reload a modal. When the modal already exists in the DOM the
128
197
  * existing instance is reloaded via its onReloadModalListener callbacks.
@@ -175,52 +244,21 @@ class Modal {
175
244
  const heightDefaultTopBar = 50;
176
245
  const heightDefaultBottomBar = 0;
177
246
  const idModal = options.id ? options.id : getId(this.Data, 'modal-');
247
+ const { bus: eventBus, channels: eventChannels } = createModalEvents();
178
248
  this.Data[idModal] = {
179
249
  options,
180
- onCloseListener: {},
181
- onMenuListener: {},
182
- onCollapseMenuListener: {},
183
- onExtendMenuListener: {},
184
- onDragEndListener: {},
185
- onObserverListener: {},
186
- onClickListener: {},
187
- onExpandUiListener: {},
188
- onBarUiOpen: {},
189
- onBarUiClose: {},
190
- onReloadModalListener: {},
191
- onHome: {},
250
+ events: eventBus,
251
+ ...eventChannels,
192
252
  homeModals: options.homeModals ? options.homeModals : [],
193
253
  query: options.query ? `${window.location.search}` : undefined,
194
- getTop: () => {
195
- const result = windowGetH() - (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar);
196
- return result;
197
- },
198
- getHeight: () => {
199
- return (
200
- windowGetH() -
201
- (s(`.main-body-btn-ui-close`) && !s(`.main-body-btn-ui-close`).classList.contains('hide')
202
- ? (options.heightTopBar ? options.heightTopBar : heightDefaultTopBar) +
203
- (options.heightBottomBar ? options.heightBottomBar : heightDefaultBottomBar)
204
- : 0)
205
- );
206
- },
254
+ getTop: () => Modal.getModalTop(options, heightDefaultBottomBar),
255
+ getHeight: () => Modal.getModalHeight(options, heightDefaultTopBar, heightDefaultBottomBar),
207
256
  getMenuLeftStyle: (ops = { open: false }) =>
208
- `${
209
- options.mode === 'slide-menu-right'
210
- ? `${
211
- windowGetW() +
212
- (ops?.open
213
- ? -1 * originSlideMenuWidth +
214
- (options.mode === 'slide-menu-right' && s(`.btn-icon-menu-mode-right`).classList.contains('hide')
215
- ? originSlideMenuWidth - collapseSlideMenuWidth
216
- : 0)
217
- : originSlideMenuWidth)
218
- }px`
219
- : `-${ops?.open ? '0px' : originSlideMenuWidth}px`
220
- }`,
257
+ Modal.getModalMenuLeftStyle(options, { originSlideMenuWidth, collapseSlideMenuWidth }, ops),
221
258
  center: () => {
222
- top = `${windowGetH() / 2 - height / 2}px`;
223
- left = `${windowGetW() / 2 - width / 2}px`;
259
+ const { top: centeredTop, left: centeredLeft } = Modal.getModalCenter({ width, height });
260
+ top = centeredTop;
261
+ left = centeredLeft;
224
262
  },
225
263
  ...this.Data[idModal],
226
264
  };
@@ -1029,6 +1067,10 @@ class Modal {
1029
1067
  hoverFocusCtl.checkDismiss();
1030
1068
  };
1031
1069
  EventsUI.onClick(`.top-bar-search-box-container`, () => {
1070
+ if (SearchBox.Data.skipOpen) {
1071
+ SearchBox.Data.skipOpen = false;
1072
+ return;
1073
+ }
1032
1074
  searchBoxHistoryOpen();
1033
1075
  searchBoxCallBack(formDataInfoNode[0]);
1034
1076
  const inputEl = s(`.${inputSearchBoxId}`);
@@ -2138,7 +2180,6 @@ class Modal {
2138
2180
  dragInstance = setDragInstance();
2139
2181
  if (options && options.maximize) s(`.btn-maximize-${idModal}`).click();
2140
2182
  if (options.observer) {
2141
- this.Data[idModal].onObserverListener = {};
2142
2183
  this.Data[idModal].observerCallBack = () => {
2143
2184
  // logger.info('ResizeObserver', `.${idModal}`, s(`.${idModal}`).offsetWidth, s(`.${idModal}`).offsetHeight);
2144
2185
  if (this.Data[idModal] && this.Data[idModal].onObserverListener)
@@ -332,16 +332,10 @@ class PanelForm {
332
332
  }, 50);
333
333
  },
334
334
  initEdit: async function ({ data }) {
335
- // Clear file input when entering edit mode
336
- const fileFormData = formData.find((f) => f.inputType === 'file');
337
- if (fileFormData && s(`.${fileFormData.id}`)) {
338
- s(`.${fileFormData.id}`).value = '';
339
- s(`.${fileFormData.id}`).inputFiles = null;
340
- htmls(
341
- `.file-name-render-${fileFormData.id}`,
342
- `<div class="abs center"><i style="font-size: 25px" class="fa-solid fa-cloud"></i></div>`,
343
- );
344
- }
335
+ // Do NOT clear the file input here - the file should remain as-is when entering edit mode.
336
+ // If user wants to remove the file, they use the "clean file" button.
337
+ // If user wants to replace the file, they select a new file.
338
+ // Unconditionally clearing the file here would cause the server to receive fileId: null on save.
345
339
  setTimeout(() => {
346
340
  s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
347
341
  }, 50);
@@ -434,7 +428,17 @@ class PanelForm {
434
428
  for (const file of inputFiles) {
435
429
  indexFormDoc++;
436
430
  let fileId = undefined; // Reset for each iteration - only set if user uploaded a file
431
+ // Track whether the file input was explicitly cleared (null) vs never had a file (undefined)
432
+ // In edit mode, null means user cleared the file - we need to tell server to remove it
433
+ const isFileCleared = data.fileId === null && editId;
437
434
  await (async () => {
435
+ // When file is null, no markdown content, and not clearing a file, skip upload
436
+ if (!file && !isFileCleared && !hasMdContent) return;
437
+ // When user cleared file in edit mode, set fileId=null so server removes the reference
438
+ if (isFileCleared) {
439
+ fileId = null;
440
+ return;
441
+ }
438
442
  const body = new FormData();
439
443
  // Only append md file if it was created (has content)
440
444
  if (md) body.append('md', md);