@underpostnet/underpost 2.97.5 → 2.98.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 (34) hide show
  1. package/.vscode/settings.json +7 -8
  2. package/README.md +2 -2
  3. package/bin/build.js +21 -5
  4. package/bin/deploy.js +1 -0
  5. package/bin/file.js +2 -1
  6. package/bin/util.js +0 -17
  7. package/cli.md +2 -2
  8. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  9. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  10. package/package.json +2 -4
  11. package/scripts/rocky-pwa.sh +200 -0
  12. package/scripts/rocky-setup.sh +12 -39
  13. package/src/api/document/document.model.js +1 -1
  14. package/src/api/document/document.service.js +88 -98
  15. package/src/cli/cluster.js +5 -9
  16. package/src/cli/repository.js +9 -9
  17. package/src/cli/run.js +108 -106
  18. package/src/client/components/core/Auth.js +2 -0
  19. package/src/client/components/core/Content.js +52 -4
  20. package/src/client/components/core/Css.js +30 -0
  21. package/src/client/components/core/FileExplorer.js +699 -42
  22. package/src/client/components/core/Input.js +3 -1
  23. package/src/client/components/core/Panel.js +93 -23
  24. package/src/client/components/core/PanelForm.js +1 -0
  25. package/src/client/components/core/Responsive.js +15 -7
  26. package/src/client/components/core/SearchBox.js +0 -110
  27. package/src/client/components/core/Translate.js +58 -0
  28. package/src/client/services/default/default.management.js +327 -148
  29. package/src/client/sw/default.sw.js +107 -184
  30. package/src/index.js +58 -20
  31. package/src/client/components/core/ObjectLayerEngine.js +0 -1520
  32. package/src/client/components/core/ObjectLayerEngineModal.js +0 -1245
  33. package/src/client/components/core/ObjectLayerEngineViewer.js +0 -880
  34. package/src/server/object-layer.js +0 -335
@@ -31,6 +31,8 @@ const DocumentService = {
31
31
  const Document = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.Document;
32
32
  /** @type {import('../user/user.model.js').UserModel} */
33
33
  const User = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.User;
34
+ /** @type {import('../file/file.model.js').FileModel} */
35
+ const File = DataBaseProvider.instance[`${options.host}${options.path}`].mongoose.models.File;
34
36
 
35
37
  // High-query endpoint for typeahead search
36
38
  //
@@ -38,21 +40,8 @@ const DocumentService = {
38
40
  // - Unauthenticated users: CAN see public documents (isPublic=true) from publishers (admin/moderator)
39
41
  // - Authenticated users: CAN see public documents from publishers + ALL their own documents (public or private)
40
42
  // - No user can see private documents from other users
41
- //
42
- // Search Optimization Strategy:
43
- // 1. Case-insensitive matching ($options: 'i') - maximizes matches across case variations
44
- // 2. Multi-term search - splits "hello world" into ["hello", "world"] and matches ANY term
45
- // 3. Multi-field search - searches BOTH title AND tags array
46
- // 4. OR logic - ANY term matching ANY field counts as a match
47
- // 5. Minimum length: 1 character - allows maximum user flexibility
48
- //
49
- // Example: Query "javascript tutorial"
50
- // - Matches documents with title "JavaScript Guide" (term 1, case-insensitive)
51
- // - Matches documents with tag "tutorial" (term 2, tag match)
52
- // - Matches documents with both terms in different fields
53
- //
43
+ // - PANEL FILTER: Only documents with idPanel tag are returned (prevents out-of-panel context results)
54
44
  if (req.path.startsWith('/public/high') && req.query['q']) {
55
- // Input validation
56
45
  const rawQuery = req.query['q'];
57
46
  if (!rawQuery || typeof rawQuery !== 'string') {
58
47
  throw new Error('Invalid search query');
@@ -68,6 +57,13 @@ const DocumentService = {
68
57
  throw new Error('Search query too long (max 100 characters)');
69
58
  }
70
59
 
60
+ // Get idPanel filter to prevent out-of-panel context results
61
+ const idPanel = req.query['idPanel'];
62
+ if (!idPanel || typeof idPanel !== 'string') {
63
+ logger.warn('Missing idPanel parameter for high-query search');
64
+ return { data: [] };
65
+ }
66
+
71
67
  const publisherUsers = await User.find({ $or: [{ role: 'admin' }, { role: 'moderator' }] });
72
68
 
73
69
  const token = getBearerToken(req);
@@ -97,15 +93,9 @@ const DocumentService = {
97
93
  const queryPayload = {
98
94
  isPublic: true,
99
95
  userId: { $in: publisherUsers.map((p) => p._id) },
96
+ tags: { $in: [idPanel] }, // Filter by idPanel to prevent out-of-panel context results (tags is array)
100
97
  };
101
98
 
102
- logger.info('Special "public" search query', {
103
- authenticated: !!user,
104
- userId: user?._id?.toString(),
105
- role: user?.role,
106
- limit,
107
- });
108
-
109
99
  const data = await Document.find(queryPayload)
110
100
  .sort({ createdAt: -1 })
111
101
  .limit(limit)
@@ -151,21 +141,7 @@ const DocumentService = {
151
141
  .map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // Escape each term for regex safety
152
142
 
153
143
  // Build query based on authentication status
154
- // ============================================
155
- // OPTIMIZED FOR MAXIMUM RESULTS:
156
- // - Multi-term search: matches ANY term
157
- // - Case-insensitive: $options: 'i' flag
158
- // - Multi-field: searches title AND tags
159
- // - Minimum match: ANY term in ANY field = result
160
- //
161
- // Example Query: "javascript react"
162
- // Matches:
163
- // ✓ Document with title "JavaScript Tutorial" (term 1 in title)
164
- // ✓ Document with tag "react" (term 2 in tags)
165
- // ✓ Document with title "Learn React JS" (term 2 in title, case-insensitive)
166
- // ✓ Document with tags ["javascript", "tutorial"] (term 1 in tags)
167
-
168
- // Build search conditions for maximum permissiveness
144
+ // and search conditions for maximum permissiveness
169
145
  const buildSearchConditions = () => {
170
146
  const conditions = [];
171
147
 
@@ -185,14 +161,11 @@ const DocumentService = {
185
161
  // Authenticated user can see:
186
162
  // 1. ALL their own documents (public AND private - no tag restriction)
187
163
  // 2. Public-tagged documents from publishers (admin/moderator only)
188
- //
189
- // MAXIMUM RESULTS STRATEGY:
190
- // - Search by: ANY term matches title OR ANY tag
191
- // - Case-insensitive matching
192
- // - No minimum match threshold beyond 1 character
164
+
193
165
  const searchConditions = buildSearchConditions();
194
166
 
195
167
  queryPayload = {
168
+ tags: { $in: [idPanel] }, // Filter by idPanel to prevent out-of-panel context results (tags is array)
196
169
  $or: [
197
170
  {
198
171
  // Public documents from publishers (admin/moderator)
@@ -210,14 +183,11 @@ const DocumentService = {
210
183
  };
211
184
  } else {
212
185
  // Public/unauthenticated user: ONLY public-tagged documents from publishers (admin/moderator)
213
- //
214
- // MAXIMUM RESULTS STRATEGY for public users:
215
- // - Search by: ANY term matches title OR ANY tag
216
- // - Case-insensitive matching
217
- // - Still respects security: only public docs from trusted publishers
186
+
218
187
  const searchConditions = buildSearchConditions();
219
188
 
220
189
  queryPayload = {
190
+ tags: { $in: [idPanel] }, // Filter by idPanel to prevent out-of-panel context results (tags is array)
221
191
  userId: { $in: publisherUsers.map((p) => p._id) },
222
192
  isPublic: true,
223
193
  $or: searchConditions, // ANY term in title OR tags
@@ -230,19 +200,6 @@ const DocumentService = {
230
200
  return { data: [] };
231
201
  }
232
202
 
233
- // Security audit logging
234
- logger.info('High-query search (OPTIMIZED FOR MAX RESULTS)', {
235
- query: searchQuery.substring(0, 50), // Log only first 50 chars for privacy
236
- terms: searchTerms.length, // Number of search terms
237
- searchStrategy: 'multi-term OR matching, case-insensitive, title+tags',
238
- authenticated: !!user,
239
- userId: user?._id?.toString(),
240
- role: user?.role,
241
- limit,
242
- publishersCount: publisherUsers.length,
243
- timestamp: new Date().toISOString(),
244
- });
245
-
246
203
  const data = await Document.find(queryPayload)
247
204
  .sort({ createdAt: -1 })
248
205
  .limit(limit)
@@ -265,10 +222,7 @@ const DocumentService = {
265
222
  }
266
223
  }
267
224
  // Remove role field from userId before sending to client (all users)
268
- if (filteredDoc.userId && filteredDoc.userId.role) {
269
- const { role, ...userWithoutRole } = filteredDoc.userId;
270
- filteredDoc.userId = userWithoutRole;
271
- }
225
+ delete filteredDoc.userId.role;
272
226
  return filteredDoc;
273
227
  });
274
228
 
@@ -372,18 +326,7 @@ const DocumentService = {
372
326
  };
373
327
  }
374
328
  }
375
- // Security audit logging
376
- logger.info('Public tag search', {
377
- authenticated: !!user,
378
- userId: user?._id?.toString(),
379
- role: user?.role,
380
- requestedTags,
381
- hasPublicTag,
382
- hasCidFilter: !!req.query.cid,
383
- limit,
384
- skip,
385
- publishersCount: publisherUsers.length,
386
- });
329
+
387
330
  // sort in descending (-1) order by length
388
331
  const sort = { createdAt: -1 };
389
332
 
@@ -417,9 +360,11 @@ const DocumentService = {
417
360
  if ((!docObj.isPublic || !isPublisher) && !isOwnDoc) userInfo = undefined;
418
361
  return {
419
362
  ...docObj,
420
- role: undefined,
421
- email: undefined,
422
- userId: userInfo,
363
+ userId: {
364
+ ...userInfo,
365
+ role: undefined,
366
+ email: undefined,
367
+ },
423
368
  tags: DocumentDto.filterPublicTag(docObj.tags),
424
369
  totalCopyShareLinkCount: DocumentDto.getTotalCopyShareLinkCount(doc),
425
370
  };
@@ -430,32 +375,65 @@ const DocumentService = {
430
375
 
431
376
  switch (req.params.id) {
432
377
  default: {
433
- const data = await Document.find({
378
+ // Simple pagination support for FileExplorer
379
+ const limit = req.query.limit ? parseInt(req.query.limit, 10) : 50;
380
+ const skip = req.query.skip ? parseInt(req.query.skip, 10) : 0;
381
+
382
+ // Search filter parameters
383
+ const searchTitle = req.query.searchTitle ? req.query.searchTitle.trim() : '';
384
+ const searchMdFile = req.query.searchMdFile ? req.query.searchMdFile.trim() : '';
385
+ const searchFile = req.query.searchFile ? req.query.searchFile.trim() : '';
386
+
387
+ const query = {
434
388
  userId: req.auth.user._id,
435
389
  ...(req.params.id ? { _id: req.params.id } : undefined),
436
- })
390
+ };
391
+
392
+ // Filter by title
393
+ if (searchTitle) {
394
+ const searchRegex = searchTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
395
+ query.title = { $regex: searchRegex, $options: 'i' };
396
+ }
397
+
398
+ // Filter by markdown file name
399
+ if (searchMdFile) {
400
+ const searchRegex = searchMdFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
401
+ const files = await File.find({ name: { $regex: searchRegex, $options: 'i' } }).select('_id');
402
+ query.mdFileId = { $in: files.map((f) => f._id) };
403
+ }
404
+
405
+ // Filter by generic file name
406
+ if (searchFile) {
407
+ const searchRegex = searchFile.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
408
+ const files = await File.find({ name: { $regex: searchRegex, $options: 'i' } }).select('_id');
409
+ query.fileId = { $in: files.map((f) => f._id) };
410
+ }
411
+
412
+ // Get total count for pagination
413
+ const totalCount = await Document.countDocuments(query);
414
+
415
+ const data = await Document.find(query)
416
+ .sort({ createdAt: -1 })
417
+ .limit(limit)
418
+ .skip(skip)
437
419
  .populate(DocumentDto.populate.file())
438
420
  .populate(DocumentDto.populate.mdFile())
439
421
  .populate(DocumentDto.populate.user());
440
422
 
441
- // Add totalCopyShareLinkCount to each document and filter 'public' from tags
442
- return data.map((doc) => {
443
- const docObj = doc.toObject ? doc.toObject() : doc;
444
-
445
- // Remove role field from userId before sending to client
446
- let userInfo = docObj.userId;
447
- if (userInfo && userInfo.role) {
448
- const { role, ...userWithoutRole } = userInfo;
449
- userInfo = userWithoutRole;
450
- }
451
-
452
- return {
453
- ...docObj,
454
- userId: userInfo,
455
- tags: DocumentDto.filterPublicTag(docObj.tags),
456
- totalCopyShareLinkCount: DocumentDto.getTotalCopyShareLinkCount(doc),
457
- };
458
- });
423
+ return {
424
+ data,
425
+ pagination: {
426
+ totalCount,
427
+ limit,
428
+ skip,
429
+ hasMore: skip + data.length < totalCount,
430
+ search: {
431
+ title: searchTitle,
432
+ mdFile: searchMdFile,
433
+ file: searchFile,
434
+ },
435
+ },
436
+ };
459
437
  }
460
438
  }
461
439
  },
@@ -502,6 +480,18 @@ const DocumentService = {
502
480
  File,
503
481
  });
504
482
 
483
+ // Update file names if provided
484
+ if (req.body.mdFileName && document.mdFileId) {
485
+ await File.findByIdAndUpdate(document.mdFileId, { name: req.body.mdFileName });
486
+ }
487
+ if (req.body.fileName && document.fileId) {
488
+ await File.findByIdAndUpdate(document.fileId, { name: req.body.fileName });
489
+ }
490
+
491
+ // Remove file name fields from body as they are not part of Document schema
492
+ delete req.body.mdFileName;
493
+ delete req.body.fileName;
494
+
505
495
  // Extract 'public' from tags and set isPublic field on update
506
496
  const { isPublic, tags } = DocumentDto.extractPublicFromTags(req.body.tags);
507
497
  req.body.isPublic = isPublic;
@@ -249,15 +249,11 @@ class UnderpostCluster {
249
249
  } else {
250
250
  // Kind cluster initialization (if not using kubeadm or k3s)
251
251
  logger.info('Initializing Kind cluster...');
252
- if (options.full === true || options.dedicatedGpu === true) {
253
- shellExec(`cd ${underpostRoot}/manifests && kind create cluster --config kind-config-cuda.yaml`);
254
- } else {
255
- shellExec(
256
- `cd ${underpostRoot}/manifests && kind create cluster --config kind-config${
257
- options?.dev === true ? '-dev' : ''
258
- }.yaml`,
259
- );
260
- }
252
+ shellExec(
253
+ `cd ${underpostRoot}/manifests && kind create cluster --config kind-config${
254
+ options?.dev === true ? '-dev' : ''
255
+ }.yaml`,
256
+ );
261
257
  UnderpostCluster.API.chown('kind'); // Pass 'kind' to chown
262
258
  }
263
259
  } else if (options.worker === true) {
@@ -31,13 +31,13 @@ class UnderpostRepository {
31
31
  /**
32
32
  * Clones a Git repository from GitHub.
33
33
  * @param {string} [gitUri=`${process.env.GITHUB_USERNAME}/pwa-microservices-template`] - The URI of the GitHub repository (e.g., "username/repository").
34
- * @param {object} [options={ bare: false, g8: false }] - Cloning options.
34
+ * @param {object} [options={ bare: false, G8: false }] - Cloning options.
35
35
  * @param {boolean} [options.bare=false] - If true, performs a bare clone.
36
36
  * @param {boolean} [options.g8=false] - If true, uses the .g8 extension.
37
37
  * @memberof UnderpostRepository
38
38
  */
39
- clone(gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`, options = { bare: false, g8: false }) {
40
- const gExtension = options.g8 === true ? '.g8' : '.git';
39
+ clone(gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`, options = { bare: false, G8: false }) {
40
+ const gExtension = options.G8 === true ? '.g8' : '.git';
41
41
  const repoName = gitUri.split('/').pop();
42
42
  if (fs.existsSync(`./${repoName}`)) fs.removeSync(`./${repoName}`);
43
43
  shellExec(
@@ -53,16 +53,16 @@ class UnderpostRepository {
53
53
  * Pulls updates from a GitHub repository.
54
54
  * @param {string} [repoPath='./'] - The local path to the repository.
55
55
  * @param {string} [gitUri=`${process.env.GITHUB_USERNAME}/pwa-microservices-template`] - The URI of the GitHub repository.
56
- * @param {object} [options={ g8: false }] - Pulling options.
56
+ * @param {object} [options={ G8: false }] - Pulling options.
57
57
  * @param {boolean} [options.g8=false] - If true, uses the .g8 extension.
58
58
  * @memberof UnderpostRepository
59
59
  */
60
60
  pull(
61
61
  repoPath = './',
62
62
  gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`,
63
- options = { g8: false },
63
+ options = { G8: false },
64
64
  ) {
65
- const gExtension = options.g8 === true ? '.g8' : '.git';
65
+ const gExtension = options.G8 === true ? '.g8' : '.git';
66
66
  shellExec(
67
67
  `cd ${repoPath} && git pull https://${
68
68
  process.env.GITHUB_TOKEN ? `${process.env.GITHUB_TOKEN}@` : ''
@@ -201,7 +201,7 @@ class UnderpostRepository {
201
201
  * Pushes commits to a remote GitHub repository.
202
202
  * @param {string} [repoPath='./'] - The local path to the repository.
203
203
  * @param {string} [gitUri=`${process.env.GITHUB_USERNAME}/pwa-microservices-template`] - The URI of the GitHub repository.
204
- * @param {object} [options={ f: false, g8: false }] - Push options.
204
+ * @param {object} [options={ f: false, G8: false }] - Push options.
205
205
  * @param {boolean} [options.f=false] - If true, forces the push.
206
206
  * @param {boolean} [options.g8=false] - If true, uses the .g8 extension.
207
207
  * @memberof UnderpostRepository
@@ -209,9 +209,9 @@ class UnderpostRepository {
209
209
  push(
210
210
  repoPath = './',
211
211
  gitUri = `${process.env.GITHUB_USERNAME}/pwa-microservices-template`,
212
- options = { f: false, g8: false },
212
+ options = { f: false, G8: false },
213
213
  ) {
214
- const gExtension = options.g8 === true || options.G8 === true ? '.g8' : '.git';
214
+ const gExtension = options.G8 === true ? '.g8' : '.git';
215
215
  shellExec(
216
216
  `cd ${repoPath} && git push https://${process.env.GITHUB_TOKEN}@github.com/${gitUri}${gExtension}${
217
217
  options?.f === true ? ' --force' : ''
package/src/cli/run.js CHANGED
@@ -146,111 +146,6 @@ class UnderpostRun {
146
146
  * @memberof UnderpostRun
147
147
  */
148
148
  static RUNNERS = {
149
- /**
150
- * @method spark-template
151
- * @description Creates a new Spark template project using `sbt new` in `/home/dd/spark-template`, initializes a Git repository, and runs `replace_params.sh` and `build.sh`.
152
- * @param {string} path - The input value, identifier, or path for the operation.
153
- * @param {Object} options - The default underpost runner options for customizing workflow
154
- * @memberof UnderpostRun
155
- */
156
- 'spark-template': (path, options = UnderpostRun.DEFAULT_OPTION) => {
157
- const dir = '/home/dd/spark-template';
158
- shellExec(`sudo rm -rf ${dir}`);
159
- shellCd('/home/dd');
160
-
161
- // pbcopy(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8`);
162
- // await read({ prompt: 'Command copy to clipboard, press enter to continue.\n' });
163
- shellExec(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8 '--name=spark-template'`);
164
-
165
- shellCd(dir);
166
-
167
- shellExec(`git init && git add . && git commit -m "Base implementation"`);
168
- shellExec(`chmod +x ./replace_params.sh`);
169
- shellExec(`chmod +x ./build.sh`);
170
-
171
- shellExec(`./replace_params.sh`);
172
- shellExec(`./build.sh`);
173
-
174
- shellCd('/home/dd/engine');
175
- },
176
- /**
177
- * @method rmi
178
- * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
179
- * @param {string} path - The input value, identifier, or path for the operation.
180
- * @param {Object} options - The default underpost runner options for customizing workflow
181
- * @memberof UnderpostRun
182
- */
183
- rmi: (path, options = UnderpostRun.DEFAULT_OPTION) => {
184
- shellExec(`podman rmi $(podman images -qa) --force`);
185
- },
186
- /**
187
- * @method kill
188
- * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
189
- * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
190
- * @param {Object} options - The default underpost runner options for customizing workflow
191
- * @memberof UnderpostRun
192
- */
193
- kill: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
194
- if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
195
- for (const _path of path.split(',')) {
196
- if (_path.split('+')[1]) {
197
- let [port, sumPortOffSet] = _path.split('+');
198
- port = parseInt(port);
199
- sumPortOffSet = parseInt(sumPortOffSet);
200
- for (const sumPort of range(0, sumPortOffSet))
201
- shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`);
202
- } else shellExec(`sudo kill -9 $(lsof -t -i:${_path})`);
203
- }
204
- },
205
- /**
206
- * @method secret
207
- * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
208
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
209
- * @param {Object} options - The default underpost runner options for customizing workflow
210
- * @memberof UnderpostRun
211
- */
212
- secret: (path, options = UnderpostRun.DEFAULT_OPTION) => {
213
- shellExec(
214
- `underpost secret underpost --create-from-file ${
215
- path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`
216
- }`,
217
- );
218
- },
219
- /**
220
- * @method underpost-config
221
- * @description Calls `UnderpostDeploy.API.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
222
- * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
223
- * @param {Object} options - The default underpost runner options for customizing workflow
224
- * @memberof UnderpostRun
225
- */
226
- 'underpost-config': (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
227
- UnderpostDeploy.API.configMap(path ? path : 'production', options.namespace);
228
- },
229
- /**
230
- * @method gpu-env
231
- * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
232
- * @param {string} path - The input value, identifier, or path for the operation.
233
- * @param {Object} options - The default underpost runner options for customizing workflow
234
- * @memberof UnderpostRun
235
- */
236
- 'gpu-env': (path, options = UnderpostRun.DEFAULT_OPTION) => {
237
- shellExec(
238
- `node bin cluster --dev --reset && node bin cluster --dev --dedicated-gpu --kubeadm && kubectl get pods --all-namespaces -o wide -w`,
239
- );
240
- },
241
- /**
242
- * @method tf-gpu-test
243
- * @description Deletes existing `tf-gpu-test-script` ConfigMap and `tf-gpu-test-pod`, and applies the test manifest from `manifests/deployment/tensorflow/tf-gpu-test.yaml`.
244
- * @param {string} path - The input value, identifier, or path for the operation.
245
- * @param {Object} options - The default underpost runner options for customizing workflow
246
- * @memberof UnderpostRun
247
- */
248
- 'tf-gpu-test': (path, options = UnderpostRun.DEFAULT_OPTION) => {
249
- const { underpostRoot, namespace } = options;
250
- shellExec(`kubectl delete configmap tf-gpu-test-script -n ${namespace} --ignore-not-found`);
251
- shellExec(`kubectl delete pod tf-gpu-test-pod -n ${namespace} --ignore-not-found`);
252
- shellExec(`kubectl apply -f ${underpostRoot}/manifests/deployment/tensorflow/tf-gpu-test.yaml -n ${namespace}`);
253
- },
254
149
  /**
255
150
  * @method dev-cluster
256
151
  * @description Resets and deploys a full development cluster including MongoDB, Valkey, exposes services, and updates `/etc/hosts` for local access.
@@ -899,7 +794,7 @@ EOF
899
794
  'host-update': async (path, options = UnderpostRun.DEFAULT_OPTION) => {
900
795
  // const baseCommand = options.dev ? 'node bin' : 'underpost';
901
796
  shellExec(`chmod +x ${options.underpostRoot}/scripts/rocky-setup.sh`);
902
- shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh --yes${options.dev ? ' --install-dev' : ``}`);
797
+ shellExec(`${options.underpostRoot}/scripts/rocky-setup.sh${options.dev ? ' --install-dev' : ``}`);
903
798
  },
904
799
 
905
800
  /**
@@ -1600,6 +1495,113 @@ EOF
1600
1495
  ],
1601
1496
  });
1602
1497
  },
1498
+
1499
+ /**
1500
+ * @method spark-template
1501
+ * @description Creates a new Spark template project using `sbt new` in `/home/dd/spark-template`, initializes a Git repository, and runs `replace_params.sh` and `build.sh`.
1502
+ * @param {string} path - The input value, identifier, or path for the operation.
1503
+ * @param {Object} options - The default underpost runner options for customizing workflow
1504
+ * @memberof UnderpostRun
1505
+ */
1506
+ 'spark-template': (path, options = UnderpostRun.DEFAULT_OPTION) => {
1507
+ const dir = '/home/dd/spark-template';
1508
+ shellExec(`sudo rm -rf ${dir}`);
1509
+ shellCd('/home/dd');
1510
+
1511
+ // pbcopy(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8`);
1512
+ // await read({ prompt: 'Command copy to clipboard, press enter to continue.\n' });
1513
+ shellExec(`cd /home/dd && sbt new ${process.env.GITHUB_USERNAME}/spark-template.g8 '--name=spark-template'`);
1514
+
1515
+ shellCd(dir);
1516
+
1517
+ shellExec(`git init && git add . && git commit -m "Base implementation"`);
1518
+ shellExec(`chmod +x ./replace_params.sh`);
1519
+ shellExec(`chmod +x ./build.sh`);
1520
+
1521
+ shellExec(`./replace_params.sh`);
1522
+ shellExec(`./build.sh`);
1523
+
1524
+ shellCd('/home/dd/engine');
1525
+ },
1526
+ /**
1527
+ * @method rmi
1528
+ * @description Forces the removal of all local Podman images (`podman rmi $(podman images -qa) --force`).
1529
+ * @param {string} path - The input value, identifier, or path for the operation.
1530
+ * @param {Object} options - The default underpost runner options for customizing workflow
1531
+ * @memberof UnderpostRun
1532
+ */
1533
+ rmi: (path, options = UnderpostRun.DEFAULT_OPTION) => {
1534
+ shellExec(`podman rmi $(podman images -qa) --force`);
1535
+ },
1536
+ /**
1537
+ * @method kill
1538
+ * @description Kills processes listening on the specified port(s). If the `path` contains a `+`, it treats it as a range of ports to kill.
1539
+ * @param {string} path - The input value, identifier, or path for the operation (used as the port number).
1540
+ * @param {Object} options - The default underpost runner options for customizing workflow
1541
+ * @memberof UnderpostRun
1542
+ */
1543
+ kill: (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
1544
+ if (options.pid) return shellExec(`sudo kill -9 ${options.pid}`);
1545
+ for (const _path of path.split(',')) {
1546
+ if (_path.split('+')[1]) {
1547
+ let [port, sumPortOffSet] = _path.split('+');
1548
+ port = parseInt(port);
1549
+ sumPortOffSet = parseInt(sumPortOffSet);
1550
+ for (const sumPort of range(0, sumPortOffSet))
1551
+ shellExec(`sudo kill -9 $(lsof -t -i:${parseInt(port) + parseInt(sumPort)})`);
1552
+ } else shellExec(`sudo kill -9 $(lsof -t -i:${_path})`);
1553
+ }
1554
+ },
1555
+ /**
1556
+ * @method secret
1557
+ * @description Creates an Underpost secret named 'underpost' from a file, defaulting to `/home/dd/engine/engine-private/conf/dd-cron/.env.production` if no path is provided.
1558
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional path to the secret file).
1559
+ * @param {Object} options - The default underpost runner options for customizing workflow
1560
+ * @memberof UnderpostRun
1561
+ */
1562
+ secret: (path, options = UnderpostRun.DEFAULT_OPTION) => {
1563
+ const secretPath = path ? path : `/home/dd/engine/engine-private/conf/dd-cron/.env.production`;
1564
+ const command = options.dev
1565
+ ? `node bin secret underpost --create-from-file ${secretPath}`
1566
+ : `underpost secret underpost --create-from-file ${secretPath}`;
1567
+ shellExec(command);
1568
+ },
1569
+ /**
1570
+ * @method underpost-config
1571
+ * @description Calls `UnderpostDeploy.API.configMap` to create a Kubernetes ConfigMap, defaulting to the 'production' environment.
1572
+ * @param {string} path - The input value, identifier, or path for the operation (used as the optional configuration name/environment).
1573
+ * @param {Object} options - The default underpost runner options for customizing workflow
1574
+ * @memberof UnderpostRun
1575
+ */
1576
+ 'underpost-config': (path = '', options = UnderpostRun.DEFAULT_OPTION) => {
1577
+ UnderpostDeploy.API.configMap(path ? path : 'production', options.namespace);
1578
+ },
1579
+ /**
1580
+ * @method gpu-env
1581
+ * @description Sets up a dedicated GPU development environment cluster, resetting and then setting up the cluster with `--dedicated-gpu` and monitoring the pods.
1582
+ * @param {string} path - The input value, identifier, or path for the operation.
1583
+ * @param {Object} options - The default underpost runner options for customizing workflow
1584
+ * @memberof UnderpostRun
1585
+ */
1586
+ 'gpu-env': (path, options = UnderpostRun.DEFAULT_OPTION) => {
1587
+ shellExec(
1588
+ `node bin cluster --dev --reset && node bin cluster --dev --dedicated-gpu --kubeadm && kubectl get pods --all-namespaces -o wide -w`,
1589
+ );
1590
+ },
1591
+ /**
1592
+ * @method tf-gpu-test
1593
+ * @description Deletes existing `tf-gpu-test-script` ConfigMap and `tf-gpu-test-pod`, and applies the test manifest from `manifests/deployment/tensorflow/tf-gpu-test.yaml`.
1594
+ * @param {string} path - The input value, identifier, or path for the operation.
1595
+ * @param {Object} options - The default underpost runner options for customizing workflow
1596
+ * @memberof UnderpostRun
1597
+ */
1598
+ 'tf-gpu-test': (path, options = UnderpostRun.DEFAULT_OPTION) => {
1599
+ const { underpostRoot, namespace } = options;
1600
+ shellExec(`kubectl delete configmap tf-gpu-test-script -n ${namespace} --ignore-not-found`);
1601
+ shellExec(`kubectl delete pod tf-gpu-test-pod -n ${namespace} --ignore-not-found`);
1602
+ shellExec(`kubectl apply -f ${underpostRoot}/manifests/deployment/tensorflow/tf-gpu-test.yaml -n ${namespace}`);
1603
+ },
1604
+
1603
1605
  /**
1604
1606
  * @method deploy-job
1605
1607
  * @description Creates and applies a custom Kubernetes Pod manifest (Job) for running arbitrary commands inside a container image (defaulting to a TensorFlow/NVIDIA image).
@@ -11,6 +11,7 @@ import { loggerFactory } from './Logger.js';
11
11
  import { LogIn } from './LogIn.js';
12
12
  import { LogOut } from './LogOut.js';
13
13
  import { NotificationManager } from './NotificationManager.js';
14
+ import { SearchBox } from './SearchBox.js';
14
15
  import { Translate } from './Translate.js';
15
16
  import { s } from './VanillaJs.js';
16
17
 
@@ -267,6 +268,7 @@ class Auth {
267
268
  try {
268
269
  const result = await UserService.delete({ id: 'logout' });
269
270
  localStorage.removeItem('jwt');
271
+ SearchBox.RecentResults.clear();
270
272
  this.deleteToken();
271
273
  if (this.#refreshTimeout) {
272
274
  clearTimeout(this.#refreshTimeout);