@underpostnet/underpost 2.98.1 → 2.99.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.
Files changed (43) hide show
  1. package/README.md +2 -3
  2. package/bin/build.js +5 -5
  3. package/bin/deploy.js +10 -1
  4. package/cli.md +109 -110
  5. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  6. package/manifests/deployment/dd-test-development/deployment.yaml +4 -4
  7. package/package.json +1 -2
  8. package/src/api/user/user.router.js +7 -40
  9. package/src/cli/baremetal.js +67 -71
  10. package/src/cli/cloud-init.js +11 -12
  11. package/src/cli/cluster.js +22 -24
  12. package/src/cli/db.js +43 -50
  13. package/src/cli/deploy.js +162 -60
  14. package/src/cli/env.js +20 -5
  15. package/src/cli/fs.js +19 -21
  16. package/src/cli/index.js +35 -32
  17. package/src/cli/lxd.js +5 -5
  18. package/src/cli/monitor.js +66 -88
  19. package/src/cli/repository.js +7 -6
  20. package/src/cli/run.js +369 -261
  21. package/src/cli/secrets.js +3 -3
  22. package/src/cli/ssh.js +31 -32
  23. package/src/cli/static.js +1 -1
  24. package/src/cli/test.js +6 -7
  25. package/src/client/components/core/Content.js +42 -40
  26. package/src/client/components/core/FullScreen.js +202 -9
  27. package/src/client/components/core/PanelForm.js +4 -2
  28. package/src/client/components/core/VanillaJs.js +80 -29
  29. package/src/index.js +49 -32
  30. package/src/runtime/express/Express.js +7 -6
  31. package/src/server/auth.js +6 -1
  32. package/src/server/backup.js +11 -1
  33. package/src/server/conf.js +4 -4
  34. package/src/{cli → server}/cron.js +56 -29
  35. package/src/server/dns.js +39 -31
  36. package/src/server/peer.js +2 -2
  37. package/src/server/process.js +2 -2
  38. package/src/server/proxy.js +8 -7
  39. package/src/server/runtime.js +4 -7
  40. package/src/server/start.js +28 -15
  41. package/src/ws/IoServer.js +2 -3
  42. package/src/cli/script.js +0 -85
  43. package/src/monitor.js +0 -34
@@ -7,7 +7,7 @@
7
7
  import dotenv from 'dotenv';
8
8
  import { shellExec } from '../server/process.js';
9
9
  import fs from 'fs-extra';
10
- import UnderpostRootEnv from './env.js';
10
+ import Underpost from '../index.js';
11
11
 
12
12
  dotenv.config();
13
13
 
@@ -41,7 +41,7 @@ class UnderpostSecret {
41
41
  createFromEnvFile(envPath) {
42
42
  const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
43
43
  for (const key of Object.keys(envObj)) {
44
- UnderpostSecret.API.docker.set(key, envObj[key]);
44
+ Underpost.secret.docker.set(key, envObj[key]);
45
45
  }
46
46
  },
47
47
  set(key, value) {
@@ -61,7 +61,7 @@ class UnderpostSecret {
61
61
  createFromEnvFile(envPath) {
62
62
  const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
63
63
  for (const key of Object.keys(envObj)) {
64
- UnderpostRootEnv.API.set(key, envObj[key]);
64
+ Underpost.env.set(key, envObj[key]);
65
65
  }
66
66
  },
67
67
  },
package/src/cli/ssh.js CHANGED
@@ -5,11 +5,10 @@
5
5
  */
6
6
 
7
7
  import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
8
- import Dns from '../server/dns.js';
9
8
  import { pbcopy, shellExec } from '../server/process.js';
10
9
  import { loggerFactory } from '../server/logger.js';
11
10
  import fs from 'fs-extra';
12
- import UnderpostRootEnv from './env.js';
11
+ import Underpost from '../index.js';
13
12
 
14
13
  const logger = loggerFactory(import.meta);
15
14
 
@@ -252,17 +251,17 @@ EOF`);
252
251
 
253
252
  // Set defaults
254
253
  if (!options.user) options.user = 'root';
255
- if (!options.host) options.host = await Dns.getPublicIp();
254
+ if (!options.host) options.host = await Underpost.dns.getPublicIp();
256
255
  if (!options.password) options.password = options.disablePassword ? '' : generateRandomPasswordSelection(16);
257
256
  if (!options.groups) options.groups = 'wheel';
258
257
  if (!options.port) options.port = 22; // Handle connect uri
259
258
 
260
- const userHome = UnderpostSSH.API.getUserHome(options.user);
259
+ const userHome = Underpost.ssh.getUserHome(options.user);
261
260
  options.userHome = userHome;
262
261
 
263
262
  // Load config and override password and host if user exists in config
264
263
  if (options.deployId) {
265
- const config = UnderpostSSH.API.loadConfigNode(options.deployId);
264
+ const config = Underpost.ssh.loadConfigNode(options.deployId);
266
265
  confNode = config.confNode;
267
266
  confNodePath = config.confNodePath;
268
267
 
@@ -335,7 +334,7 @@ EOF`);
335
334
  // Remove the private key copy folder and update config only if deployId is provided
336
335
  if (options.deployId) {
337
336
  if (!confNode) {
338
- const config = UnderpostSSH.API.loadConfigNode(options.deployId);
337
+ const config = Underpost.ssh.loadConfigNode(options.deployId);
339
338
  confNode = config.confNode;
340
339
  confNodePath = config.confNodePath;
341
340
  }
@@ -347,7 +346,7 @@ EOF`);
347
346
  }
348
347
 
349
348
  delete confNode.users[options.user];
350
- UnderpostSSH.API.saveConfigNode(confNodePath, confNode);
349
+ Underpost.ssh.saveConfigNode(confNodePath, confNode);
351
350
  }
352
351
 
353
352
  logger.info(`User removed`);
@@ -362,7 +361,7 @@ EOF`);
362
361
  // If deployId is provided, check for existing config and backup keys
363
362
  if (options.deployId) {
364
363
  if (!confNode) {
365
- const config = UnderpostSSH.API.loadConfigNode(options.deployId);
364
+ const config = Underpost.ssh.loadConfigNode(options.deployId);
366
365
  confNode = config.confNode;
367
366
  confNodePath = config.confNodePath;
368
367
  }
@@ -378,14 +377,14 @@ EOF`);
378
377
  logger.info(`User ${options.user} already exists in config. Importing existing keys...`);
379
378
 
380
379
  // Create system user if it doesn't exist
381
- const userExists = UnderpostSSH.API.checkUserExists(options.user);
380
+ const userExists = Underpost.ssh.checkUserExists(options.user);
382
381
  if (!userExists) {
383
- UnderpostSSH.API.createSystemUser(options.user, options.password, options.groups);
382
+ Underpost.ssh.createSystemUser(options.user, options.password, options.groups);
384
383
  }
385
384
 
386
- const userHome = UnderpostSSH.API.getUserHome(options.user);
385
+ const userHome = Underpost.ssh.getUserHome(options.user);
387
386
  const sshDir = `${userHome}/.ssh`;
388
- UnderpostSSH.API.ensureSSHDirectory(sshDir);
387
+ Underpost.ssh.ensureSSHDirectory(sshDir);
389
388
 
390
389
  const userKeyPath = `${sshDir}/id_rsa`;
391
390
  const userPubKeyPath = `${sshDir}/id_rsa.pub`;
@@ -394,10 +393,10 @@ EOF`);
394
393
  fs.copyFileSync(privateKeyPath, userKeyPath);
395
394
  fs.copyFileSync(publicKeyPath, userPubKeyPath);
396
395
 
397
- UnderpostSSH.API.configureAuthorizedKeys(sshDir, userPubKeyPath, options.disablePassword);
398
- UnderpostSSH.API.configureSudoAccess(options.user, options.password, options.disablePassword);
399
- UnderpostSSH.API.configureKnownHosts(sshDir, options.port, options.host);
400
- UnderpostSSH.API.setSSHFilePermissions(sshDir, options.user, userKeyPath, userPubKeyPath);
396
+ Underpost.ssh.configureAuthorizedKeys(sshDir, userPubKeyPath, options.disablePassword);
397
+ Underpost.ssh.configureSudoAccess(options.user, options.password, options.disablePassword);
398
+ Underpost.ssh.configureKnownHosts(sshDir, options.port, options.host);
399
+ Underpost.ssh.setSSHFilePermissions(sshDir, options.user, userKeyPath, userPubKeyPath);
401
400
 
402
401
  logger.info(`Keys imported from ${privateCopyDir} to ${sshDir}`);
403
402
  logger.info(`User added with existing keys`);
@@ -406,11 +405,11 @@ EOF`);
406
405
  }
407
406
 
408
407
  // New user or no existing keys - create new user and generate keys
409
- UnderpostSSH.API.createSystemUser(options.user, options.password, options.groups);
408
+ Underpost.ssh.createSystemUser(options.user, options.password, options.groups);
410
409
 
411
- const userHome = UnderpostSSH.API.getUserHome(options.user);
410
+ const userHome = Underpost.ssh.getUserHome(options.user);
412
411
  const sshDir = `${userHome}/.ssh`;
413
- UnderpostSSH.API.ensureSSHDirectory(sshDir);
412
+ Underpost.ssh.ensureSSHDirectory(sshDir);
414
413
 
415
414
  const keyPath = `${sshDir}/id_rsa`;
416
415
  const pubKeyPath = `${sshDir}/id_rsa.pub`;
@@ -421,10 +420,10 @@ EOF`);
421
420
  );
422
421
  }
423
422
 
424
- UnderpostSSH.API.configureAuthorizedKeys(sshDir, pubKeyPath, options.disablePassword);
425
- UnderpostSSH.API.configureSudoAccess(options.user, options.password, options.disablePassword);
426
- UnderpostSSH.API.configureKnownHosts(sshDir, options.port, options.host);
427
- UnderpostSSH.API.setSSHFilePermissions(sshDir, options.user, keyPath, pubKeyPath);
423
+ Underpost.ssh.configureAuthorizedKeys(sshDir, pubKeyPath, options.disablePassword);
424
+ Underpost.ssh.configureSudoAccess(options.user, options.password, options.disablePassword);
425
+ Underpost.ssh.configureKnownHosts(sshDir, options.port, options.host);
426
+ Underpost.ssh.setSSHFilePermissions(sshDir, options.user, keyPath, pubKeyPath);
428
427
 
429
428
  // Save a copy of the keys to the private folder only if deployId is provided
430
429
  if (options.deployId) {
@@ -447,7 +446,7 @@ EOF`);
447
446
  privateKeyCopyPath,
448
447
  publicKeyCopyPath,
449
448
  };
450
- UnderpostSSH.API.saveConfigNode(confNodePath, confNode);
449
+ Underpost.ssh.saveConfigNode(confNodePath, confNode);
451
450
  }
452
451
 
453
452
  logger.info(`User added`);
@@ -457,7 +456,7 @@ EOF`);
457
456
  // Handle config user listing (only with deployId)
458
457
  if (options.deployId) {
459
458
  if (!confNode) {
460
- const config = UnderpostSSH.API.loadConfigNode(options.deployId);
459
+ const config = Underpost.ssh.loadConfigNode(options.deployId);
461
460
  confNode = config.confNode;
462
461
  confNodePath = config.confNodePath;
463
462
  }
@@ -472,7 +471,7 @@ EOF`);
472
471
 
473
472
  // Handle generate root keys
474
473
  if (options.generate)
475
- UnderpostSSH.API.generateKeys({ user: options.user, password: options.password, host: options.host });
474
+ Underpost.ssh.generateKeys({ user: options.user, password: options.password, host: options.host });
476
475
 
477
476
  // Handle list operations
478
477
  if (options.keysList) shellExec(`cat ${userHome}/.ssh/authorized_keys`);
@@ -489,8 +488,8 @@ EOF`);
489
488
 
490
489
  // Handle start server
491
490
  if (options.start) {
492
- UnderpostSSH.API.chmod({ user: options.user });
493
- UnderpostSSH.API.initService({ port: options.port });
491
+ Underpost.ssh.chmod({ user: options.user });
492
+ Underpost.ssh.initService({ port: options.port });
494
493
  }
495
494
 
496
495
  // Handle status server
@@ -512,10 +511,10 @@ EOF`);
512
511
  if (fs.existsSync(confNodePath)) {
513
512
  const { users } = JSON.parse(fs.readFileSync(confNodePath, 'utf8'));
514
513
  const { user, host, keyPath, port } = users[options.user];
515
- UnderpostRootEnv.API.set('DEFAULT_SSH_USER', user);
516
- UnderpostRootEnv.API.set('DEFAULT_SSH_HOST', host);
517
- UnderpostRootEnv.API.set('DEFAULT_SSH_KEY_PATH', keyPath);
518
- UnderpostRootEnv.API.set('DEFAULT_SSH_PORT', port);
514
+ Underpost.env.set('DEFAULT_SSH_USER', user);
515
+ Underpost.env.set('DEFAULT_SSH_HOST', host);
516
+ Underpost.env.set('DEFAULT_SSH_KEY_PATH', keyPath);
517
+ Underpost.env.set('DEFAULT_SSH_PORT', port);
519
518
  } else logger.warn(`No SSH config found at ${confNodePath}`);
520
519
  },
521
520
 
package/src/cli/static.js CHANGED
@@ -412,7 +412,7 @@ class UnderpostStatic {
412
412
  // Handle config template generation
413
413
  if (options.generateConfig) {
414
414
  const configPath = typeof options.generateConfig === 'string' ? options.generateConfig : './static-config.json';
415
- return UnderpostStatic.API.generateConfigTemplate(configPath);
415
+ return Underpost.static.generateConfigTemplate(configPath);
416
416
  }
417
417
 
418
418
  // Parse comma-separated options
package/src/cli/test.js CHANGED
@@ -9,8 +9,7 @@ import { MariaDB } from '../db/mariadb/MariaDB.js';
9
9
  import { getNpmRootPath } from '../server/conf.js';
10
10
  import { actionInitLog, loggerFactory, setUpInfo } from '../server/logger.js';
11
11
  import { pbcopy, shellExec } from '../server/process.js';
12
- import UnderpostDeploy from './deploy.js';
13
-
12
+ import Underpost from '../index.js';
14
13
  const logger = loggerFactory(import.meta);
15
14
 
16
15
  /**
@@ -67,10 +66,10 @@ class UnderpostTest {
67
66
  options.podStatus &&
68
67
  typeof options.podStatus === 'string'
69
68
  )
70
- return await UnderpostTest.API.statusMonitor(options.podName, options.podStatus, options.kindType);
69
+ return await Underpost.test.statusMonitor(options.podName, options.podStatus, options.kindType);
71
70
 
72
71
  if (options.sh === true || options.logs === true) {
73
- const [pod] = UnderpostDeploy.API.get(deployList);
72
+ const [pod] = Underpost.deploy.get(deployList);
74
73
  if (pod) {
75
74
  if (options.sh) return pbcopy(`sudo kubectl exec -it ${pod.NAME} -- sh`);
76
75
  if (options.logs) return shellExec(`sudo kubectl logs -f ${pod.NAME}`);
@@ -104,7 +103,7 @@ class UnderpostTest {
104
103
  break;
105
104
  }
106
105
  else {
107
- const pods = UnderpostDeploy.API.get(deployId);
106
+ const pods = Underpost.deploy.get(deployId);
108
107
  if (pods.length > 0)
109
108
  for (const deployData of pods) {
110
109
  const { NAME } = deployData;
@@ -115,7 +114,7 @@ class UnderpostTest {
115
114
  else logger.warn(`Couldn't find pods in deployment`, { deployId });
116
115
  }
117
116
  }
118
- } else return UnderpostTest.API.run();
117
+ } else return Underpost.test.run();
119
118
  },
120
119
  /**
121
120
  * @method statusMonitor
@@ -134,7 +133,7 @@ class UnderpostTest {
134
133
  logger.info(`Loading instance`, { podName, status, kindType, deltaMs, maxAttempts });
135
134
  const _monitor = async () => {
136
135
  await timer(deltaMs);
137
- const pods = UnderpostDeploy.API.get(podName, kindType);
136
+ const pods = Underpost.deploy.get(podName, kindType);
138
137
  let result = pods.find((p) => p.STATUS === status || (status === 'Running' && p.STATUS === 'Completed'));
139
138
  logger.info(
140
139
  `Testing pod ${podName}... ${result ? 1 : 0}/1 - elapsed time ${deltaMs * (index + 1)}s - attempt ${
@@ -8,50 +8,50 @@ import { DocumentService } from '../../services/document/document.service.js';
8
8
  import { CoreService, getApiBaseUrl, headersFactory } from '../../services/core/core.service.js';
9
9
  import { loggerFactory } from './Logger.js';
10
10
  import { imageShimmer, renderChessPattern, renderCssAttr, styleFactory } from './Css.js';
11
- import { getQueryParams } from './Router.js';
11
+ import { getQueryParams, setPath } from './Router.js';
12
12
 
13
13
  const logger = loggerFactory(import.meta);
14
14
 
15
15
  const attachMarkdownLinkHandlers = (containerSelector) => {
16
- const links = sa(`${containerSelector} a[href]`);
17
- links.forEach((link) => {
18
- link.addEventListener('click', async (e) => {
19
- e.preventDefault();
20
- const href = link.getAttribute('href');
21
-
22
- // Check if link is external
23
- const isExternal = href.startsWith('http://') || href.startsWith('https://');
24
-
25
- if (isExternal) {
26
- // Show warning modal for external links
27
- const result = await Modal.RenderConfirm({
28
- id: `external-link-${s4()}`,
29
- html: async () => html`
30
- <div class="in section-mp" style="text-align: center; padding: 20px;">
31
- <p>${Translate.Render('external-link-warning')}</p>
32
- <p style="word-break: break-all; margin-top: 10px;"><strong>${href}</strong></p>
33
- </div>
34
- `,
35
- icon: html`<i class="fas fa-external-link-alt"></i>`,
36
- style: {
37
- width: '350px',
38
- height: '500px',
39
- overflow: 'auto',
40
- 'z-index': '11',
41
- resize: 'none',
42
- },
43
- });
44
-
45
- // Only open link if user confirmed (not cancelled or closed)
46
- if (result && result.status === 'confirm') {
47
- window.open(href, '_blank', 'noopener,noreferrer');
48
- }
49
- // If cancelled, do nothing - don't navigate
50
- } else {
51
- // Internal link - navigate normally
52
- window.location.href = href;
16
+ const container = s(containerSelector);
17
+ if (!container || container.dataset.mdLinkHandler) return;
18
+ container.dataset.mdLinkHandler = 'true';
19
+
20
+ container.addEventListener('click', async (e) => {
21
+ const link = e.target.closest('.markdown-content a[href]');
22
+ if (!link) return;
23
+
24
+ const href = link.getAttribute('href');
25
+ if (!href || href.startsWith('#')) return;
26
+
27
+ e.preventDefault();
28
+ const isExternal = href.startsWith('http://') || href.startsWith('https://');
29
+
30
+ if (isExternal) {
31
+ const result = await Modal.RenderConfirm({
32
+ id: `external-link-${s4()}`,
33
+ html: async () => html`
34
+ <div class="in section-mp" style="text-align: center; padding: 20px;">
35
+ <p>${Translate.Render('external-link-warning')}</p>
36
+ <p style="word-break: break-all; margin-top: 10px;"><strong>${href}</strong></p>
37
+ </div>
38
+ `,
39
+ icon: html`<i class="fas fa-external-link-alt"></i>`,
40
+ style: {
41
+ width: '350px',
42
+ height: '500px',
43
+ overflow: 'auto',
44
+ 'z-index': '11',
45
+ resize: 'none',
46
+ },
47
+ });
48
+
49
+ if (result && result.status === 'confirm') {
50
+ window.open(href, '_blank', 'noopener,noreferrer');
53
51
  }
54
- });
52
+ } else {
53
+ setPath(href);
54
+ }
55
55
  });
56
56
  };
57
57
 
@@ -214,7 +214,9 @@ const Content = {
214
214
  case 'md':
215
215
  {
216
216
  const content = await Content.getFileContent(file, options);
217
- render += html`<div class="${options.class}" ${styleFactory(options.style)}>${marked.parse(content)}</div>`;
217
+ render += html`<div class="${options.class} markdown-content" ${styleFactory(options.style)}>
218
+ ${marked.parse(content)}
219
+ </div>`;
218
220
  }
219
221
  break;
220
222
 
@@ -1,31 +1,224 @@
1
+ /**
2
+ * Utility module for fullscreen mode management with cross-browser and PWA compatibility.
3
+ * Provides robust fullscreen detection, event handling, and UI synchronization.
4
+ * @module src/client/components/core/FullScreen.js
5
+ * @namespace FullScreenClient
6
+ */
7
+
1
8
  import { Responsive } from './Responsive.js';
2
9
  import { ToggleSwitch } from './ToggleSwitch.js';
3
10
  import { Translate } from './Translate.js';
4
11
  import { checkFullScreen, fullScreenIn, fullScreenOut, s } from './VanillaJs.js';
5
12
 
13
+ /**
14
+ * Manages fullscreen mode state, event handling, and UI synchronization.
15
+ * Supports all major browsers and PWA/Nativefier environments with comprehensive
16
+ * vendor-prefixed API detection.
17
+ * @memberof FullScreenClient
18
+ */
6
19
  const FullScreen = {
20
+ /**
21
+ * Internal state flag tracking the intended fullscreen mode.
22
+ * @type {boolean}
23
+ * @private
24
+ */
25
+ _fullScreenSwitch: false,
26
+
27
+ /**
28
+ * Flag indicating whether event listeners have been attached.
29
+ * Prevents duplicate event listener registration.
30
+ * @type {boolean}
31
+ * @private
32
+ */
33
+ _eventListenersAdded: false,
34
+
35
+ /**
36
+ * Flag preventing concurrent sync operations.
37
+ * Ensures state synchronization happens sequentially.
38
+ * @type {boolean}
39
+ * @private
40
+ */
41
+ _syncInProgress: false,
42
+
43
+ /**
44
+ * Checks if the browser is currently in fullscreen mode.
45
+ * Supports all vendor-prefixed fullscreen APIs for maximum compatibility:
46
+ * - Standard Fullscreen API
47
+ * - Webkit (Chrome, Safari, Opera)
48
+ * - Mozilla (Firefox)
49
+ * - Microsoft (IE/Edge)
50
+ * @private
51
+ * @memberof FullScreenClient.FullScreen
52
+ * @returns {boolean} True if currently in fullscreen mode, false otherwise.
53
+ */
54
+ _isFullScreen: function () {
55
+ return !!(
56
+ document.fullscreenElement ||
57
+ document.webkitFullscreenElement ||
58
+ document.mozFullScreenElement ||
59
+ document.msFullscreenElement ||
60
+ document.fullscreen ||
61
+ document.webkitIsFullScreen ||
62
+ document.mozFullScreen
63
+ );
64
+ },
65
+
66
+ /**
67
+ * Synchronizes the toggle switch UI state with the actual fullscreen state.
68
+ * Prevents race conditions using the _syncInProgress flag.
69
+ * Updates the toggle switch only if there's a mismatch between UI and actual state.
70
+ * @private
71
+ * @memberof FullScreenClient.FullScreen
72
+ * @returns {void}
73
+ */
74
+ _syncToggleState: function () {
75
+ if (this._syncInProgress) return;
76
+ this._syncInProgress = true;
77
+
78
+ const actualFullScreen = this._isFullScreen();
79
+
80
+ // Only update if there's a mismatch
81
+ if (this._fullScreenSwitch !== actualFullScreen) {
82
+ this._fullScreenSwitch = actualFullScreen;
83
+
84
+ // Update toggle switch UI if it exists
85
+ const toggle = s('.fullscreen');
86
+ if (toggle && ToggleSwitch.Tokens[`fullscreen`]) {
87
+ const switchState = ToggleSwitch.Tokens[`fullscreen`].checked;
88
+ if (switchState !== actualFullScreen) {
89
+ ToggleSwitch.Tokens[`fullscreen`].click();
90
+ }
91
+ }
92
+ }
93
+
94
+ setTimeout(() => {
95
+ this._syncInProgress = false;
96
+ }, 100);
97
+ },
98
+
99
+ /**
100
+ * Event handler for fullscreen change events.
101
+ * Triggers UI state synchronization when fullscreen mode changes.
102
+ * @private
103
+ * @memberof FullScreenClient.FullScreen
104
+ * @returns {void}
105
+ */
106
+ _handleFullScreenChange: function () {
107
+ this._syncToggleState();
108
+ },
109
+
110
+ /**
111
+ * Attaches all necessary event listeners for fullscreen mode detection.
112
+ * Handles multiple scenarios:
113
+ * - Vendor-prefixed fullscreen change events (Chrome, Firefox, IE/Edge)
114
+ * - Window resize events (for PWA/Nativefier compatibility)
115
+ * - ESC key detection (fallback for manual fullscreen exit)
116
+ * Only attaches listeners once to prevent duplicates.
117
+ * @private
118
+ * @memberof FullScreenClient.FullScreen
119
+ * @returns {void}
120
+ */
121
+ _addEventListeners: function () {
122
+ if (this._eventListenersAdded) return;
123
+
124
+ const events = ['fullscreenchange', 'webkitfullscreenchange', 'mozfullscreenchange', 'MSFullscreenChange'];
125
+
126
+ events.forEach((eventName) => {
127
+ document.addEventListener(eventName, () => this._handleFullScreenChange(), false);
128
+ });
129
+
130
+ // Additional check for PWA/Nativefier window resize events
131
+ window.addEventListener(
132
+ 'resize',
133
+ () => {
134
+ setTimeout(() => this._syncToggleState(), 150);
135
+ },
136
+ false,
137
+ );
138
+
139
+ // ESC key detection fallback
140
+ document.addEventListener(
141
+ 'keydown',
142
+ (e) => {
143
+ if (e.key === 'Escape' || e.keyCode === 27) {
144
+ setTimeout(() => this._syncToggleState(), 100);
145
+ }
146
+ },
147
+ false,
148
+ );
149
+
150
+ this._eventListenersAdded = true;
151
+ },
152
+
153
+ /**
154
+ * Enters fullscreen mode if not already in fullscreen.
155
+ * Updates internal state and triggers fullscreen API.
156
+ * Verifies the state change after a delay to ensure synchronization.
157
+ * @private
158
+ * @memberof FullScreenClient.FullScreen
159
+ * @returns {void}
160
+ */
161
+ _enterFullScreen: function () {
162
+ if (this._isFullScreen()) return;
163
+
164
+ this._fullScreenSwitch = true;
165
+ fullScreenIn();
166
+
167
+ // Verify after attempt
168
+ setTimeout(() => this._syncToggleState(), 300);
169
+ },
170
+
171
+ /**
172
+ * Exits fullscreen mode if currently in fullscreen.
173
+ * Updates internal state and triggers fullscreen exit API.
174
+ * Verifies the state change after a delay to ensure synchronization.
175
+ * @private
176
+ * @memberof FullScreenClient.FullScreen
177
+ * @returns {void}
178
+ */
179
+ _exitFullScreen: function () {
180
+ if (!this._isFullScreen()) return;
181
+
182
+ this._fullScreenSwitch = false;
183
+ fullScreenOut();
184
+
185
+ // Verify after attempt
186
+ setTimeout(() => this._syncToggleState(), 300);
187
+ },
188
+
189
+ /**
190
+ * Renders the fullscreen toggle setting UI component.
191
+ * Initializes fullscreen state detection, sets up event listeners,
192
+ * and creates a toggle switch for user interaction.
193
+ * Integrates with the Responsive system for dynamic updates.
194
+ * @memberof FullScreenClient.FullScreen
195
+ * @returns {Promise<string>} A promise resolving to the HTML string for the fullscreen setting component.
196
+ */
7
197
  RenderSetting: async function () {
8
- let fullScreenSwitch = checkFullScreen();
198
+ // Initialize state from actual fullscreen status
199
+ this._fullScreenSwitch = this._isFullScreen();
200
+
201
+ // Setup event listeners once
202
+ this._addEventListeners();
203
+
204
+ // Update responsive event
9
205
  Responsive.Event['full-screen-settings'] = () => {
10
- let fullScreenMode = checkFullScreen();
11
- if ((fullScreenSwitch && !fullScreenMode) || (!fullScreenSwitch && fullScreenMode))
12
- if (s('.fullscreen')) ToggleSwitch.Tokens[`fullscreen`].click();
206
+ this._syncToggleState();
13
207
  };
208
+
14
209
  return html`<div class="in section-mp">
15
210
  ${await ToggleSwitch.Render({
16
211
  wrapper: true,
17
212
  wrapperLabel: html`<i class="fa-solid fa-expand"></i> ${Translate.Render('fullscreen')}`,
18
213
  id: 'fullscreen',
19
214
  disabledOnClick: true,
20
- checked: fullScreenSwitch,
215
+ checked: this._fullScreenSwitch,
21
216
  on: {
22
217
  unchecked: () => {
23
- fullScreenSwitch = false;
24
- if (checkFullScreen()) fullScreenOut();
218
+ this._exitFullScreen();
25
219
  },
26
220
  checked: () => {
27
- fullScreenSwitch = true;
28
- if (!checkFullScreen()) fullScreenIn();
221
+ this._enterFullScreen();
29
222
  },
30
223
  },
31
224
  })}
@@ -455,7 +455,9 @@ const PanelForm = {
455
455
 
456
456
  const baseNewDoc = newInstance(data);
457
457
  baseNewDoc.tags = tags.filter((t) => !prefixTags.includes(t));
458
- baseNewDoc.mdFileId = hasMdContent ? marked.parse(data.mdFileId) : null;
458
+ baseNewDoc.mdFileId = hasMdContent
459
+ ? `<div class="markdown-content">${marked.parse(data.mdFileId)}</div>`
460
+ : null;
459
461
  baseNewDoc.userId = Elements.Data.user?.main?.model?.user?._id;
460
462
 
461
463
  // Ensure profileImageId is properly formatted as object with _id property
@@ -677,7 +679,7 @@ const PanelForm = {
677
679
  mdPlain = await blobArray[0].text();
678
680
  // Parse markdown with proper error handling
679
681
  try {
680
- parsedMarkdown = mdPlain ? marked.parse(mdPlain) : '';
682
+ parsedMarkdown = mdPlain ? `<div class="markdown-content">${marked.parse(mdPlain)}</div>` : '';
681
683
  } catch (parseError) {
682
684
  logger.error('Error parsing markdown for document:', documentObject._id, parseError);
683
685
  parsedMarkdown = `<p><strong>Error rendering markdown:</strong> ${parseError.message}</p>`;