underpost 3.2.4 → 3.2.8

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 (141) hide show
  1. package/.github/workflows/release.cd.yml +1 -2
  2. package/CHANGELOG.md +268 -1
  3. package/CLI-HELP.md +26 -13
  4. package/Dockerfile +0 -4
  5. package/README.md +3 -3
  6. package/bin/build.js +13 -3
  7. package/bin/deploy.js +570 -1
  8. package/bin/file.js +5 -0
  9. package/conf.js +11 -2
  10. package/jsconfig.json +1 -1
  11. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +2 -3
  12. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +2 -3
  13. package/manifests/deployment/dd-default-development/deployment.yaml +2 -6
  14. package/manifests/deployment/dd-test-development/deployment.yaml +136 -66
  15. package/manifests/deployment/dd-test-development/proxy.yaml +41 -5
  16. package/package.json +20 -11
  17. package/src/api/core/core.controller.js +10 -10
  18. package/src/api/core/core.service.js +10 -10
  19. package/src/api/default/default.controller.js +10 -10
  20. package/src/api/default/default.service.js +10 -10
  21. package/src/api/document/document.controller.js +12 -12
  22. package/src/api/document/document.model.js +10 -16
  23. package/src/api/file/file.controller.js +8 -8
  24. package/src/api/file/file.model.js +10 -10
  25. package/src/api/file/file.service.js +36 -36
  26. package/src/api/test/test.controller.js +8 -8
  27. package/src/api/test/test.service.js +8 -8
  28. package/src/api/user/guest.service.js +99 -0
  29. package/src/api/user/user.controller.js +6 -6
  30. package/src/api/user/user.model.js +8 -13
  31. package/src/api/user/user.service.js +3 -20
  32. package/src/cli/deploy.js +33 -30
  33. package/src/cli/fs.js +62 -5
  34. package/src/cli/image.js +43 -1
  35. package/src/cli/index.js +5 -1
  36. package/src/cli/release.js +58 -2
  37. package/src/cli/repository.js +35 -3
  38. package/src/cli/run.js +304 -38
  39. package/src/cli/ssh.js +1 -1
  40. package/src/cli/static.js +43 -115
  41. package/src/client/Default.index.js +21 -33
  42. package/src/client/components/core/404.js +4 -4
  43. package/src/client/components/core/500.js +4 -4
  44. package/src/client/components/core/Account.js +73 -60
  45. package/src/client/components/core/AgGrid.js +23 -33
  46. package/src/client/components/core/Alert.js +12 -13
  47. package/src/client/components/core/AppStore.js +1 -1
  48. package/src/client/components/core/Auth.js +20 -32
  49. package/src/client/components/core/Badge.js +7 -13
  50. package/src/client/components/core/BtnIcon.js +15 -17
  51. package/src/client/components/core/CalendarCore.js +42 -63
  52. package/src/client/components/core/Chat.js +13 -15
  53. package/src/client/components/core/ClientEvents.js +87 -0
  54. package/src/client/components/core/ColorPaletteElement.js +309 -0
  55. package/src/client/components/core/Content.js +17 -14
  56. package/src/client/components/core/Css.js +15 -71
  57. package/src/client/components/core/CssCore.js +12 -16
  58. package/src/client/components/core/D3Chart.js +4 -4
  59. package/src/client/components/core/Docs.js +60 -59
  60. package/src/client/components/core/DropDown.js +69 -91
  61. package/src/client/components/core/EventBus.js +92 -0
  62. package/src/client/components/core/EventsUI.js +14 -17
  63. package/src/client/components/core/FileExplorer.js +102 -234
  64. package/src/client/components/core/FullScreen.js +47 -75
  65. package/src/client/components/core/Input.js +24 -69
  66. package/src/client/components/core/Keyboard.js +25 -18
  67. package/src/client/components/core/KeyboardAvoidance.js +145 -0
  68. package/src/client/components/core/LoadingAnimation.js +25 -31
  69. package/src/client/components/core/LogIn.js +41 -41
  70. package/src/client/components/core/LogOut.js +23 -14
  71. package/src/client/components/core/Modal.js +397 -176
  72. package/src/client/components/core/NotificationManager.js +14 -18
  73. package/src/client/components/core/Panel.js +54 -50
  74. package/src/client/components/core/PanelForm.js +25 -125
  75. package/src/client/components/core/Polyhedron.js +110 -214
  76. package/src/client/components/core/PublicProfile.js +39 -32
  77. package/src/client/components/core/Recover.js +52 -48
  78. package/src/client/components/core/Responsive.js +88 -32
  79. package/src/client/components/core/RichText.js +9 -18
  80. package/src/client/components/core/Router.js +24 -3
  81. package/src/client/components/core/SearchBox.js +37 -37
  82. package/src/client/components/core/SignUp.js +39 -30
  83. package/src/client/components/core/SocketIo.js +31 -2
  84. package/src/client/components/core/SocketIoHandler.js +6 -6
  85. package/src/client/components/core/ToggleSwitch.js +8 -20
  86. package/src/client/components/core/ToolTip.js +5 -17
  87. package/src/client/components/core/Translate.js +56 -59
  88. package/src/client/components/core/Validator.js +26 -16
  89. package/src/client/components/core/Wallet.js +15 -26
  90. package/src/client/components/core/Worker.js +140 -25
  91. package/src/client/components/core/windowGetDimensions.js +7 -7
  92. package/src/client/components/default/{MenuDefault.js → AppShellDefault.js} +87 -87
  93. package/src/client/components/default/CssDefault.js +12 -12
  94. package/src/client/components/default/LogInDefault.js +6 -4
  95. package/src/client/components/default/LogOutDefault.js +6 -4
  96. package/src/client/components/default/RouterDefault.js +47 -0
  97. package/src/client/components/default/SettingsDefault.js +4 -4
  98. package/src/client/components/default/SignUpDefault.js +6 -4
  99. package/src/client/components/default/TranslateDefault.js +3 -3
  100. package/src/client/services/core/core.service.js +17 -49
  101. package/src/client/services/default/default.management.js +139 -242
  102. package/src/client/services/default/default.service.js +10 -16
  103. package/src/client/services/document/document.service.js +14 -19
  104. package/src/client/services/file/file.service.js +8 -13
  105. package/src/client/services/test/test.service.js +8 -13
  106. package/src/client/services/user/guest.service.js +79 -0
  107. package/src/client/services/user/user.management.js +5 -5
  108. package/src/client/services/user/user.service.js +14 -20
  109. package/src/client/ssr/body/404.js +3 -3
  110. package/src/client/ssr/body/500.js +3 -3
  111. package/src/client/ssr/body/CacheControl.js +5 -2
  112. package/src/client/ssr/body/DefaultSplashScreen.js +19 -12
  113. package/src/client/ssr/mailer/DefaultRecoverEmail.js +19 -20
  114. package/src/client/ssr/mailer/DefaultVerifyEmail.js +15 -16
  115. package/src/client/ssr/offline/Maintenance.js +12 -11
  116. package/src/client/ssr/offline/NoNetworkConnection.js +3 -3
  117. package/src/client/ssr/pages/Test.js +2 -2
  118. package/src/client/sw/core.sw.js +212 -0
  119. package/src/index.js +1 -1
  120. package/src/runtime/express/Dockerfile +4 -4
  121. package/src/runtime/lampp/Dockerfile +8 -7
  122. package/src/runtime/wp/Dockerfile +11 -17
  123. package/src/server/backup.js +1 -2
  124. package/src/server/client-build-docs.js +45 -46
  125. package/src/server/client-build.js +334 -60
  126. package/src/server/client-formatted.js +47 -16
  127. package/src/server/conf.js +29 -13
  128. package/src/server/cron.js +6 -8
  129. package/src/server/dns.js +2 -1
  130. package/src/server/ipfs-client.js +232 -91
  131. package/src/server/process.js +13 -27
  132. package/src/server/start.js +6 -3
  133. package/src/server/valkey.js +134 -235
  134. package/tsconfig.docs.json +15 -0
  135. package/typedoc.json +20 -0
  136. package/jsdoc.json +0 -52
  137. package/src/client/components/core/ColorPalette.js +0 -5267
  138. package/src/client/components/core/JoyStick.js +0 -80
  139. package/src/client/components/default/RoutesDefault.js +0 -49
  140. package/src/client/sw/default.sw.js +0 -127
  141. package/src/client/sw/template.sw.js +0 -84
@@ -19,7 +19,7 @@ const main = () => {
19
19
  es: 'Volver a <br> la pagina principal',
20
20
  },
21
21
  },
22
- Render: function (id) {
22
+ instance: function (id) {
23
23
  return this.Data[id][getLang()] ? this.Data[id][getLang()] : this.Data[id]['en'];
24
24
  },
25
25
  };
@@ -49,9 +49,9 @@ const main = () => {
49
49
  ${icon}
50
50
  <br />
51
51
  <br />
52
- <br />${Translate.Render('no-internet-connection')} <br />
52
+ <br />${Translate.instance('no-internet-connection')} <br />
53
53
  <br />
54
- <a target="_top" href="${location.origin}">${Translate.Render('back')}</a>
54
+ <a target="_top" href="${location.origin}">${Translate.instance('back')}</a>
55
55
  </div>`,
56
56
  );
57
57
  };
@@ -15,7 +15,7 @@ const main = () => {
15
15
  es: 'Volver a <br> la pagina principal',
16
16
  },
17
17
  },
18
- Render: function (id) {
18
+ instance: function (id) {
19
19
  return this.Data[id][getLang()] ? this.Data[id][getLang()] : this.Data[id]['en'];
20
20
  },
21
21
  };
@@ -182,7 +182,7 @@ const main = () => {
182
182
  <span class="bold">Test Page</span>
183
183
  <br />
184
184
  <br />
185
- <a target="_top" href="${location.origin}">${Translate.Render('back')}</a>
185
+ <a target="_top" href="${location.origin}">${Translate.instance('back')}</a>
186
186
  </div>`,
187
187
  );
188
188
  };
@@ -0,0 +1,212 @@
1
+ // Workbox modules are bundled inline by esbuild — no CDN, no importScripts.
2
+ import { setCacheNameDetails, clientsClaim } from 'workbox-core';
3
+ import { precacheAndRoute, cleanupOutdatedCaches, matchPrecache } from 'workbox-precaching';
4
+ import { registerRoute, setCatchHandler } from 'workbox-routing';
5
+ import { StaleWhileRevalidate, NetworkFirst, NetworkOnly } from 'workbox-strategies';
6
+ import { CacheableResponsePlugin } from 'workbox-cacheable-response';
7
+ import { ExpirationPlugin } from 'workbox-expiration';
8
+ import { BackgroundSyncPlugin } from 'workbox-background-sync';
9
+
10
+ // ─── Runtime config injected by client-build.js ───────────────────────────────
11
+ const CACHE_PREFIX = self.renderPayload?.CACHE_PREFIX || 'engine-core-v3';
12
+ const PRE_CACHED_RESOURCES = Array.isArray(self.renderPayload?.PRE_CACHED_RESOURCES)
13
+ ? self.renderPayload.PRE_CACHED_RESOURCES
14
+ : [];
15
+ const PROXY_PATH = self.renderPayload?.PROXY_PATH || '/';
16
+ const OFFLINE_PATH = self.renderPayload?.OFFLINE_PATH || '/offline';
17
+ const MAINTENANCE_PATH = self.renderPayload?.MAINTENANCE_PATH || '/maintenance';
18
+ const proxyBase = PROXY_PATH === '/' ? '' : PROXY_PATH;
19
+ const normalizeRoutePath = (candidatePath, fallbackPath) => {
20
+ const routePath = typeof candidatePath === 'string' && candidatePath.length > 0 ? candidatePath : fallbackPath;
21
+ const withLeadingSlash = routePath.startsWith('/') ? routePath : `/${routePath}`;
22
+ const withoutTrailingSlash = withLeadingSlash.replace(/\/+$/, '');
23
+ return withoutTrailingSlash.length > 0 ? withoutTrailingSlash : '/';
24
+ };
25
+ const offlinePath = normalizeRoutePath(OFFLINE_PATH, '/offline');
26
+ const maintenancePath = normalizeRoutePath(MAINTENANCE_PATH, '/maintenance');
27
+ const toRouteIndexUrl = (routePath) => `${proxyBase}${routePath === '/' ? '' : routePath}/index.html`;
28
+ const offlineUrl = toRouteIndexUrl(offlinePath);
29
+ const maintenanceUrl = toRouteIndexUrl(maintenancePath);
30
+
31
+ // Dedicated cache for fallback pages — populated independently from precacheAndRoute
32
+ // so offline/maintenance pages are always available even if the main precache install fails.
33
+ const FALLBACK_CACHE_NAME = `${CACHE_PREFIX}-fallbacks`;
34
+
35
+ const getFallbackResponse = async (preferMaintenance) => {
36
+ const cache = await caches.open(FALLBACK_CACHE_NAME);
37
+ if (preferMaintenance) {
38
+ return (
39
+ (await cache.match(maintenanceUrl)) ||
40
+ (await matchPrecache(maintenanceUrl)) ||
41
+ (await cache.match(offlineUrl)) ||
42
+ (await matchPrecache(offlineUrl)) ||
43
+ Response.error()
44
+ );
45
+ }
46
+ return (
47
+ (await cache.match(offlineUrl)) ||
48
+ (await matchPrecache(offlineUrl)) ||
49
+ (await cache.match(maintenanceUrl)) ||
50
+ (await matchPrecache(maintenanceUrl)) ||
51
+ Response.error()
52
+ );
53
+ };
54
+
55
+ // ─── Core setup ───────────────────────────────────────────────────────────────
56
+ setCacheNameDetails({ prefix: CACHE_PREFIX });
57
+ clientsClaim();
58
+
59
+ self.addEventListener('install', (event) => {
60
+ self.skipWaiting();
61
+ // Cache fallback pages in a dedicated cache so they are available even if
62
+ // precacheAndRoute fails (e.g. some asset in the manifest returns non-200).
63
+ event.waitUntil(
64
+ caches.open(FALLBACK_CACHE_NAME).then((cache) =>
65
+ // Try together first; fall back to individual adds so a single failure
66
+ // does not prevent the other page from being cached.
67
+ cache
68
+ .addAll([offlineUrl, maintenanceUrl])
69
+ .catch(() => Promise.all([cache.add(offlineUrl).catch(() => {}), cache.add(maintenanceUrl).catch(() => {})])),
70
+ ),
71
+ );
72
+ });
73
+
74
+ self.addEventListener('activate', (event) => {
75
+ event.waitUntil(
76
+ (async () => {
77
+ if ('navigationPreload' in self.registration) {
78
+ await self.registration.navigationPreload.enable();
79
+ }
80
+ })(),
81
+ );
82
+ });
83
+
84
+ self.addEventListener('message', (event) => {
85
+ const payload = event.data || {};
86
+
87
+ if (payload.status === 'skipWaiting') {
88
+ self.skipWaiting();
89
+ return;
90
+ }
91
+
92
+ if (payload.status === 'workbox-reset') {
93
+ event.waitUntil(
94
+ (async () => {
95
+ const cacheNames = await caches.keys();
96
+ await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName)));
97
+ if (event.ports && event.ports[0]) {
98
+ event.ports[0].postMessage({ status: 'workbox-reset-done', deleted: cacheNames.length });
99
+ }
100
+ })(),
101
+ );
102
+ }
103
+ });
104
+
105
+ // ─── Precaching ───────────────────────────────────────────────────────────────
106
+ precacheAndRoute(PRE_CACHED_RESOURCES.map((url) => ({ url, revision: null })));
107
+ cleanupOutdatedCaches();
108
+
109
+ // ─── Static assets: StaleWhileRevalidate ─────────────────────────────────────
110
+ registerRoute(
111
+ ({ request, url }) =>
112
+ request.method === 'GET' &&
113
+ url.origin === self.location.origin &&
114
+ ['style', 'script', 'font', 'image'].includes(request.destination),
115
+ new StaleWhileRevalidate({
116
+ cacheName: `${CACHE_PREFIX}-assets`,
117
+ plugins: [
118
+ new CacheableResponsePlugin({
119
+ statuses: [0, 200],
120
+ }),
121
+ new ExpirationPlugin({
122
+ maxEntries: 350,
123
+ maxAgeSeconds: 30 * 24 * 60 * 60, // 30 days
124
+ }),
125
+ ],
126
+ }),
127
+ );
128
+
129
+ // ─── API GET: NetworkFirst with short timeout ─────────────────────────────────
130
+ registerRoute(
131
+ ({ request, url }) => request.method === 'GET' && url.pathname.includes('/api/'),
132
+ new NetworkFirst({
133
+ cacheName: `${CACHE_PREFIX}-api-get`,
134
+ networkTimeoutSeconds: 5,
135
+ plugins: [
136
+ new CacheableResponsePlugin({
137
+ statuses: [0, 200],
138
+ }),
139
+ new ExpirationPlugin({
140
+ maxEntries: 120,
141
+ maxAgeSeconds: 5 * 60, // 5 minutes
142
+ }),
143
+ ],
144
+ }),
145
+ );
146
+
147
+ // ─── API mutations: NetworkOnly + background sync replay ─────────────────────
148
+ registerRoute(
149
+ ({ request, url }) => request.method !== 'GET' && url.pathname.includes('/api/'),
150
+ new NetworkOnly({
151
+ plugins: [
152
+ new BackgroundSyncPlugin('api-mutation-queue', {
153
+ maxRetentionTime: 24 * 60, // 24 hours
154
+ }),
155
+ ],
156
+ }),
157
+ );
158
+
159
+ // ─── Navigation: NetworkFirst with offline fallback ───────────────────────────
160
+ registerRoute(
161
+ ({ request }) => request.mode === 'navigate',
162
+ async ({ event }) => {
163
+ const navigationStrategy = new NetworkFirst({
164
+ cacheName: `${CACHE_PREFIX}-pages`,
165
+ networkTimeoutSeconds: 4,
166
+ plugins: [
167
+ new CacheableResponsePlugin({
168
+ statuses: [0, 200],
169
+ }),
170
+ new ExpirationPlugin({
171
+ maxEntries: 60,
172
+ maxAgeSeconds: 12 * 60 * 60, // 12 hours
173
+ }),
174
+ ],
175
+ });
176
+
177
+ // Distinguish server-down (online but unreachable) from no-network (offline).
178
+ // navigator.onLine is false only when the device has no network at all.
179
+ const isOnline = () => typeof navigator !== 'undefined' && navigator.onLine !== false;
180
+
181
+ try {
182
+ const preload = await event.preloadResponse;
183
+ if (preload) {
184
+ if (preload.status >= 500) {
185
+ return getFallbackResponse(true);
186
+ }
187
+ return preload;
188
+ }
189
+
190
+ const networkResponse = await navigationStrategy.handle({ event, request: event.request });
191
+ if (networkResponse && networkResponse.status >= 500) {
192
+ return getFallbackResponse(true);
193
+ }
194
+ return networkResponse;
195
+ } catch (_) {
196
+ // If device reports it has network but the request failed, it means the
197
+ // server is unreachable/down → show maintenance. True offline → show offline.
198
+ return getFallbackResponse(isOnline());
199
+ }
200
+ },
201
+ );
202
+
203
+ // ─── Global catch handler ─────────────────────────────────────────────────────
204
+ setCatchHandler(async ({ request }) => {
205
+ if (request.mode === 'navigate') {
206
+ return getFallbackResponse(typeof navigator !== 'undefined' && navigator.onLine !== false);
207
+ }
208
+ return new Response(JSON.stringify({ status: 'error', message: 'request failed' }), {
209
+ status: 503,
210
+ headers: { 'Content-Type': 'application/json' },
211
+ });
212
+ });
package/src/index.js CHANGED
@@ -44,7 +44,7 @@ class Underpost {
44
44
  * @type {String}
45
45
  * @memberof Underpost
46
46
  */
47
- static version = 'v3.2.4';
47
+ static version = 'v3.2.8';
48
48
 
49
49
  /**
50
50
  * Required Node.js major version
@@ -30,11 +30,11 @@ RUN dnf clean all
30
30
  RUN node --version
31
31
  RUN npm --version
32
32
 
33
- # Create non-root user for secure container execution (cron jobs, init containers)
34
- # Deployment containers override to root via securityContext when npm install -g is needed
35
- RUN useradd -m -u 1000 -s /bin/bash dd
33
+ # Install underpost ci/cd cli
34
+ RUN npm install -g underpost
35
+ RUN underpost --version
36
36
 
37
- # Set working directory
37
+ # Create working directory
38
38
  WORKDIR /home/dd
39
39
 
40
40
  # Expose necessary ports
@@ -1,6 +1,6 @@
1
1
  FROM rockylinux:9
2
2
 
3
- # Update and install required packages
3
+ # System packages
4
4
  RUN dnf -y update && \
5
5
  dnf -y install epel-release && \
6
6
  dnf -y install --allowerasing \
@@ -20,13 +20,14 @@ RUN dnf -y update && \
20
20
  perl && \
21
21
  dnf clean all
22
22
 
23
- # --- Download and install XAMPP (PHP 8.2)
23
+
24
+ # Download and install XAMPP (PHP 8.2)
24
25
  RUN curl -L -o /tmp/xampp-linux-installer.run "https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/8.2.12/xampp-linux-x64-8.2.12-0-installer.run" && \
25
26
  chmod +x /tmp/xampp-linux-installer.run && \
26
27
  bash -c "/tmp/xampp-linux-installer.run --mode unattended" && \
27
28
  ln -sf /opt/lampp/lampp /usr/bin/lampp
28
29
 
29
- # --- Create /xampp/htdocs (static root) and set permissions
30
+ # Create /xampp/htdocs (static root) and set permissions
30
31
  RUN mkdir -p /opt/lampp/htdocs && \
31
32
  chown -R root:root /opt/lampp/htdocs && \
32
33
  chmod -R a+rX /opt/lampp/htdocs
@@ -47,11 +48,11 @@ RUN dnf clean all
47
48
  RUN node --version
48
49
  RUN npm --version
49
50
 
50
- # Create non-root user for secure container execution (cron jobs, init containers)
51
- # Deployment containers override to root via securityContext when npm install -g is needed
52
- RUN useradd -m -u 1000 -s /bin/bash dd
51
+ # Install underpost CLI
52
+ RUN npm install -g underpost
53
+ RUN underpost --version
53
54
 
54
- # Set working directory
55
+ # Runtime root expected by startup/build scripts.
55
56
  WORKDIR /home/dd
56
57
 
57
58
  EXPOSE 22
@@ -1,13 +1,12 @@
1
1
  FROM rockylinux:9
2
2
 
3
- # Update and install required packages
3
+ # System packages
4
4
  RUN dnf -y update && \
5
5
  dnf -y install epel-release && \
6
6
  dnf -y install --allowerasing \
7
7
  bzip2 \
8
8
  sudo \
9
9
  curl \
10
- unzip \
11
10
  net-tools \
12
11
  openssh-server \
13
12
  nano \
@@ -21,13 +20,13 @@ RUN dnf -y update && \
21
20
  perl && \
22
21
  dnf clean all
23
22
 
24
- # --- Download and install XAMPP (PHP 8.2)
23
+ # Download and install XAMPP (PHP 8.2)
25
24
  RUN curl -L -o /tmp/xampp-linux-installer.run "https://sourceforge.net/projects/xampp/files/XAMPP%20Linux/8.2.12/xampp-linux-x64-8.2.12-0-installer.run" && \
26
25
  chmod +x /tmp/xampp-linux-installer.run && \
27
26
  bash -c "/tmp/xampp-linux-installer.run --mode unattended" && \
28
27
  ln -sf /opt/lampp/lampp /usr/bin/lampp
29
28
 
30
- # --- Create /xampp/htdocs (static root) and set permissions
29
+ # Create /xampp/htdocs (static root) and set permissions
31
30
  RUN mkdir -p /opt/lampp/htdocs && \
32
31
  chown -R root:root /opt/lampp/htdocs && \
33
32
  chmod -R a+rX /opt/lampp/htdocs
@@ -39,25 +38,20 @@ RUN echo 'export PATH="/opt/lampp/bin:/usr/local/bin:${PATH}"' > /etc/profile.d/
39
38
  # Provide a no-op sendmail so WP plugins don't error on mail calls
40
39
  RUN printf '#!/bin/sh\ncat > /dev/null\n' > /usr/sbin/sendmail && chmod +x /usr/sbin/sendmail
41
40
 
41
+ # Install Node.js (includes npm)
42
+ RUN curl -fsSL https://rpm.nodesource.com/setup_24.x | bash - && \
43
+ dnf install -y nodejs && \
44
+ dnf clean all
45
+
42
46
  # Install WP-CLI
43
47
  RUN curl -sL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -o /usr/local/bin/wp && \
44
48
  chmod +x /usr/local/bin/wp && \
45
49
  wp --info --allow-root
46
50
 
47
- # Install Node.js
48
- RUN curl -fsSL https://rpm.nodesource.com/setup_24.x | bash -
49
- RUN dnf install nodejs -y
50
- RUN dnf clean all
51
-
52
- # Verify Node.js and npm versions
53
- RUN node --version
54
- RUN npm --version
55
-
56
- # Create non-root user for secure container execution (cron jobs, init containers)
57
- # Deployment containers override to root via securityContext when npm install -g is needed
58
- RUN useradd -m -u 1000 -s /bin/bash dd
51
+ # Install underpost CLI
52
+ RUN npm install -g underpost
59
53
 
60
- # Set working directory
54
+ # Runtime root expected by startup/build scripts.
61
55
  WORKDIR /home/dd
62
56
 
63
57
  EXPOSE 22
@@ -50,8 +50,7 @@ class BackUp {
50
50
  for (const _deployId of deployList.split(',')) {
51
51
  const deployId = _deployId.trim();
52
52
  if (!deployId) continue;
53
-
54
- const dbCommand = `node bin db ${options.git ? '--git --force-clone ' : ''}--export --primary-pod${clusterFlag} ${deployId}`;
53
+ const dbCommand = `node bin db ${options.git ? '--git --force-clone ' : ''}--export --primary-pod --preserveUUID${clusterFlag} ${deployId}`;
55
54
  const repoCommand = `node bin db --repo-backup${clusterFlag} ${deployId}`;
56
55
 
57
56
  // Pass GITHUB_TOKEN and GITHUB_USERNAME ephemerally through the SSH command
@@ -324,59 +324,56 @@ const buildApiDocs = async ({
324
324
  };
325
325
 
326
326
  /**
327
- * Builds JSDoc documentation
327
+ * Builds API documentation using TypeDoc (generates a modern static site from JSDoc-annotated JS).
328
+ * Config is read from the base typedoc JSON, merged with runtime values, written to a temporary
329
+ * file, and deleted after the build — the base config file is never mutated on disk.
328
330
  * @function buildJsDocs
329
331
  * @memberof clientBuildDocs
330
- * @param {Object} options - JSDoc build options
332
+ * @param {Object} options - TypeDoc build options
331
333
  * @param {string} options.host - The hostname for the documentation
332
334
  * @param {string} options.path - The base path for the documentation
333
335
  * @param {Object} options.metadata - Metadata for the documentation
334
- * @param {string} options.publicClientId - Client ID used to resolve the tutorials/references directory
336
+ * @param {string} options.publicClientId - Client ID used to resolve the references directory
335
337
  * @param {Object} options.docs - Documentation config from server conf
336
- * @param {string} options.docs.jsJsonPath - Path to the JSDoc JSON config file
338
+ * @param {string} options.docs.jsJsonPath - Path to the base typedoc JSON config file
339
+ * @param {string} options.docsDestination - Resolved output path for the generated docs
337
340
  */
338
- const buildJsDocs = async ({ host, path, metadata = {}, publicClientId, docs }) => {
341
+ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId, docs, docsDestination }) => {
339
342
  const logger = loggerFactory(import.meta);
340
343
 
341
- const jsDocSourcePath = docs.jsJsonPath;
342
- if (!fs.existsSync(jsDocSourcePath)) {
343
- logger.warn('jsdoc config not found, skipping', jsDocSourcePath);
344
+ const typedocConfigPath = docs.jsJsonPath;
345
+ if (!fs.existsSync(typedocConfigPath)) {
346
+ logger.warn('typedoc config not found, skipping', typedocConfigPath);
344
347
  return;
345
348
  }
346
- const jsDocsConfig = JSON.parse(fs.readFileSync(jsDocSourcePath, 'utf8'));
347
- logger.info('using jsdoc config', jsDocSourcePath);
349
+ const baseConfig = JSON.parse(fs.readFileSync(typedocConfigPath, 'utf8'));
350
+ logger.info('using typedoc config', typedocConfigPath);
348
351
 
349
- jsDocsConfig.opts.destination = `./public/${host}${path === '/' ? path : `${path}/`}docs/`;
350
- jsDocsConfig.opts.theme_opts.title = metadata?.title ? metadata.title : undefined;
351
- jsDocsConfig.opts.theme_opts.favicon = `./public/${host}${path === '/' ? '/' : `${path}/`}favicon.ico`;
352
-
353
- const tutorialsPath = `./src/client/public/${publicClientId}/docs/references`;
352
+ // Build runtime config in memory never mutate the base config file
353
+ // tsconfig must be absolute so TypeDoc resolves it regardless of where the
354
+ // tmp config file is located on disk.
355
+ const runtimeConfig = {
356
+ ...baseConfig,
357
+ tsconfig: fs.realpathSync(baseConfig.tsconfig || './tsconfig.docs.json'),
358
+ out: docsDestination,
359
+ name: metadata?.title || baseConfig.name,
360
+ favicon: `./public/${host}${path === '/' ? '/' : `${path}/`}favicon.ico`,
361
+ };
354
362
 
363
+ // Include extra reference documents as TypeDoc document pages
364
+ // TypeDoc 0.28+: option is `projectDocuments`, not `documents`
355
365
  if (Array.isArray(docs.references) && docs.references.length > 0) {
356
- fs.mkdirSync(tutorialsPath, { recursive: true });
357
- for (const refPath of docs.references) {
358
- if (fs.existsSync(refPath)) {
359
- const fileName = refPath.split('/').pop();
360
- fs.copySync(refPath, `${tutorialsPath}/${fileName}`);
361
- logger.info('copied reference to tutorials', refPath);
362
- }
363
- }
366
+ runtimeConfig.projectDocuments = docs.references.filter((p) => fs.existsSync(p));
367
+ if (runtimeConfig.projectDocuments.length > 0) logger.info('typedoc documents', runtimeConfig.projectDocuments);
364
368
  }
365
369
 
366
- if (fs.existsSync(tutorialsPath) && fs.readdirSync(tutorialsPath).length > 0) {
367
- jsDocsConfig.opts.tutorials = tutorialsPath;
368
- if (jsDocsConfig.opts.theme_opts.sections && !jsDocsConfig.opts.theme_opts.sections.includes('Tutorials')) {
369
- jsDocsConfig.opts.theme_opts.sections.push('Tutorials');
370
- }
371
- logger.info('build jsdoc tutorials', tutorialsPath);
372
- } else {
373
- delete jsDocsConfig.opts.tutorials;
374
- }
370
+ const tmpConfigPath = `.typedoc.tmp.json`;
371
+ fs.writeFileSync(tmpConfigPath, JSON.stringify(runtimeConfig, null, 2), 'utf8');
372
+ logger.warn('build typedoc view', docsDestination);
375
373
 
376
- fs.writeFileSync(jsDocSourcePath, JSON.stringify(jsDocsConfig, null, 4), 'utf8');
377
- logger.warn('build jsdoc view', jsDocsConfig.opts.destination);
374
+ shellExec(`node_modules/.bin/typedoc --options ${tmpConfigPath}`, { silent: true });
378
375
 
379
- shellExec(`npx jsdoc -c ${jsDocSourcePath}`, { silent: true });
376
+ fs.removeSync(tmpConfigPath);
380
377
  };
381
378
 
382
379
  /**
@@ -384,17 +381,13 @@ const buildJsDocs = async ({ host, path, metadata = {}, publicClientId, docs })
384
381
  * @function buildCoverage
385
382
  * @memberof clientBuildDocs
386
383
  * @param {Object} options - Coverage build options
387
- * @param {string} options.host - The hostname for the coverage
388
- * @param {string} options.path - The base path for the coverage
389
384
  * @param {Object} options.docs - Documentation config from server conf
390
- * @param {string} options.docs.coveragePath - Directory where to run npm run coverage
385
+ * @param {string} options.docs.coveragePath - Directory where coverage reports are generated
386
+ * @param {string} options.docsDestination - Resolved output path where docs were built
391
387
  */
392
- const buildCoverage = async ({ host, path, docs }) => {
388
+ const buildCoverage = async ({ docs, docsDestination }) => {
393
389
  const logger = loggerFactory(import.meta);
394
- const jsDocSourcePath = docs.jsJsonPath;
395
- const jsDocsConfig = JSON.parse(fs.readFileSync(jsDocSourcePath, 'utf8'));
396
- const coveragePath = docs.coveragePath;
397
- const coverageOutputDir = docs.coverageOutputDir || 'coverage';
390
+ const { coveragePath, coverageOutputDir = 'coverage' } = docs;
398
391
 
399
392
  const coverageOutputPath = `${coveragePath}/coverage`;
400
393
  if (!fs.existsSync(coverageOutputPath)) {
@@ -412,7 +405,7 @@ const buildCoverage = async ({ host, path, docs }) => {
412
405
  }
413
406
 
414
407
  if (fs.existsSync(coverageOutputPath) && fs.readdirSync(coverageOutputPath).length > 0) {
415
- const coverageBuildPath = `${jsDocsConfig.opts.destination}${coverageOutputDir}`;
408
+ const coverageBuildPath = `${docsDestination}${coverageOutputDir}`;
416
409
  fs.mkdirSync(coverageBuildPath, { recursive: true });
417
410
  // Hardhat 3 outputs HTML to coverage/html/; Hardhat 2 / c8 output directly to coverage/
418
411
  const coverageHtmlSubdir = `${coverageOutputPath}/html`;
@@ -453,8 +446,14 @@ const buildDocs = async ({
453
446
  packageData,
454
447
  docs,
455
448
  }) => {
456
- await buildJsDocs({ host, path, metadata, publicClientId, docs });
457
- await buildCoverage({ host, path, docs });
449
+ const pathPrefix = path === '/' ? '/' : `${path}/`;
450
+ // TypeDoc output is versioned: served at /docs/engine/{version}/
451
+ const version = (packageData?.version || '').replace(/^v/, '');
452
+ const jsDocsDestination = `./public/${host}${pathPrefix}docs/engine/${version}/`;
453
+ // Coverage output at /docs/coverage/ (or /docs/{coverageOutputDir}/)
454
+ const coverageBaseDestination = `./public/${host}${pathPrefix}docs/`;
455
+ await buildJsDocs({ host, path, metadata, publicClientId, docs, docsDestination: jsDocsDestination });
456
+ await buildCoverage({ docs, docsDestination: coverageBaseDestination });
458
457
  await buildApiDocs({
459
458
  host,
460
459
  path,