underpost 2.85.7 → 2.89.1

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 (38) hide show
  1. package/.github/workflows/release.cd.yml +3 -1
  2. package/README.md +2 -2
  3. package/bin/build.js +8 -10
  4. package/bin/index.js +8 -1
  5. package/cli.md +3 -2
  6. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  7. package/manifests/deployment/dd-test-development/deployment.yaml +50 -50
  8. package/manifests/deployment/dd-test-development/proxy.yaml +4 -4
  9. package/package.json +1 -1
  10. package/scripts/rpmfusion-ffmpeg-setup.sh +55 -0
  11. package/src/api/file/file.service.js +29 -3
  12. package/src/cli/baremetal.js +1 -2
  13. package/src/cli/index.js +1 -0
  14. package/src/cli/repository.js +8 -1
  15. package/src/cli/run.js +104 -36
  16. package/src/client/components/core/AgGrid.js +42 -3
  17. package/src/client/components/core/CommonJs.js +4 -0
  18. package/src/client/components/core/Css.js +95 -48
  19. package/src/client/components/core/CssCore.js +0 -1
  20. package/src/client/components/core/LoadingAnimation.js +2 -2
  21. package/src/client/components/core/Logger.js +2 -9
  22. package/src/client/components/core/Modal.js +22 -14
  23. package/src/client/components/core/ObjectLayerEngine.js +300 -9
  24. package/src/client/components/core/ObjectLayerEngineModal.js +686 -148
  25. package/src/client/components/core/ObjectLayerEngineViewer.js +1061 -0
  26. package/src/client/components/core/Pagination.js +57 -12
  27. package/src/client/components/core/Router.js +37 -1
  28. package/src/client/components/core/Translate.js +4 -0
  29. package/src/client/components/core/Worker.js +8 -1
  30. package/src/client/services/default/default.management.js +86 -16
  31. package/src/db/mariadb/MariaDB.js +2 -2
  32. package/src/index.js +1 -1
  33. package/src/server/client-build.js +57 -2
  34. package/src/server/object-layer.js +44 -0
  35. package/src/server/start.js +12 -0
  36. package/src/ws/IoInterface.js +2 -3
  37. package/AUTHORS.md +0 -21
  38. package/src/server/network.js +0 -72
@@ -1,21 +1,24 @@
1
- import { getQueryParams, setQueryParams } from './Router.js';
1
+ import { getQueryParams, setQueryParams, listenQueryParamsChange } from './Router.js';
2
2
 
3
3
  class AgPagination extends HTMLElement {
4
4
  constructor() {
5
5
  super();
6
6
  this.attachShadow({ mode: 'open' });
7
7
  this._gridId = null;
8
- const queryParams = getQueryParams();
8
+ let queryParams = getQueryParams();
9
9
  this._currentPage = parseInt(queryParams.page, 10) || 1;
10
10
  this._limit = parseInt(queryParams.limit, 10) || 10;
11
11
  this._totalPages = 1;
12
12
  this._totalItems = 0;
13
+ this._limitOptions = [10, 20, 50, 100]; // Default options
13
14
  this.handlePageChange = this.handlePageChange.bind(this);
14
15
  this.handleLimitChange = this.handleLimitChange.bind(this);
16
+
17
+ this.handleQueryParamsChange = this.handleQueryParamsChange.bind(this);
15
18
  }
16
19
 
17
20
  static get observedAttributes() {
18
- return ['grid-id', 'current-page', 'total-pages', 'total-items', 'limit'];
21
+ return ['grid-id', 'current-page', 'total-pages', 'total-items', 'limit', 'limit-options'];
19
22
  }
20
23
 
21
24
  attributeChangedCallback(name, oldValue, newValue) {
@@ -35,6 +38,16 @@ class AgPagination extends HTMLElement {
35
38
  case 'limit':
36
39
  this._limit = parseInt(newValue, 10) || this._limit;
37
40
  break;
41
+ case 'limit-options':
42
+ try {
43
+ const parsed = JSON.parse(newValue);
44
+ if (Array.isArray(parsed) && parsed.length > 0) {
45
+ this._limitOptions = parsed.map((v) => parseInt(v, 10)).filter((v) => !isNaN(v) && v > 0);
46
+ }
47
+ } catch (e) {
48
+ console.warn('Invalid limit-options format, using defaults');
49
+ }
50
+ break;
38
51
  }
39
52
  this.update();
40
53
  }
@@ -42,11 +55,43 @@ class AgPagination extends HTMLElement {
42
55
  connectedCallback() {
43
56
  this.render();
44
57
  this.addEventListeners();
45
- this.update();
58
+ // Subscribe to query parameter changes once the component is connected and _gridId is potentially set.
59
+ listenQueryParamsChange({
60
+ id: `ag-pagination-${this._gridId || 'default'}`, // Use _gridId for a unique listener ID
61
+ event: this.handleQueryParamsChange,
62
+ });
63
+ // The initial state is already set in the constructor from getQueryParams().
64
+ // The `listenQueryParamsChange` in `Router.js` will trigger an immediate (setTimeout) call
65
+ // to `handleQueryParamsChange`, which will re-sync the state if needed and update the component.
66
+ this.update(); // Keep the initial update for rendering based on constructor's initial state.
46
67
  }
47
68
 
48
69
  disconnectedCallback() {
49
- // Event listeners on shadow DOM are garbage collected with the component
70
+ // If Router.js held strong references to queryParamsChangeListeners, you might need to unsubscribe here.
71
+ // However, for this example, we'll assume the component's lifecycle or weak references handle cleanup.
72
+ }
73
+
74
+ handleQueryParamsChange(queryParams) {
75
+ const newPage = parseInt(queryParams.page, 10) || 1;
76
+ const newLimit = parseInt(queryParams.limit, 10) || 10;
77
+
78
+ let shouldUpdate = false;
79
+
80
+ if (newPage !== this._currentPage) {
81
+ this._currentPage = newPage;
82
+ shouldUpdate = true;
83
+ }
84
+ if (newLimit !== this._limit) {
85
+ this._limit = newLimit;
86
+ shouldUpdate = true;
87
+ }
88
+
89
+ if (shouldUpdate) {
90
+ this.update();
91
+ this.dispatchEvent(
92
+ new CustomEvent('pagination-query-params-change', { detail: { page: this._currentPage, limit: this._limit } }),
93
+ );
94
+ }
50
95
  }
51
96
 
52
97
  handlePageChange(newPage) {
@@ -54,7 +99,7 @@ class AgPagination extends HTMLElement {
54
99
  return;
55
100
  }
56
101
  this._currentPage = newPage;
57
- setQueryParams({ page: newPage, limit: this._limit });
102
+ setQueryParams({ page: newPage, limit: this._limit }, { replace: false }); // Use pushState
58
103
  this.dispatchEvent(new CustomEvent('page-change', { detail: { page: newPage } }));
59
104
  }
60
105
 
@@ -65,7 +110,7 @@ class AgPagination extends HTMLElement {
65
110
  }
66
111
  this._limit = newLimit;
67
112
  this._currentPage = 1; // Reset to first page on limit change
68
- setQueryParams({ page: 1, limit: newLimit });
113
+ setQueryParams({ page: 1, limit: newLimit }, { replace: false }); // Use pushState
69
114
  this.dispatchEvent(new CustomEvent('limit-change', { detail: { limit: newLimit } }));
70
115
  }
71
116
 
@@ -87,7 +132,8 @@ class AgPagination extends HTMLElement {
87
132
  this.shadowRoot.querySelector('#page-info').textContent = `Page ${this._currentPage} of ${this._totalPages}`;
88
133
 
89
134
  const limitSelector = this.shadowRoot.querySelector('#limit-selector');
90
- if (limitSelector.value != this._limit) {
135
+ if (limitSelector && limitSelector.value != this._limit) {
136
+ // Added null check for limitSelector
91
137
  limitSelector.value = this._limit;
92
138
  }
93
139
 
@@ -133,6 +179,8 @@ class AgPagination extends HTMLElement {
133
179
  }
134
180
 
135
181
  render() {
182
+ const limitOptionsHtml = this._limitOptions.map((value) => `<option value="${value}">${value}</option>`).join('');
183
+
136
184
  this.shadowRoot.innerHTML = html`
137
185
  <style>
138
186
  :host {
@@ -193,10 +241,7 @@ class AgPagination extends HTMLElement {
193
241
  <button id="next-page">Next</button>
194
242
  <button id="last-page">Last</button>
195
243
  <select id="limit-selector">
196
- <option value="10">10</option>
197
- <option value="20">20</option>
198
- <option value="50">50</option>
199
- <option value="100">100</option>
244
+ ${limitOptionsHtml}
200
245
  </select>
201
246
  `;
202
247
  }
@@ -19,6 +19,13 @@ const logger = loggerFactory(import.meta, { trace: true });
19
19
  */
20
20
  const RouterEvents = {};
21
21
 
22
+ /**
23
+ * @type {Object.<string, function>}\n
24
+ * @description Holds event listeners for query parameter changes.\n
25
+ * @memberof PwaRouter
26
+ */
27
+ const queryParamsChangeListeners = {};
28
+
22
29
  /**
23
30
  * @type {string[]}
24
31
  * @description Array of core UI component IDs that should not trigger modal close route changes.
@@ -227,6 +234,22 @@ const listenQueryPathInstance = ({ id, routeId, event }, queryKey = 'cid') => {
227
234
  });
228
235
  };
229
236
 
237
+ /**
238
+ * Registers a listener for changes to query parameters.\n
239
+ * The provided event callback is triggered with the current query parameters object.\n
240
+ * @param {object} options - The listener options.\n
241
+ * @param {string} options.id - A unique ID for the listener.\n
242
+ * @param {function(Object.<string, string>): void} options.event - The callback function to execute with the new query parameters.\n
243
+ * @memberof PwaRouter
244
+ */
245
+ const listenQueryParamsChange = ({ id, event }) => {
246
+ queryParamsChangeListeners[id] = event;
247
+ // Immediately call with current query params for initial state
248
+ setTimeout(() => {
249
+ event(getQueryParams());
250
+ });
251
+ };
252
+
230
253
  /**
231
254
  * Handles the logic for changing the route when a modal is closed. It determines the next URL
232
255
  * based on the remaining open modals or falls back to a home URL.
@@ -300,7 +323,18 @@ const setQueryParams = (newParams, options = { replace: true }) => {
300
323
 
301
324
  const newPath = url.pathname + url.search + url.hash;
302
325
 
303
- history.pushState(history.state, '', newPath);
326
+ if (options.replace) {
327
+ history.replaceState(history.state, '', newPath);
328
+ } else {
329
+ history.pushState(history.state, '', newPath);
330
+ }
331
+
332
+ const updatedParams = getQueryParams();
333
+ for (const listenerId in queryParamsChangeListeners) {
334
+ if (Object.hasOwnProperty.call(queryParamsChangeListeners, listenerId)) {
335
+ queryParamsChangeListeners[listenerId](updatedParams);
336
+ }
337
+ }
304
338
  };
305
339
 
306
340
  export {
@@ -319,4 +353,6 @@ export {
319
353
  setPath,
320
354
  setQueryParams,
321
355
  sanitizeRoute,
356
+ queryParamsChangeListeners,
357
+ listenQueryParamsChange,
322
358
  };
@@ -516,6 +516,10 @@ const TranslateCore = {
516
516
  en: 'Cron Management',
517
517
  es: 'Gestion de cron jobs',
518
518
  };
519
+ Translate.Data['success-reload-data'] = {
520
+ en: 'Data reloaded successfully.',
521
+ es: 'Datos recargados con éxito.',
522
+ };
519
523
  },
520
524
  };
521
525
 
@@ -53,15 +53,22 @@ class PwaWorker {
53
53
  */
54
54
  constructor() {
55
55
  this.title = `${s('title').textContent}`;
56
+ if (!window.renderPayload.dev) {
57
+ console.log = () => null;
58
+ console.error = () => null;
59
+ console.info = () => null;
60
+ console.warn = () => null;
61
+ }
56
62
  }
57
63
 
58
64
  /**
59
65
  * Checks if the application is running in development mode (localhost or 127.0.0.1).
66
+ * @method devMode
60
67
  * @memberof PwaWorker
61
68
  * @returns {boolean} True if in development mode.
62
69
  */
63
70
  devMode() {
64
- return location.origin.match('localhost') || location.origin.match('127.0.0.1');
71
+ return window.renderPayload.dev || location.origin.match('localhost') || location.origin.match('127.0.0.1');
65
72
  }
66
73
 
67
74
  /**
@@ -18,7 +18,7 @@ const DefaultOptions = {
18
18
  serviceId: 'default-management',
19
19
  entity: 'default',
20
20
  columnDefs: [
21
- { field: '0', headerName: '0' },
21
+ { field: '0', headerName: '0', cellClassRules: { 'row-new-highlight': (params) => true } },
22
22
  { field: '1', headerName: '1' },
23
23
  { field: '2', headerName: '2' },
24
24
  { field: 'createdAt', headerName: 'createdAt', cellDataType: 'date', editable: false },
@@ -29,6 +29,10 @@ const DefaultOptions = {
29
29
  permissions: {
30
30
  add: true,
31
31
  remove: true,
32
+ reload: true,
33
+ },
34
+ paginationOptions: {
35
+ limitOptions: [10, 20, 50, 100],
32
36
  },
33
37
  };
34
38
 
@@ -66,25 +70,36 @@ const DefaultManagement = {
66
70
  paginationComp.setAttribute('current-page', this.Tokens[id].page);
67
71
  paginationComp.setAttribute('total-pages', this.Tokens[id].totalPages);
68
72
  paginationComp.setAttribute('total-items', this.Tokens[id].total);
73
+ setTimeout(async () => {
74
+ if (DefaultManagement.Tokens[id].readyRowDataEvent)
75
+ for (const event of Object.keys(DefaultManagement.Tokens[id].readyRowDataEvent))
76
+ await DefaultManagement.Tokens[id].readyRowDataEvent[event](rowDataScope);
77
+ }, 1);
78
+ }
79
+ },
80
+ refreshTable: async function (id) {
81
+ const gridApi = AgGrid.grids[this.Tokens[id].gridId];
82
+ if (gridApi) {
83
+ // Use refreshCells with change detection for optimal performance
84
+ // This is preferred over redrawRows() as it only updates changed cells
85
+ gridApi.refreshCells({
86
+ force: false, // Use change detection - only refresh cells whose values have changed
87
+ suppressFlash: false, // Show flash animation for changed cells (requires enableCellChangeFlash)
88
+ });
69
89
  }
70
90
  },
71
91
  RenderTable: async function (options = DefaultOptions) {
72
92
  if (!options) options = DefaultOptions;
73
- const { serviceId, columnDefs, entity, defaultColKeyFocus, ServiceProvider, permissions } = options;
93
+ const { serviceId, columnDefs, entity, defaultColKeyFocus, ServiceProvider, permissions, paginationOptions } =
94
+ options;
74
95
  logger.info('DefaultManagement RenderTable', options);
75
96
  const id = options?.idModal ? options.idModal : getId(this.Tokens, `${serviceId}-`);
76
97
  const gridId = `${serviceId}-grid-${id}`;
77
98
  const queryParams = getQueryParams();
78
99
  const page = parseInt(queryParams.page) || 1;
79
- const limit = parseInt(queryParams.limit) || 10;
80
- this.Tokens[id] = {
81
- ...options,
82
- gridId,
83
- page,
84
- limit,
85
- total: 0,
86
- totalPages: 1,
87
- };
100
+ const defaultLimit = paginationOptions?.limitOptions?.[0] || 10;
101
+ const limit = parseInt(queryParams.limit) || defaultLimit;
102
+ this.Tokens[id] = { ...this.Tokens[id], ...options, gridId, page, limit, total: 0, totalPages: 1 };
88
103
 
89
104
  setQueryParams({ page, limit });
90
105
  setTimeout(async () => {
@@ -107,7 +122,7 @@ const DefaultManagement = {
107
122
  label: html`<div class="abs center">
108
123
  <i class="fas fa-times"></i>
109
124
  </div> `,
110
- class: `in fll section-mp management-table-btn-mini management-table-btn-remove-${id}-${cellRenderId}`,
125
+ class: `in fll section-mp management-table-btn-mini management-table-btn-remove-${id}-${cellRenderId} ${!params.data._id ? 'hide' : ''}`,
111
126
  })}`;
112
127
  setTimeout(() => {
113
128
  EventsUI.onClick(
@@ -191,9 +206,16 @@ const DefaultManagement = {
191
206
  // }
192
207
  // }
193
208
  s(`.management-table-btn-save-${id}`).onclick = () => {
209
+ s(`.management-table-btn-save-${id}`).classList.add('hide');
210
+ // s(`.management-table-btn-stop-${id}`).classList.add('hide');
211
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
212
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
213
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
194
214
  AgGrid.grids[gridId].stopEditing();
195
215
  };
196
216
  EventsUI.onClick(`.management-table-btn-add-${id}`, async () => {
217
+ if (options.customEvent && options.customEvent.add) return await options.customEvent.add();
218
+
197
219
  const rowObj = {};
198
220
  for (const def of columnDefs) {
199
221
  rowObj[def.field] = '';
@@ -260,6 +282,11 @@ const DefaultManagement = {
260
282
  // }
261
283
 
262
284
  setTimeout(() => {
285
+ s(`.management-table-btn-save-${id}`).classList.remove('hide');
286
+ // s(`.management-table-btn-stop-${id}`).classList.remove('hide');
287
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.add('hide');
288
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.add('hide');
289
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.add('hide');
263
290
  AgGrid.grids[gridId].startEditingCell({
264
291
  rowIndex: 0,
265
292
  colKey: defaultColKeyFocus,
@@ -268,6 +295,15 @@ const DefaultManagement = {
268
295
  });
269
296
  });
270
297
  });
298
+
299
+ EventsUI.onClick(`.management-table-btn-stop-${id}`, async () => {
300
+ s(`.management-table-btn-save-${id}`).classList.add('hide');
301
+ // s(`.management-table-btn-stop-${id}`).classList.add('hide');
302
+ if (permissions.add) s(`.management-table-btn-add-${id}`).classList.remove('hide');
303
+ if (permissions.remove) s(`.management-table-btn-clean-${id}`).classList.remove('hide');
304
+ if (permissions.reload) s(`.management-table-btn-reload-${id}`).classList.remove('hide');
305
+ AgGrid.grids[gridId].stopEditing();
306
+ });
271
307
  EventsUI.onClick(`.management-table-btn-clean-${id}`, async () => {
272
308
  const confirmResult = await Modal.RenderConfirm(
273
309
  {
@@ -292,6 +328,26 @@ const DefaultManagement = {
292
328
  DefaultManagement.loadTable(id);
293
329
  }
294
330
  });
331
+ EventsUI.onClick(`.management-table-btn-reload-${id}`, async () => {
332
+ try {
333
+ // Reload data from server
334
+ await DefaultManagement.loadTable(id);
335
+
336
+ // Other option: Refresh cells to update UI
337
+ // DefaultManagement.refreshTable(id);
338
+
339
+ NotificationManager.Push({
340
+ html: Translate.Render('success-reload-data') || 'Data reloaded successfully',
341
+ status: 'success',
342
+ });
343
+ } catch (error) {
344
+ NotificationManager.Push({
345
+ html: error.message || 'Error reloading data',
346
+ status: 'error',
347
+ });
348
+ } finally {
349
+ }
350
+ });
295
351
  s(`#ag-pagination-${gridId}`).addEventListener('page-change', async (event) => {
296
352
  const token = DefaultManagement.Tokens[id];
297
353
  token.page = event.detail.page;
@@ -318,7 +374,7 @@ const DefaultManagement = {
318
374
  }
319
375
  };
320
376
  }, 1);
321
- return html`<div class="fl">
377
+ return html`<div class="fl management-table-toolbar">
322
378
  ${await BtnIcon.Render({
323
379
  class: `in fll section-mp management-table-btn-mini management-table-btn-add-${id} ${
324
380
  permissions.add ? '' : 'hide'
@@ -327,12 +383,15 @@ const DefaultManagement = {
327
383
  type: 'button',
328
384
  })}
329
385
  ${await BtnIcon.Render({
330
- class: `in fll section-mp management-table-btn-mini management-table-btn-save-${id} ${
331
- permissions.add ? '' : 'hide'
332
- }`,
386
+ class: `in fll section-mp management-table-btn-mini management-table-btn-save-${id} hide`,
333
387
  label: html`<div class="abs center btn-save-${id}-label"><i class="fas fa-save"></i></div> `,
334
388
  type: 'button',
335
389
  })}
390
+ ${await BtnIcon.Render({
391
+ class: `in fll section-mp management-table-btn-mini management-table-btn-stop-${id} hide`,
392
+ label: html`<div class="abs center btn-save-${id}-label"><i class="fa-solid fa-rectangle-xmark"></i></div> `,
393
+ type: 'button',
394
+ })}
336
395
  ${await BtnIcon.Render({
337
396
  class: `in fll section-mp management-table-btn-mini management-table-btn-clean-${id} ${
338
397
  permissions.remove ? '' : 'hide'
@@ -340,12 +399,21 @@ const DefaultManagement = {
340
399
  label: html`<div class="abs center btn-clean-${id}-label"><i class="fas fa-broom"></i></div> `,
341
400
  type: 'button',
342
401
  })}
402
+ ${await BtnIcon.Render({
403
+ class: `in fll section-mp management-table-btn-mini management-table-btn-reload-${id} ${
404
+ permissions.reload ? '' : 'hide'
405
+ }`,
406
+ label: html`<div class="abs center btn-reload-${id}-label"><i class="fas fa-sync-alt"></i></div> `,
407
+ type: 'button',
408
+ })}
343
409
  </div>
344
410
  <div class="in section-mp">
345
411
  ${await AgGrid.Render({
346
412
  id: gridId,
347
413
  parentModal: options.idModal,
348
414
  usePagination: true,
415
+ paginationOptions,
416
+ customHeightOffset: !permissions.add && !permissions.remove && !permissions.reload ? 50 : 0,
349
417
  darkTheme,
350
418
  gridOptions: {
351
419
  defaultColDef: {
@@ -425,6 +493,7 @@ const DefaultManagement = {
425
493
  // rowNode.setData(newRow);
426
494
  // }, 2000);
427
495
  }
496
+ s(`.management-table-btn-save-${id}`).click();
428
497
  }
429
498
  } else {
430
499
  const body = event.data ? event.data : {};
@@ -441,6 +510,7 @@ const DefaultManagement = {
441
510
  }
442
511
  }
443
512
  },
513
+ ...(options.gridOptions ? options.gridOptions : undefined),
444
514
  },
445
515
  })}
446
516
  </div>`;
@@ -42,9 +42,9 @@ class MariaDBService {
42
42
  try {
43
43
  conn = await pool.getConnection();
44
44
  result = await conn.query(query, { supportBigNumbers: true, bigNumberStrings: true });
45
- logger.info(query, result);
45
+ logger.info('query');
46
+ console.log(result);
46
47
  } catch (error) {
47
- if (error.stack.startsWith('TypeError: Do not know how to serialize a BigInt')) return;
48
48
  logger.error(error, error.stack);
49
49
  } finally {
50
50
  if (conn) conn.release(); // release to pool
package/src/index.js CHANGED
@@ -35,7 +35,7 @@ class Underpost {
35
35
  * @type {String}
36
36
  * @memberof Underpost
37
37
  */
38
- static version = 'v2.85.7';
38
+ static version = 'v2.89.1';
39
39
  /**
40
40
  * Repository cli API
41
41
  * @static
@@ -32,6 +32,51 @@ dotenv.config();
32
32
 
33
33
  // Static Site Generation (SSG)
34
34
 
35
+ /**
36
+ * Recursively copies files from source to destination, but only files that don't exist in destination.
37
+ * @function copyNonExistingFiles
38
+ * @param {string} src - Source directory path
39
+ * @param {string} dest - Destination directory path
40
+ * @returns {void}
41
+ * @memberof clientBuild
42
+ */
43
+ const copyNonExistingFiles = (src, dest) => {
44
+ // Ensure source exists
45
+ if (!fs.existsSync(src)) {
46
+ throw new Error(`Source directory does not exist: ${src}`);
47
+ }
48
+
49
+ // Get stats for source
50
+ const srcStats = fs.statSync(src);
51
+
52
+ // If source is a file, copy only if it doesn't exist in destination
53
+ if (srcStats.isFile()) {
54
+ if (!fs.existsSync(dest)) {
55
+ const destDir = dir.dirname(dest);
56
+ fs.mkdirSync(destDir, { recursive: true });
57
+ fs.copyFileSync(src, dest);
58
+ }
59
+ return;
60
+ }
61
+
62
+ // If source is a directory, create destination if it doesn't exist
63
+ if (srcStats.isDirectory()) {
64
+ if (!fs.existsSync(dest)) {
65
+ fs.mkdirSync(dest, { recursive: true });
66
+ }
67
+
68
+ // Read all items in source directory
69
+ const items = fs.readdirSync(src);
70
+
71
+ // Recursively process each item
72
+ for (const item of items) {
73
+ const srcPath = dir.join(src, item);
74
+ const destPath = dir.join(dest, item);
75
+ copyNonExistingFiles(srcPath, destPath);
76
+ }
77
+ }
78
+ };
79
+
35
80
  /**
36
81
  * @async
37
82
  * @function buildClient
@@ -83,6 +128,7 @@ const buildClient = async (options = { liveClientBuildPaths: [], instances: [] }
83
128
  * @param {string} options.publicClientId - Public client ID.
84
129
  * @param {boolean} options.iconsBuild - Whether to build icons.
85
130
  * @param {Object} options.metadata - Metadata for the client.
131
+ * @param {boolean} options.publicCopyNonExistingFiles - Whether to copy non-existing files from public directory.
86
132
  * @returns {Promise<void>} - Promise that resolves when the full build is complete.
87
133
  * @throws {Error} - If the full build fails.
88
134
  * @memberof clientBuild
@@ -98,6 +144,7 @@ const buildClient = async (options = { liveClientBuildPaths: [], instances: [] }
98
144
  publicClientId,
99
145
  iconsBuild,
100
146
  metadata,
147
+ publicCopyNonExistingFiles,
101
148
  }) => {
102
149
  logger.warn('Full build', rootClientPath);
103
150
 
@@ -169,11 +216,15 @@ const buildClient = async (options = { liveClientBuildPaths: [], instances: [] }
169
216
  fs.copySync(dist.styles, `${rootClientPath}${dist.public_styles_folder}`);
170
217
  }
171
218
  }
219
+
220
+ if (publicCopyNonExistingFiles)
221
+ copyNonExistingFiles(`./src/client/public/${publicCopyNonExistingFiles}`, rootClientPath);
172
222
  };
173
223
 
174
224
  // { srcBuildPath, publicBuildPath }
175
225
  const enableLiveRebuild =
176
226
  options && options.liveClientBuildPaths && options.liveClientBuildPaths.length > 0 ? true : false;
227
+ const isDevelopment = process.env.NODE_ENV === 'development';
177
228
 
178
229
  let currentPort = parseInt(process.env.PORT) + 1;
179
230
  for (const host of Object.keys(confServer)) {
@@ -205,7 +256,8 @@ const buildClient = async (options = { liveClientBuildPaths: [], instances: [] }
205
256
  } = confServer[host][path];
206
257
  if (singleReplica) continue;
207
258
  if (!confClient[client]) confClient[client] = {};
208
- const { components, dists, views, services, metadata, publicRef } = confClient[client];
259
+ const { components, dists, views, services, metadata, publicRef, publicCopyNonExistingFiles } =
260
+ confClient[client];
209
261
  let backgroundImage;
210
262
  if (metadata) {
211
263
  backgroundImage = metadata.backgroundImage;
@@ -240,6 +292,7 @@ const buildClient = async (options = { liveClientBuildPaths: [], instances: [] }
240
292
  publicClientId,
241
293
  iconsBuild,
242
294
  metadata,
295
+ publicCopyNonExistingFiles,
243
296
  });
244
297
 
245
298
  if (components)
@@ -520,6 +573,7 @@ const buildClient = async (options = { liveClientBuildPaths: [], instances: [] }
520
573
  apiBaseHost,
521
574
  apiBasePath: process.env.BASE_API,
522
575
  version: Underpost.version,
576
+ ...(isDevelopment ? { dev: true } : undefined),
523
577
  },
524
578
  renderApi: {
525
579
  JSONweb,
@@ -613,6 +667,7 @@ Sitemap: https://${host}${path === '/' ? '' : path}/sitemap.xml`,
613
667
  apiBaseHost,
614
668
  apiBasePath: process.env.BASE_API,
615
669
  version: Underpost.version,
670
+ ...(isDevelopment ? { dev: true } : undefined),
616
671
  },
617
672
  renderApi: {
618
673
  JSONweb,
@@ -687,4 +742,4 @@ ${fs.readFileSync(`${rootClientPath}/sw.js`, 'utf8')}`,
687
742
  }
688
743
  };
689
744
 
690
- export { buildClient };
745
+ export { buildClient, copyNonExistingFiles };
@@ -192,6 +192,49 @@ export class ObjectLayerEngine {
192
192
  return objectLayerFrameDirections;
193
193
  }
194
194
 
195
+ /**
196
+ * @memberof CyberiaObjectLayer
197
+ * @static
198
+ * @description Processes an image file through frameFactory and adds the resulting frame to the render data structure.
199
+ * Updates the color palette and pushes the frame to all keyframe directions corresponding to the given direction code.
200
+ * Initializes colors array, frames object, and direction arrays if they don't exist.
201
+ * @param {Object} renderData - The render data object containing frames and colors.
202
+ * @param {string} imagePath - The path to the image file to process.
203
+ * @param {string} directionCode - The numerical direction code (e.g., '08', '14').
204
+ * @returns {Promise<Object>} - The updated render data object.
205
+ * @memberof CyberiaObjectLayer
206
+ */
207
+ static async processAndPushFrame(renderData, imagePath, directionCode) {
208
+ // Initialize colors array if it doesn't exist
209
+ if (!renderData.colors) {
210
+ renderData.colors = [];
211
+ }
212
+
213
+ // Initialize frames object if it doesn't exist
214
+ if (!renderData.frames) {
215
+ renderData.frames = {};
216
+ }
217
+
218
+ // Process the image and extract frame matrix and updated colors
219
+ const frameFactoryResult = await ObjectLayerEngine.frameFactory(imagePath, renderData.colors);
220
+
221
+ // Update the colors palette
222
+ renderData.colors = frameFactoryResult.colors;
223
+
224
+ // Get all keyframe directions for this direction code
225
+ const keyframeDirections = ObjectLayerEngine.getKeyFramesDirectionsFromNumberFolderDirection(directionCode);
226
+
227
+ // Push the frame to all corresponding directions
228
+ for (const keyframeDirection of keyframeDirections) {
229
+ if (!renderData.frames[keyframeDirection]) {
230
+ renderData.frames[keyframeDirection] = [];
231
+ }
232
+ renderData.frames[keyframeDirection].push(frameFactoryResult.frame);
233
+ }
234
+
235
+ return renderData;
236
+ }
237
+
195
238
  /**
196
239
  * @memberof CyberiaObjectLayer
197
240
  * @static
@@ -290,5 +333,6 @@ export const readPngAsync = ObjectLayerEngine.readPngAsync;
290
333
  export const frameFactory = ObjectLayerEngine.frameFactory;
291
334
  export const getKeyFramesDirectionsFromNumberFolderDirection =
292
335
  ObjectLayerEngine.getKeyFramesDirectionsFromNumberFolderDirection;
336
+ export const processAndPushFrame = ObjectLayerEngine.processAndPushFrame;
293
337
  export const buildImgFromTile = ObjectLayerEngine.buildImgFromTile;
294
338
  export const generateRandomStats = ObjectLayerEngine.generateRandomStats;
@@ -122,6 +122,12 @@ class UnderpostStartUp {
122
122
  if (options.build === true) await UnderpostStartUp.API.build(deployId, env);
123
123
  if (options.run === true) await UnderpostStartUp.API.run(deployId, env);
124
124
  },
125
+ /**
126
+ * Run itc-scripts and builds client bundle.
127
+ * @param {string} deployId - The ID of the deployment.
128
+ * @param {string} env - The environment of the deployment.
129
+ * @memberof UnderpostStartUp
130
+ */
125
131
  async build(deployId = 'dd-default', env = 'development') {
126
132
  const buildBasePath = `/home/dd`;
127
133
  const repoName = `engine-${deployId.split('-')[1]}`;
@@ -139,6 +145,12 @@ class UnderpostStartUp {
139
145
  }
140
146
  shellExec(`node bin/deploy build-full-client ${deployId}`);
141
147
  },
148
+ /**
149
+ * Runs a deployment.
150
+ * @param {string} deployId - The ID of the deployment.
151
+ * @param {string} env - The environment of the deployment.
152
+ * @memberof UnderpostStartUp
153
+ */
142
154
  async run(deployId = 'dd-default', env = 'development') {
143
155
  const runCmd = env === 'production' ? 'run prod-img' : 'run dev-img';
144
156
  if (fs.existsSync(`./engine-private/replica`)) {