@underpostnet/underpost 2.96.1 → 2.97.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 (51) hide show
  1. package/.dockerignore +1 -2
  2. package/.env.development +0 -3
  3. package/.env.production +0 -3
  4. package/.env.test +0 -3
  5. package/.prettierignore +1 -2
  6. package/README.md +31 -31
  7. package/baremetal/commission-workflows.json +94 -17
  8. package/bin/deploy.js +1 -1
  9. package/cli.md +75 -41
  10. package/conf.js +1 -0
  11. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  12. package/manifests/deployment/dd-test-development/deployment.yaml +4 -4
  13. package/package.json +3 -2
  14. package/packer/scripts/fuse-tar-root +3 -3
  15. package/scripts/disk-clean.sh +128 -187
  16. package/scripts/gpu-diag.sh +2 -2
  17. package/scripts/ip-info.sh +11 -11
  18. package/scripts/ipxe-setup.sh +197 -0
  19. package/scripts/maas-upload-boot-resource.sh +1 -1
  20. package/scripts/nvim.sh +1 -1
  21. package/scripts/packer-setup.sh +13 -13
  22. package/scripts/ports-ls.sh +31 -0
  23. package/scripts/quick-tftp.sh +19 -0
  24. package/scripts/rocky-setup.sh +2 -2
  25. package/scripts/rpmfusion-ffmpeg-setup.sh +4 -4
  26. package/scripts/ssl.sh +7 -7
  27. package/src/api/document/document.controller.js +15 -0
  28. package/src/api/document/document.model.js +44 -1
  29. package/src/api/document/document.router.js +2 -0
  30. package/src/api/document/document.service.js +398 -26
  31. package/src/cli/baremetal.js +2001 -463
  32. package/src/cli/cloud-init.js +354 -231
  33. package/src/cli/cluster.js +51 -53
  34. package/src/cli/db.js +22 -0
  35. package/src/cli/deploy.js +7 -3
  36. package/src/cli/image.js +1 -0
  37. package/src/cli/index.js +40 -37
  38. package/src/cli/lxd.js +3 -3
  39. package/src/cli/run.js +78 -12
  40. package/src/cli/ssh.js +1 -1
  41. package/src/client/components/core/Css.js +16 -2
  42. package/src/client/components/core/Input.js +3 -1
  43. package/src/client/components/core/Modal.js +125 -159
  44. package/src/client/components/core/Panel.js +436 -31
  45. package/src/client/components/core/PanelForm.js +222 -37
  46. package/src/client/components/core/SearchBox.js +801 -0
  47. package/src/client/components/core/Translate.js +11 -0
  48. package/src/client/services/document/document.service.js +42 -0
  49. package/src/index.js +1 -1
  50. package/src/server/dns.js +12 -6
  51. package/src/server/start.js +14 -6
@@ -6,7 +6,7 @@ import { NotificationManager } from './NotificationManager.js';
6
6
  import { DocumentService } from '../../services/document/document.service.js';
7
7
  import { FileService } from '../../services/file/file.service.js';
8
8
  import { getSrcFromFileData } from './Input.js';
9
- import { imageShimmer, renderCssAttr } from './Css.js';
9
+ import { imageShimmer, renderCssAttr, darkTheme, ThemeEvents, subThemeManager, lightenHex, darkenHex } from './Css.js';
10
10
  import { Translate } from './Translate.js';
11
11
  import { Modal } from './Modal.js';
12
12
  import { closeModalRouteChangeEvents, listenQueryPathInstance, setQueryPath, getQueryParams } from './Router.js';
@@ -27,11 +27,17 @@ const PanelForm = {
27
27
  route: 'home',
28
28
  htmlFormHeader: async () => '',
29
29
  firsUpdateEvent: async () => {},
30
+ share: {
31
+ copyLink: false,
32
+ },
33
+ showCreatorProfile: false,
30
34
  },
31
35
  ) {
32
36
  const { idPanel, defaultUrlImage, Elements } = options;
33
37
 
34
- let prefixTags = [idPanel, 'public'];
38
+ // Authenticated users don't need 'public' tag - they see all their own posts
39
+ // Only include 'public' for unauthenticated users (handled by backend)
40
+ let prefixTags = [idPanel];
35
41
  this.Data[idPanel] = {
36
42
  originData: [],
37
43
  data: [],
@@ -48,7 +54,7 @@ const PanelForm = {
48
54
  id: 'panel-title',
49
55
  model: 'title',
50
56
  inputType: 'text',
51
- rules: [{ type: 'isEmpty' }],
57
+ rules: [],
52
58
  panel: { type: 'title' },
53
59
  },
54
60
  {
@@ -80,7 +86,7 @@ const PanelForm = {
80
86
  // value: html``,
81
87
  // },
82
88
  // },
83
- rules: [{ type: 'isEmpty' }],
89
+ rules: [],
84
90
  },
85
91
  {
86
92
  id: 'panel-mdFileId',
@@ -108,6 +114,8 @@ const PanelForm = {
108
114
  titleIcon,
109
115
  route: options.route,
110
116
  formContainerClass: 'session-in-log-in',
117
+ share: options.share,
118
+ showCreatorProfile: options.showCreatorProfile,
111
119
  onClick: async function ({ payload }) {
112
120
  if (options.route) {
113
121
  setQueryPath({ path: options.route, queryPath: payload._id });
@@ -169,9 +177,113 @@ const PanelForm = {
169
177
  html: status,
170
178
  status,
171
179
  });
172
- if (getQueryParams().cid === data.id) {
173
- setQueryPath({ path: options.route, queryPath: '' });
174
- if (PanelForm.Data[idPanel].updatePanel) await PanelForm.Data[idPanel].updatePanel();
180
+
181
+ // Handle cid query param update (supports comma-separated list)
182
+ if (status === 'success') {
183
+ const currentCid = getQueryParams().cid;
184
+
185
+ if (currentCid) {
186
+ // Parse cid as comma-separated list
187
+ const cidList = currentCid
188
+ .split(',')
189
+ .map((id) => id.trim())
190
+ .filter((id) => id);
191
+
192
+ // Remove the deleted panel's id from the list
193
+ const updatedCidList = cidList.filter((id) => id !== data.id);
194
+
195
+ if (updatedCidList.length !== cidList.length) {
196
+ // Wait for DOM cleanup before updating query
197
+
198
+ if (updatedCidList.length === 0) {
199
+ // No cids remain, clear query and reload panels with limit
200
+ logger.warn('All cids removed, clearing query');
201
+ setQueryPath({ path: options.route, queryPath: '' });
202
+
203
+ if (options.parentIdModal) Modal.Data[options.parentIdModal].query = window.location.search;
204
+ if (PanelForm.Data[idPanel].updatePanel) await PanelForm.Data[idPanel].updatePanel();
205
+ } else {
206
+ // Update query params with remaining cids only (without ?cid= prefix)
207
+ const cidValue = updatedCidList.join(',');
208
+ setQueryPath({ path: options.route, queryPath: cidValue });
209
+ const actualQuery = window.location.search;
210
+ if (options.parentIdModal) Modal.Data[options.parentIdModal].query = actualQuery;
211
+ }
212
+ }
213
+
214
+ // Return early to skip smart deletion logic when cid is present
215
+ return { status };
216
+ }
217
+ }
218
+
219
+ // Smart deletion: remove from arrays and intelligently load more if needed
220
+ if (status === 'success') {
221
+ const panelData = PanelForm.Data[idPanel];
222
+
223
+ // Remove the deleted item from all data arrays
224
+ const indexInOrigin = panelData.originData.findIndex((d) => d._id === data._id);
225
+ const indexInData = panelData.data.findIndex((d) => d._id === data._id);
226
+ const indexInFiles = panelData.filesData.findIndex((d) => d._id === data._id);
227
+
228
+ if (indexInOrigin > -1) panelData.originData.splice(indexInOrigin, 1);
229
+ if (indexInData > -1) panelData.data.splice(indexInData, 1);
230
+ if (indexInFiles > -1) panelData.filesData.splice(indexInFiles, 1);
231
+
232
+ // Adjust skip count since we removed an item
233
+ if (panelData.skip > 0) panelData.skip--;
234
+
235
+ // If panels are below limit and there might be more, load them
236
+ if (panelData.data.length < panelData.limit && panelData.hasMore && !panelData.loading) {
237
+ const oldDataCount = panelData.data.length;
238
+ const needed = panelData.limit - panelData.data.length; // Calculate exact number needed
239
+ const originalLimit = panelData.limit;
240
+
241
+ // Temporarily set limit to only fetch what's needed (1-to-1 replacement)
242
+ panelData.limit = needed;
243
+ await getPanelData(true); // Load only the needed items
244
+ panelData.limit = originalLimit; // Restore original limit
245
+
246
+ const newItems = panelData.data.slice(oldDataCount);
247
+
248
+ if (oldDataCount === 0) {
249
+ // List was empty, render all panels
250
+ if (panelData.data.length > 0) {
251
+ const containerSelector = `.${options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body'}`;
252
+ htmls(
253
+ containerSelector,
254
+ html`
255
+ <div class="in">${await panelRender({ data: panelData.data })}</div>
256
+ <div class="in panel-placeholder-bottom panel-placeholder-bottom-${idPanel}"></div>
257
+ `,
258
+ );
259
+
260
+ // Show spinner if there's potentially more data
261
+ const lastOriginItem = panelData.originData[panelData.originData.length - 1];
262
+ if (
263
+ !panelData.lasIdAvailable ||
264
+ !lastOriginItem ||
265
+ panelData.lasIdAvailable !== lastOriginItem._id
266
+ )
267
+ LoadingAnimation.spinner.play(`.panel-placeholder-bottom-${idPanel}`, 'dual-ring-mini');
268
+ } else {
269
+ // No more data available, show empty state
270
+ const containerSelector = `.${options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body'}`;
271
+ htmls(
272
+ containerSelector,
273
+ html`
274
+ <div class="in">${await panelRender({ data: [] })}</div>
275
+ <div class="in panel-placeholder-bottom panel-placeholder-bottom-${idPanel}"></div>
276
+ `,
277
+ );
278
+ }
279
+ } else {
280
+ // List had some panels, append new ones
281
+ if (newItems.length > 0) {
282
+ for (const item of newItems)
283
+ append(`.${idPanel}-render`, await Panel.Tokens[idPanel].renderPanel(item));
284
+ }
285
+ }
286
+ }
175
287
  }
176
288
 
177
289
  return { status };
@@ -179,30 +291,61 @@ const PanelForm = {
179
291
  return { status: 'error' };
180
292
  },
181
293
  initAdd: async function () {
182
- s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
294
+ setTimeout(() => {
295
+ s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
296
+ }, 50);
183
297
  },
184
298
  initEdit: async function ({ data }) {
185
- s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
299
+ // Clear file input when entering edit mode
300
+ const fileFormData = formData.find((f) => f.inputType === 'file');
301
+ if (fileFormData && s(`.${fileFormData.id}`)) {
302
+ s(`.${fileFormData.id}`).value = '';
303
+ s(`.${fileFormData.id}`).inputFiles = null;
304
+ htmls(
305
+ `.file-name-render-${fileFormData.id}`,
306
+ `<div class="abs center"><i style="font-size: 25px" class="fa-solid fa-cloud"></i></div>`,
307
+ );
308
+ }
309
+ setTimeout(() => {
310
+ s(`.modal-${options.route}`).scrollTo({ top: 0, behavior: 'smooth' });
311
+ }, 50);
186
312
  },
187
313
  noResultFound: async function () {
188
314
  LoadingAnimation.spinner.stop(`.panel-placeholder-bottom-${idPanel}`);
189
315
  },
190
316
  add: async function ({ data, editId }) {
317
+ // Validate that either mdFileId has content OR fileId has files
318
+ const hasMdContent = data.mdFileId && data.mdFileId.trim().length > 0;
319
+ const hasFiles = data.fileId && data.fileId.length > 0;
320
+
321
+ if (!data.title || (!hasMdContent && !hasFiles)) {
322
+ NotificationManager.Push({
323
+ html: Translate.Render('require-title-and-content-or-file'),
324
+ status: 'error',
325
+ });
326
+ return { data: [], status: 'error', message: 'Must provide either content or attach a file' };
327
+ }
328
+
191
329
  let mdFileId;
192
330
  const mdFileName = `${getCapVariableName(data.title)}.md`;
193
331
  const location = `${prefixTags.join('/')}`;
194
332
  const blob = new Blob([data.mdFileId], { type: 'text/markdown' });
195
333
  const md = new File([blob], mdFileName, { type: 'text/markdown' });
196
- const tags = uniqueArray(
197
- data.tags
198
- .replaceAll('/', ',')
199
- .replaceAll('-', ',')
200
- .replaceAll(' ', ',')
201
- .split(',')
202
- .map((t) => t.trim())
203
- .filter((t) => t)
204
- .concat(prefixTags),
205
- );
334
+ // Parse and normalize tags
335
+ // Note: 'public' tag is automatically extracted by the backend and converted to isPublic field
336
+ // It will be filtered from the tags array to keep visibility control separate from content tags
337
+ const tags = data.tags
338
+ ? uniqueArray(
339
+ data.tags
340
+ .replaceAll('/', ',')
341
+ .replaceAll('-', ',')
342
+ .replaceAll(' ', ',')
343
+ .split(',')
344
+ .map((t) => t.trim())
345
+ .filter((t) => t)
346
+ .concat(prefixTags),
347
+ )
348
+ : prefixTags;
206
349
  let originObj, originFileObj, indexOriginObj;
207
350
  if (editId) {
208
351
  indexOriginObj = PanelForm.Data[idPanel].originData.findIndex((d) => d._id === editId);
@@ -261,6 +404,7 @@ const PanelForm = {
261
404
  }
262
405
  }
263
406
  })();
407
+ // Backend will automatically extract 'public' from tags and set isPublic field
264
408
  const body = {
265
409
  location,
266
410
  tags,
@@ -284,6 +428,9 @@ const PanelForm = {
284
428
  _id: documentData._id,
285
429
  id: documentData._id,
286
430
  createdAt: documentData.createdAt,
431
+ // Use server response data - backend has already processed tags and isPublic
432
+ isPublic: documentData.isPublic || false,
433
+ tags: (documentData.tags || []).filter((t) => !prefixTags.includes(t)),
287
434
  };
288
435
 
289
436
  if (documentStatus === 'error') status = 'error';
@@ -341,10 +488,16 @@ const PanelForm = {
341
488
  const panelData = PanelForm.Data[idPanel];
342
489
  logger.warn('getPanelData called, isLoadMore:', isLoadMore);
343
490
  try {
344
- if (panelData.loading || !panelData.hasMore) {
345
- logger.warn('getPanelData early return - loading:', panelData.loading, 'hasMore:', panelData.hasMore);
346
- return;
491
+ const cidQuery = getQueryParams().cid;
492
+
493
+ // When cid query exists, bypass pagination and loading checks
494
+ if (!cidQuery) {
495
+ if (panelData.loading || !panelData.hasMore) {
496
+ logger.warn('getPanelData early return - loading:', panelData.loading, 'hasMore:', panelData.hasMore);
497
+ return;
498
+ }
347
499
  }
500
+
348
501
  panelData.loading = true;
349
502
 
350
503
  if (!isLoadMore) {
@@ -353,13 +506,20 @@ const PanelForm = {
353
506
  panelData.hasMore = true;
354
507
  }
355
508
 
509
+ // When cid query exists, don't apply skip/limit pagination
510
+ const params = {
511
+ tags: prefixTags.join(','),
512
+ ...(cidQuery && { cid: cidQuery }),
513
+ };
514
+
515
+ // Only apply pagination when there's no cid query
516
+ if (!cidQuery) {
517
+ params.skip = panelData.skip;
518
+ params.limit = panelData.limit;
519
+ }
520
+
356
521
  const result = await DocumentService.get({
357
- params: {
358
- tags: prefixTags.join(','),
359
- ...(getQueryParams().cid && { cid: getQueryParams().cid }),
360
- skip: panelData.skip,
361
- limit: panelData.limit,
362
- },
522
+ params,
363
523
  id: 'public/',
364
524
  });
365
525
 
@@ -411,12 +571,24 @@ const PanelForm = {
411
571
  id: documentObject._id,
412
572
  title: documentObject.title,
413
573
  createdAt: documentObject.createdAt,
574
+ // Backend filters 'public' tag automatically - it's converted to isPublic field
414
575
  tags: documentObject.tags.filter((t) => !prefixTags.includes(t)),
415
576
  mdFileId: marked.parse(mdFileId),
416
577
  userId: documentObject.userId._id,
578
+ userInfo:
579
+ documentObject.userId && typeof documentObject.userId === 'object'
580
+ ? {
581
+ username: documentObject.userId.username,
582
+ email: documentObject.userId.email,
583
+ _id: documentObject.userId._id,
584
+ profileImageId: documentObject.userId.profileImageId,
585
+ }
586
+ : null,
417
587
  fileId,
418
588
  tools: Elements.Data.user.main.model.user._id === documentObject.userId._id,
419
589
  _id: documentObject._id,
590
+ totalCopyShareLinkCount: documentObject.totalCopyShareLinkCount || 0,
591
+ isPublic: documentObject.isPublic || false,
420
592
  });
421
593
  } catch (fileError) {
422
594
  logger.error('Error fetching files for document:', documentObject._id, fileError);
@@ -425,9 +597,17 @@ const PanelForm = {
425
597
  }
426
598
  }
427
599
 
428
- panelData.skip += result.data.data.length;
429
- panelData.hasMore = result.data.data.length === panelData.limit;
430
- if (result.data.data.length === 0 || result.data.data.pop()._id === panelData.lasIdAvailable) {
600
+ // Only update pagination when not using cid query
601
+ if (!cidQuery) {
602
+ panelData.skip += result.data.data.length;
603
+ panelData.hasMore = result.data.data.length === panelData.limit;
604
+ } else {
605
+ // When cid query is used, disable infinite scroll
606
+ panelData.hasMore = false;
607
+ }
608
+
609
+ const lastItem = result.data.data[result.data.data.length - 1];
610
+ if (result.data.data.length === 0 || (lastItem && lastItem._id === panelData.lasIdAvailable)) {
431
611
  LoadingAnimation.spinner.stop(`.panel-placeholder-bottom-${idPanel}`);
432
612
  panelData.hasMore = false;
433
613
  }
@@ -538,7 +718,8 @@ const PanelForm = {
538
718
  loading: false,
539
719
  };
540
720
 
541
- if (cid) this.Data[idPanel].skip = 0;
721
+ // Always reset skip to 0 when reloading (whether cid exists or not)
722
+ this.Data[idPanel].skip = 0;
542
723
 
543
724
  const containerSelector = `.${options.parentIdModal ? 'html-' + options.parentIdModal : 'main-body'}`;
544
725
  htmls(containerSelector, await renderSrrPanelData());
@@ -553,22 +734,26 @@ const PanelForm = {
553
734
  `,
554
735
  );
555
736
 
737
+ const lastOriginItem = this.Data[idPanel].originData[this.Data[idPanel].originData.length - 1];
556
738
  if (
557
739
  !this.Data[idPanel].lasIdAvailable ||
558
- this.Data[idPanel].lasIdAvailable !==
559
- this.Data[idPanel].originData[this.Data[idPanel].originData.length - 1]._id
740
+ !lastOriginItem ||
741
+ this.Data[idPanel].lasIdAvailable !== lastOriginItem._id
560
742
  )
561
743
  LoadingAnimation.spinner.play(`.panel-placeholder-bottom-${idPanel}`, 'dual-ring-mini');
562
744
 
563
745
  const scrollContainerSelector = `.modal-${options.route}`;
564
746
 
747
+ // Always remove old scroll event before setting new one
748
+ if (this.Data[idPanel].removeScrollEvent) {
749
+ this.Data[idPanel].removeScrollEvent();
750
+ }
751
+
565
752
  if (cid) {
566
753
  LoadingAnimation.spinner.stop(`.panel-placeholder-bottom-${idPanel}`);
567
754
  return;
568
755
  }
569
- if (this.Data[idPanel].removeScrollEvent) {
570
- this.Data[idPanel].removeScrollEvent();
571
- }
756
+
572
757
  const { removeEvent } = Scroll.setEvent(scrollContainerSelector, async (payload) => {
573
758
  const panelData = PanelForm.Data[idPanel];
574
759
  if (!panelData) return;