@futdevpro/nts-dynamo 1.15.119 → 1.15.121

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 (75) hide show
  1. package/.dynamo/logs/cicd-pipeline/output.log +1899 -1878
  2. package/.dynamo/logs/cicd-pipeline/status.json +37 -37
  3. package/build/_models/control-models/endpoint-params.control-model.d.ts +5 -0
  4. package/build/_models/control-models/endpoint-params.control-model.d.ts.map +1 -1
  5. package/build/_models/control-models/endpoint-params.control-model.js +41 -11
  6. package/build/_models/control-models/endpoint-params.control-model.js.map +1 -1
  7. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts +6 -1
  8. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.d.ts.map +1 -1
  9. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js +40 -10
  10. package/build/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.js.map +1 -1
  11. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts +9 -0
  12. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.d.ts.map +1 -1
  13. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js +12 -0
  14. package/build/_modules/local-vector-search/_services/lvs-vector-persist.data-service.js.map +1 -1
  15. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts +12 -2
  16. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.d.ts.map +1 -1
  17. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js +54 -7
  18. package/build/_modules/local-vector-search/_services/lvs-vector-pool.control-service.js.map +1 -1
  19. package/build/_modules/server/server-status/server-status.control-service.d.ts +6 -0
  20. package/build/_modules/server/server-status/server-status.control-service.d.ts.map +1 -1
  21. package/build/_modules/server/server-status/server-status.control-service.js +10 -0
  22. package/build/_modules/server/server-status/server-status.control-service.js.map +1 -1
  23. package/build/_modules/server/server-status/server-status.controller.d.ts.map +1 -1
  24. package/build/_modules/server/server-status/server-status.controller.js +26 -0
  25. package/build/_modules/server/server-status/server-status.controller.js.map +1 -1
  26. package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts +11 -1
  27. package/build/_modules/socket/_models/socket-server-service-params.control-model.d.ts.map +1 -1
  28. package/build/_modules/socket/_models/socket-server-service-params.control-model.js +12 -1
  29. package/build/_modules/socket/_models/socket-server-service-params.control-model.js.map +1 -1
  30. package/build/_modules/socket/_services/socket-server.service.d.ts +2 -0
  31. package/build/_modules/socket/_services/socket-server.service.d.ts.map +1 -1
  32. package/build/_modules/socket/_services/socket-server.service.js +2 -0
  33. package/build/_modules/socket/_services/socket-server.service.js.map +1 -1
  34. package/build/_modules/socket/app-extended.server.d.ts.map +1 -1
  35. package/build/_modules/socket/app-extended.server.js +13 -8
  36. package/build/_modules/socket/app-extended.server.js.map +1 -1
  37. package/build/_services/base/db.service.d.ts +14 -0
  38. package/build/_services/base/db.service.d.ts.map +1 -1
  39. package/build/_services/base/db.service.js +27 -0
  40. package/build/_services/base/db.service.js.map +1 -1
  41. package/build/_services/core/api.service.d.ts.map +1 -1
  42. package/build/_services/core/api.service.js +29 -9
  43. package/build/_services/core/api.service.js.map +1 -1
  44. package/build/_services/core/db-stats.service.d.ts +34 -0
  45. package/build/_services/core/db-stats.service.d.ts.map +1 -0
  46. package/build/_services/core/db-stats.service.js +65 -0
  47. package/build/_services/core/db-stats.service.js.map +1 -0
  48. package/build/_services/core/diagnostics.service.d.ts +43 -0
  49. package/build/_services/core/diagnostics.service.d.ts.map +1 -0
  50. package/build/_services/core/diagnostics.service.js +122 -0
  51. package/build/_services/core/diagnostics.service.js.map +1 -0
  52. package/build/index.d.ts +2 -0
  53. package/build/index.d.ts.map +1 -1
  54. package/build/index.js +2 -0
  55. package/build/index.js.map +1 -1
  56. package/package.json +2 -2
  57. package/src/_models/control-models/endpoint-params.control-model.ts +44 -16
  58. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.spec.ts +16 -0
  59. package/src/_modules/local-vector-search/_services/lvs-persistent-vector-pool.control-service.ts +50 -14
  60. package/src/_modules/local-vector-search/_services/lvs-vector-persist.data-service.ts +22 -0
  61. package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.spec.ts +5 -3
  62. package/src/_modules/local-vector-search/_services/lvs-vector-pool.control-service.ts +62 -11
  63. package/src/_modules/server/server-status/server-status.control-service.ts +12 -0
  64. package/src/_modules/server/server-status/server-status.controller.ts +28 -0
  65. package/src/_modules/socket/_models/socket-server-service-params.control-model.spec.ts +23 -0
  66. package/src/_modules/socket/_models/socket-server-service-params.control-model.ts +12 -1
  67. package/src/_modules/socket/_services/socket-server.service.ts +2 -0
  68. package/src/_modules/socket/app-extended.server.ts +15 -9
  69. package/src/_services/base/db.service.ts +39 -1
  70. package/src/_services/core/api.service.spec.ts +47 -0
  71. package/src/_services/core/api.service.ts +32 -12
  72. package/src/_services/core/db-stats.service.ts +104 -0
  73. package/src/_services/core/diagnostics.service.spec.ts +44 -0
  74. package/src/_services/core/diagnostics.service.ts +158 -0
  75. package/src/index.ts +2 -0
@@ -43,4 +43,27 @@ describe('| DyNTS_SocketServerService_Params', () => {
43
43
  expect(instance.port).toBeUndefined();
44
44
  expect(instance.security).toBe(DyNTS_SocketSecurity.open);
45
45
  });
46
+
47
+ // BFR-MASTERPROMPTER-025 — az attach-mód EXPLICIT opt-in, a port önmagában NEM diszkriminátor
48
+ it('| dedicatedPort default FALSE (shared) — port-label önmagában nem vált dedikáltra', () => {
49
+ const instance = new DyNTS_SocketServerService_Params({
50
+ name: 'LegacyPortLabel',
51
+ port: 39047,
52
+ security: DyNTS_SocketSecurity.open,
53
+ });
54
+
55
+ expect(instance.dedicatedPort).toBe(false);
56
+ });
57
+
58
+ it('| dedicatedPort: true explicit opt-in megőrződik', () => {
59
+ const instance = new DyNTS_SocketServerService_Params({
60
+ name: 'DedicatedNotif',
61
+ port: 39157,
62
+ dedicatedPort: true,
63
+ security: DyNTS_SocketSecurity.open,
64
+ });
65
+
66
+ expect(instance.dedicatedPort).toBe(true);
67
+ expect(instance.port).toBe(39157);
68
+ });
46
69
  });
@@ -4,12 +4,22 @@ import { DyNTS_SocketSecurity } from '../_enums/socket-security.enum';
4
4
 
5
5
  /**
6
6
  * Socket service paraméterek.
7
- * A port opcionális: ha nincs megadva, az app-extended a getPortSettings() httpPort/httpsPort értékét használja (ugyanaz a port, mint a HTTP/HTTPS).
7
+ *
8
+ * **BFR-MASTERPROMPTER-025 (attach-mód):** a socket alapból a fő HTTP/HTTPS szerverre attacholódik
9
+ * (SHARED — unified-port modell). Dedikált saját listener CSAK explicit `dedicatedPort: true`-val kérhető
10
+ * (+ `port` kötelező és ≠ fő port). A korábbi implicit inferencia (port≠httpPort → dedikált) csendben
11
+ * regresszálta azokat a unified-appokat, amelyek legacy-label okból adtak portot — ezért az attach-mód
12
+ * mostantól KÓD-EXPLICIT, a `port` önmagában NEM diszkriminátor.
8
13
  */
9
14
  export class DyNTS_SocketServerService_Params<T = any> {
10
15
  name: string;
11
16
  /** Opcionális: ha nincs megadva, az extended app a HTTP/HTTPS portot használja (same port). */
12
17
  port?: number;
18
+ /**
19
+ * EXPLICIT opt-in a dedikált saját listenerre (a fő szerver HELYETT). Default: false = shared.
20
+ * `true` esetén a `port` kötelező és el kell térnie a fő HTTP-porttól.
21
+ */
22
+ dedicatedPort?: boolean;
13
23
  security?: DyNTS_SocketSecurity;
14
24
 
15
25
  constructor(
@@ -17,6 +27,7 @@ export class DyNTS_SocketServerService_Params<T = any> {
17
27
  ) {
18
28
  this.name = set.name;
19
29
  this.port = set.port;
30
+ this.dedicatedPort = set.dedicatedPort ?? false;
20
31
  this.security = set.security ? set.security : DyNTS_global_settings.defaultSocketSecurity;
21
32
  }
22
33
  }
@@ -51,6 +51,8 @@ export abstract class DyNTS_SocketServerService<
51
51
  protected params: DyNTS_SocketServerService_Params;
52
52
  get name(): string { return this.params.name; }
53
53
  get port(): number | undefined { return this.params.port; }
54
+ /** BFR-MASTERPROMPTER-025: explicit dedikált-listener opt-in (default false = shared). */
55
+ get dedicatedPort(): boolean { return this.params.dedicatedPort ?? false; }
54
56
  get security(): DyNTS_SocketSecurity { return this.params.security; }
55
57
  get activeSubscriptionsCount(): number { return this.presences.length; }
56
58
 
@@ -512,15 +512,21 @@ export abstract class DyNTS_AppExtended extends DyNTS_App {
512
512
  if (service.security === DyNTS_SocketSecurity.open) {
513
513
  this.systemControlsExt.httpSocketServer.init = true;
514
514
 
515
- // BFR-FDPTOKENSERVICE-007 (fleet-wide realtime 502): ha a service SAJÁT (dedikált) portot
516
- // kér, ami ELTÉR a fő HTTP-porttól, a socket a SAJÁT portjára listen-el — NEM a fő szerverre
517
- // attacholódik. Enélkül base-server mellett a dedikált notif-port SOSEM kap listenert (a
518
- // socket a fő portra attacholt) → a gateway a notif-porton 502-t ad. A fő-portot MEGOSZTÓ
519
- // socket (port nincs, v. == httpPort) viselkedése VÁLTOZATLAN. A dedikált-ág a már bizonyított
520
- // non-base útvonalat tükrözi (saját Http.Server + listen).
521
- const sharesMainHttpPort: boolean =
522
- service.port === undefined || service.port === this.portSettings.httpPort;
523
- const attachToMain: boolean = useBaseHttpServer && sharesMainHttpPort;
515
+ // BFR-MASTERPROMPTER-025 (a BFR-FDPTOKENSERVICE-007 3a2c381 utó-fix-e): az attach-mód
516
+ // EXPLICIT opt-in (`dedicatedPort: true`), NEM a port-érték implicit inferenciája. A
517
+ // korábbi `port≠httpPort dedikált` inferencia CSENDBEN regresszálta a unified-shared
518
+ // appokat, amelyek legacy-label/notif-konvenció okból adtak portot (MP live-igazolt:
519
+ // realtime DEAD az nts-bump után; adventor/ccap/warbots latens). Mostantól:
520
+ // - default (dedicatedPort hiányzik/false) → SHARED (a szerverre attach, a port-label irreleváns);
521
+ // - dedicatedPort: true → saját Http.Server + listen a service.port-on (a bizonyított
522
+ // non-base útvonal tükre) pl. a token-service szándékosan-dedikált notif-portja.
523
+ if (service.dedicatedPort && (service.port === undefined || service.port === this.portSettings.httpPort)) {
524
+ throw new Error(
525
+ `SOCKET CONFIG ERROR: "${service.name}" dedicatedPort:true requires an explicit ` +
526
+ `port that differs from the main httpPort (${this.portSettings.httpPort}).`
527
+ );
528
+ }
529
+ const attachToMain: boolean = useBaseHttpServer && !service.dedicatedPort;
524
530
  const serverForOpen: Http.Server = attachToMain
525
531
  ? this.httpServer
526
532
  : (useBaseHttpServer
@@ -677,8 +677,46 @@ export class DyNTS_DBService<T extends DyFM_Metadata> {
677
677
  * @param narrowByDependencyIds id
678
678
  * @returns dataList
679
679
  */
680
+ /**
681
+ * BFR-AM-013 — LEAN + projektált + BATCH-elt cursor-scan (OOM-safe nagy collection-ökhöz).
682
+ *
683
+ * A teljes result-set SOSEM él egyszerre a heapen: a Mongo-cursor batch-enként adagolja a
684
+ * PLAIN (lean, nem-Mongoose-doc) objektumokat a `handler`-nek — a Mongoose-doc-boxing tranziens
685
+ * (a 32k-vektoros LVS-hidratálás 93%-heap-spike-jának gyökere) kimarad. A `projection`-nel csak
686
+ * a szükséges mezők jönnek (pl. `{ vectorId: 1, embedding: 1 }` a content-szöveg NÉLKÜL).
687
+ * Visszaadja a feldolgozott dokumentumok számát.
688
+ */
689
+ async findLeanBatched(
690
+ filterBy: DyFM_DBFilter<T>,
691
+ set: {
692
+ projection?: Record<string, number>,
693
+ batchSize?: number,
694
+ handler: (batch: Partial<T>[]) => Promise<void> | void,
695
+ }
696
+ ): Promise<number> {
697
+ const batchSize: number = set.batchSize && set.batchSize > 0 ? set.batchSize : 500;
698
+ const cursor = this.dataModel.find(filterBy, set.projection).lean().cursor();
699
+
700
+ let batch: Partial<T>[] = [];
701
+ let total: number = 0;
702
+
703
+ for await (const doc of cursor) {
704
+ batch.push(doc as Partial<T>);
705
+ total++;
706
+ if (batch.length >= batchSize) {
707
+ await set.handler(batch);
708
+ batch = [];
709
+ }
710
+ }
711
+ if (batch.length) {
712
+ await set.handler(batch);
713
+ }
714
+
715
+ return total;
716
+ }
717
+
680
718
  async searchData(
681
- filterBy: DyFM_DBFilter<T>,
719
+ filterBy: DyFM_DBFilter<T>,
682
720
  narrowByDependencyIds?: string[],
683
721
  narrowByDependencyKey?: string,
684
722
  findDeleted?: boolean
@@ -138,3 +138,50 @@ describe('| DyNTS_ApiService', () => {
138
138
  );
139
139
  });
140
140
  });
141
+
142
+ // BFR-DYNAMOBUILDER-006 — a belső BE-BE URL NEM kerülhet a _message-be (kliens-leak);
143
+ // a confidentialContent-be megy (a response-strip env-függetlenül levágja).
144
+ describe('| DyNTS_ApiService.handleError — internal URL leak guard (BFR-DYNAMOBUILDER-006)', () => {
145
+ const internalUrl: string = 'http://auth-service:39005/api/auth/login';
146
+
147
+ function buildCallParams(): DyNTS_ApiCall_Params {
148
+ return new DyNTS_ApiCall_Params({
149
+ baseUrl: 'http://auth-service:39005/api',
150
+ endpoint: '/auth/login',
151
+ type: DyFM_HttpCallType.post,
152
+ httpOptions: { headers: {}, responseType: DyFM_HttpResponseType.json },
153
+ name: 'loginCall',
154
+ getFullResponse: false,
155
+ });
156
+ }
157
+
158
+ function invokeHandleError(error: unknown): DyFM_Error {
159
+ try {
160
+ DyNTS_ApiService['handleError'](buildCallParams(), error, { pathParams: {}, body: {} }, internalUrl);
161
+ } catch (thrown: unknown) {
162
+ return thrown as DyFM_Error;
163
+ }
164
+ throw new Error('handleError did not throw');
165
+ }
166
+
167
+ it('| HE3 (ECONNREFUSED): a _message NEM tartalmazza a belső URL-t; a confidentialContent igen', () => {
168
+ const thrown: DyFM_Error = invokeHandleError({ code: 'ECONNREFUSED', message: 'connect ECONNREFUSED' });
169
+
170
+ expect(thrown._message).not.toContain('auth-service:39005');
171
+ expect(thrown._message).toContain('loginCall');
172
+ expect(thrown.confidentialContent?.url).toBe(internalUrl);
173
+ });
174
+
175
+ it('| HE4 (status code): a _message NEM tartalmazza a belső endpointot/URL-t', () => {
176
+ const axiosLike = {
177
+ config: {},
178
+ message: 'Request failed with status code 401',
179
+ response: { status: 401 },
180
+ };
181
+ const thrown: DyFM_Error = invokeHandleError(axiosLike);
182
+
183
+ expect(thrown._message).not.toContain('auth-service:39005');
184
+ expect(thrown.confidentialContent?.url).toBe(internalUrl);
185
+ expect(thrown.additionalContent?.callName).toBe('loginCall');
186
+ });
187
+ });
@@ -264,12 +264,19 @@ export class DyNTS_ApiService {
264
264
  ),
265
265
 
266
266
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE1`,
267
- message:
267
+ // BFR-DYNAMOBUILDER-006: a BELSŐ BE-BE URL (Docker-hostname+port) NEM kerülhet a _message-be —
268
+ // non-prod env-en a teljes error kimegy a kliensnek → info-disclosure. A _message csak a
269
+ // hívás NEVÉT hordozza; az URL + callParams a confidentialContent-be megy (error-store-ba
270
+ // perzisztálódik, a response-strip env-FÜGGETLENÜL levágja).
271
+ message:
268
272
  `API call "${callParams.name}" failed on the other end!`+
269
- `\nurl: ${url}` +
270
273
  `\nerror response found: ${errorMsg}`,
271
274
  error: dyfmErrorInAxiosResponse,
272
275
  additionalContent: {
276
+ callName: callParams.name,
277
+ },
278
+ confidentialContent: {
279
+ url: url,
273
280
  callParams: callParams,
274
281
  inputParams: inputParams,
275
282
  response: (error as Axios.AxiosError)?.response,
@@ -287,8 +294,12 @@ export class DyNTS_ApiService {
287
294
 
288
295
  status: 404,
289
296
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE2`,
290
- message: 'DNS error; address cannot be resolved!',
297
+ message: `DNS error; address cannot be resolved! (call: "${callParams.name}")`,
291
298
  additionalContent: {
299
+ callName: callParams.name,
300
+ },
301
+ confidentialContent: {
302
+ url: url,
292
303
  callParams: callParams,
293
304
  inputParams: inputParams,
294
305
  response: (error as Axios.AxiosError)?.response,
@@ -306,11 +317,15 @@ export class DyNTS_ApiService {
306
317
 
307
318
  status: 404,
308
319
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE3`,
309
- message: `Can't connect to the endpoint! \n(${callParams.type} "${url}")`,
320
+ // BFR-DYNAMOBUILDER-006: az URL confidentialContent-be (nem a _message-be) — belső host-leak ellen.
321
+ message: `Can't connect to the endpoint! (call: "${callParams.name}")`,
310
322
  additionalContent: {
323
+ callName: callParams.name,
324
+ },
325
+ confidentialContent: {
326
+ url: url,
311
327
  callParams: callParams,
312
328
  inputParams: inputParams,
313
- url: url,
314
329
  response: (error as Axios.AxiosError)?.response,
315
330
  },
316
331
  });
@@ -325,15 +340,14 @@ export class DyNTS_ApiService {
325
340
  500;
326
341
 
327
342
  if ((error as Axios.AxiosError)?.message?.includes('status code')) {
328
- msg += `"${callParams.name}"` +
329
- `\nendpoint: ${callParams.type} "${callParams.baseUrl}${callParams.endpoint}"` +
330
- `\nurl: ${url}` +
343
+ // BFR-DYNAMOBUILDER-006: a _message CSAK a hívás-nevet + path-param-KULCSOKAT hordozza —
344
+ // a belső endpoint/URL/stack a confidentialContent-be megy (response-ba SOHA).
345
+ msg += ` "${callParams.name}"` +
331
346
  `\npathParams: ${
332
- inputParams?.pathParams ?
333
- Object.keys(inputParams.pathParams).join(', ') :
347
+ inputParams?.pathParams ?
348
+ Object.keys(inputParams.pathParams).join(', ') :
334
349
  '-no path params-'
335
- }` +
336
- `\nenpointStackLocation: ${callParams.stack}`;
350
+ }`;
337
351
  }
338
352
 
339
353
  throw new DyFM_Error({
@@ -346,6 +360,12 @@ export class DyNTS_ApiService {
346
360
  message: msg,
347
361
  errorCode: `${DyNTS_global_settings.systemShortCodeName}|DyNTS-API-HE4`,
348
362
  additionalContent: {
363
+ callName: callParams.name,
364
+ },
365
+ confidentialContent: {
366
+ url: url,
367
+ endpoint: `${callParams.type} "${callParams.baseUrl}${callParams.endpoint}"`,
368
+ endpointStackLocation: callParams.stack,
349
369
  callParams: callParams,
350
370
  inputParams: inputParams,
351
371
  response: (error as Axios.AxiosError)?.response,
@@ -0,0 +1,104 @@
1
+ import * as mongoose from 'mongoose';
2
+
3
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
4
+
5
+ /**
6
+ * BFR-OVERSEER-004 — stats-only DB-diagnosztika (NO direct data read).
7
+ *
8
+ * Per-collection rekordszám + adat-méret + index-méret (a `$collStats` aggregation-stage-en át —
9
+ * metaadat-olvasás, SOHA nem dokumentum-tartalom) + összesített DB-méret. A 2026-06-23 overseer-incidens
10
+ * (3 GB-ra hízott archív-collection, csak kézi mongosh-sal látszott) proaktív early-warningja: minden
11
+ * NTS-szerver default megkapja a `/server/db-stats` endpointon (app-auth-tal gate-elhető).
12
+ */
13
+
14
+ export interface DyNTS_CollectionStats {
15
+ name: string;
16
+ documentCount: number;
17
+ sizeMB: number;
18
+ indexSizeMB: number;
19
+ }
20
+
21
+ export interface DyNTS_DbStats {
22
+ dbName: string;
23
+ collections: DyNTS_CollectionStats[];
24
+ totalSizeMB: number;
25
+ totalIndexSizeMB: number;
26
+ totalDocumentCount: number;
27
+ /** A gyűjtés közben elhasalt collection-ök (never-silent — a kihagyás látható). */
28
+ failedCollections: string[];
29
+ }
30
+
31
+ const BYTES_PER_MB: number = 1024 * 1024;
32
+
33
+ export class DyNTS_DbStats_Util {
34
+
35
+ /**
36
+ * A teljes stats-only pillanatkép. Hibatűrő: egy-egy collection statjának elhasalása NEM
37
+ * borítja a többit (failedCollections-be kerül, warn-nal — no-silent-skip).
38
+ */
39
+ static async collect(connection: mongoose.Connection): Promise<DyNTS_DbStats> {
40
+ const db: typeof connection.db = connection?.db;
41
+ if (!db) {
42
+ return {
43
+ dbName: connection?.name ?? '-no-connection-',
44
+ collections: [],
45
+ totalSizeMB: 0,
46
+ totalIndexSizeMB: 0,
47
+ totalDocumentCount: 0,
48
+ failedCollections: [],
49
+ };
50
+ }
51
+
52
+ const collectionInfos: { name: string }[] = await db.listCollections({}, { nameOnly: true }).toArray();
53
+ const collections: DyNTS_CollectionStats[] = [];
54
+ const failedCollections: string[] = [];
55
+
56
+ for (const info of collectionInfos) {
57
+ try {
58
+ // $collStats storageStats: metaadat-only (size/count/totalIndexSize) — nincs dokumentum-olvasás.
59
+ const statsCursor = db.collection(info.name).aggregate([
60
+ { $collStats: { storageStats: {} } },
61
+ ]);
62
+ const statsDocs: Record<string, unknown>[] = await statsCursor.toArray();
63
+ const storage: Record<string, unknown> =
64
+ (statsDocs[0]?.['storageStats'] ?? {}) as Record<string, unknown>;
65
+
66
+ collections.push({
67
+ name: info.name,
68
+ documentCount: Number(storage['count'] ?? 0),
69
+ sizeMB: DyNTS_DbStats_Util.toMB(Number(storage['size'] ?? 0)),
70
+ indexSizeMB: DyNTS_DbStats_Util.toMB(Number(storage['totalIndexSize'] ?? 0)),
71
+ });
72
+ } catch (error) {
73
+ failedCollections.push(info.name);
74
+ DyFM_Log.warn(`(DyNTS_DbStats) collStats failed for "${info.name}" (non-fatal)`, error);
75
+ }
76
+ }
77
+
78
+ // Méret szerint csökkenő — a bloat-gyanús collection legyen elöl.
79
+ collections.sort((a: DyNTS_CollectionStats, b: DyNTS_CollectionStats): number => b.sizeMB - a.sizeMB);
80
+
81
+ return {
82
+ dbName: connection.name,
83
+ collections: collections,
84
+ totalSizeMB: DyNTS_DbStats_Util.round2(
85
+ collections.reduce((sum: number, c: DyNTS_CollectionStats): number => sum + c.sizeMB, 0)
86
+ ),
87
+ totalIndexSizeMB: DyNTS_DbStats_Util.round2(
88
+ collections.reduce((sum: number, c: DyNTS_CollectionStats): number => sum + c.indexSizeMB, 0)
89
+ ),
90
+ totalDocumentCount: collections.reduce(
91
+ (sum: number, c: DyNTS_CollectionStats): number => sum + c.documentCount, 0
92
+ ),
93
+ failedCollections: failedCollections,
94
+ };
95
+ }
96
+
97
+ static toMB(bytes: number): number {
98
+ return DyNTS_DbStats_Util.round2(bytes / BYTES_PER_MB);
99
+ }
100
+
101
+ static round2(value: number): number {
102
+ return Math.round(value * 100) / 100;
103
+ }
104
+ }
@@ -0,0 +1,44 @@
1
+ import { DyNTS_DbStats_Util } from './db-stats.service';
2
+ import { DyNTS_Diagnostics, DyNTS_DoctorCheck } from './diagnostics.service';
3
+
4
+ /**
5
+ * BFR-AM-015 / BFR-OVERSEER-004 — a diagnosztika PURE részeinek unit-tesztjei
6
+ * (interpretHealth / buildSummary / MB-kerekítés).
7
+ */
8
+ describe('| DyNTS_Diagnostics (pure)', () => {
9
+
10
+ const ok: DyNTS_DoctorCheck = { name: 'a', health: 'ok' };
11
+ const warn: DyNTS_DoctorCheck = { name: 'b', health: 'warn', fixHint: 'do b' };
12
+ const fail: DyNTS_DoctorCheck = { name: 'c', health: 'fail', fixHint: 'do c' };
13
+
14
+ it('| interpretHealth: fail dominál, aztán warn, különben ok', () => {
15
+ expect(DyNTS_Diagnostics.interpretHealth([ ok, ok ])).toBe('ok');
16
+ expect(DyNTS_Diagnostics.interpretHealth([ ok, warn ])).toBe('warn');
17
+ expect(DyNTS_Diagnostics.interpretHealth([ warn, fail, ok ])).toBe('fail');
18
+ expect(DyNTS_Diagnostics.interpretHealth([])).toBe('ok');
19
+ });
20
+
21
+ it('| buildSummary: all-ok → "all checks ok"; nem-ok check-ek fixHint-tel listázva', () => {
22
+ expect(DyNTS_Diagnostics.buildSummary([ ok ])).toBe('all checks ok');
23
+ const summary: string = DyNTS_Diagnostics.buildSummary([ ok, warn, fail ]);
24
+ expect(summary).toContain('b: warn — do b');
25
+ expect(summary).toContain('c: fail — do c');
26
+ expect(summary).not.toContain('a:');
27
+ });
28
+ });
29
+
30
+ describe('| DyNTS_DbStats_Util (pure)', () => {
31
+
32
+ it('| toMB: byte → MB két tizedesre', () => {
33
+ expect(DyNTS_DbStats_Util.toMB(1024 * 1024)).toBe(1);
34
+ expect(DyNTS_DbStats_Util.toMB(1536 * 1024)).toBe(1.5);
35
+ expect(DyNTS_DbStats_Util.toMB(0)).toBe(0);
36
+ });
37
+
38
+ it('| collect: kapcsolat nélkül üres, nem-dobó eredmény', async () => {
39
+ const stats = await DyNTS_DbStats_Util.collect(undefined as never);
40
+ expect(stats.collections).toEqual([]);
41
+ expect(stats.totalSizeMB).toBe(0);
42
+ expect(stats.failedCollections).toEqual([]);
43
+ });
44
+ });
@@ -0,0 +1,158 @@
1
+ import * as mongoose from 'mongoose';
2
+
3
+ import { DyFM_Log } from '@futdevpro/fsm-dynamo';
4
+
5
+ import { DyNTS_DbStats, DyNTS_DbStats_Util } from './db-stats.service';
6
+ import { DyNTS_EventLoopDiag } from './event-loop-diag.service';
7
+ import { DyNTS_MemoryGuard } from './memory-guard.service';
8
+
9
+ /**
10
+ * BFR-AM-015 (+ BFR-OVERSEER-004 aggregátor-fele) — generikus, újrahasználható diagnosztikai
11
+ * aggregátor MINDEN `DyNTS_App`-hoz: a `GET /server/doctor` endpoint forrása.
12
+ *
13
+ * Check-ek (mind SAJÁT try-ban — egy elhasaló check nem borítja a doctort, néma hiba TILOS):
14
+ * - mongo: kapcsolat + ping (readiness-minta);
15
+ * - memory: MemoryGuard pressure-snapshot;
16
+ * - eventLoop: EventLoopDiag snapshot;
17
+ * - dbStats: stats-only collection-méretek + küszöb-warn a bloat-gyanúra.
18
+ * Az app APP-SPECIFIKUS check-eket regisztrálhat (`registerCheck`) — pl. env-validáció,
19
+ * provider-probe (a FAM `FAM_Doctor` mintája bedrock-magként).
20
+ */
21
+
22
+ export interface DyNTS_DoctorCheck {
23
+ name: string;
24
+ health: 'ok' | 'warn' | 'fail';
25
+ detail?: unknown;
26
+ /** Mit tegyen az operátor, ha nem ok (néma hiba tilos — a hint kötelező nem-ok esetén). */
27
+ fixHint?: string;
28
+ }
29
+
30
+ export interface DyNTS_DoctorReport {
31
+ health: 'ok' | 'warn' | 'fail';
32
+ checks: DyNTS_DoctorCheck[];
33
+ summary: string;
34
+ uptimeSec: number;
35
+ }
36
+
37
+ /** App-specifikus check-függvény (a doctor futtatja, saját try-ban). */
38
+ export type DyNTS_DoctorCheckFn = () => Promise<DyNTS_DoctorCheck>;
39
+
40
+ /** Collection-méret warn-küszöb (MB) — e fölött a dbStats-check warn-ra vált. */
41
+ const DEFAULT_COLLECTION_WARN_MB: number = 512;
42
+
43
+ export class DyNTS_Diagnostics {
44
+
45
+ private static readonly customChecks: DyNTS_DoctorCheckFn[] = [];
46
+
47
+ /** App-specifikus check regisztrálása (pl. env-validáció, provider-probe). */
48
+ static registerCheck(check: DyNTS_DoctorCheckFn): void {
49
+ DyNTS_Diagnostics.customChecks.push(check);
50
+ }
51
+
52
+ /** A teljes doctor-riport. SOSEM dob — a hibás check `fail`-ként jelenik meg. */
53
+ static async runDoctor(set: {
54
+ connection: mongoose.Connection,
55
+ collectionWarnMB?: number,
56
+ }): Promise<DyNTS_DoctorReport> {
57
+ const checks: DyNTS_DoctorCheck[] = [];
58
+
59
+ // 1) Mongo-probe (readiness-minta: readyState + admin ping).
60
+ try {
61
+ const readyState: number = set.connection?.readyState ?? 0;
62
+ let pingOk: boolean = false;
63
+ if (readyState === 1) {
64
+ await set.connection.db?.admin().ping();
65
+ pingOk = true;
66
+ }
67
+ checks.push({
68
+ name: 'mongo',
69
+ health: readyState === 1 && pingOk ? 'ok' : 'fail',
70
+ detail: { readyState: readyState, pingOk: pingOk },
71
+ fixHint: readyState === 1 && pingOk ? undefined
72
+ : 'Check MONGO_URL + DB container/network reachability (readyState 1 + ping expected).',
73
+ });
74
+ } catch (error) {
75
+ checks.push({
76
+ name: 'mongo', health: 'fail', detail: String(error),
77
+ fixHint: 'Mongo ping threw — check DB availability/credentials.',
78
+ });
79
+ }
80
+
81
+ // 2) Memória (MemoryGuard pressure).
82
+ try {
83
+ const pressure: unknown = DyNTS_MemoryGuard.getInstance().pressure();
84
+ const level: string = String((pressure as { level?: string })?.level ?? 'ok');
85
+ checks.push({
86
+ name: 'memory',
87
+ health: level === 'critical' ? 'fail' : level === 'warning' ? 'warn' : 'ok',
88
+ detail: pressure,
89
+ fixHint: level === 'ok' ? undefined
90
+ : 'Heap pressure — check collection sizes (/server/db-stats) + recent load; consider retention.',
91
+ });
92
+ } catch (error) {
93
+ checks.push({ name: 'memory', health: 'warn', detail: String(error), fixHint: 'MemoryGuard snapshot failed.' });
94
+ }
95
+
96
+ // 3) Event-loop lag.
97
+ try {
98
+ const snapshot: unknown = DyNTS_EventLoopDiag.getInstance().getSnapshot();
99
+ checks.push({ name: 'eventLoop', health: 'ok', detail: snapshot });
100
+ } catch (error) {
101
+ checks.push({ name: 'eventLoop', health: 'warn', detail: String(error), fixHint: 'EventLoopDiag snapshot failed.' });
102
+ }
103
+
104
+ // 4) DB-stats + bloat-küszöb (BFR-OVERSEER-004 early-warning).
105
+ try {
106
+ const stats: DyNTS_DbStats = await DyNTS_DbStats_Util.collect(set.connection);
107
+ const warnMB: number = set.collectionWarnMB ?? DEFAULT_COLLECTION_WARN_MB;
108
+ const bloated: string[] = stats.collections
109
+ .filter((c): boolean => c.sizeMB + c.indexSizeMB > warnMB)
110
+ .map((c): string => `${c.name} (${c.sizeMB}MB + ${c.indexSizeMB}MB idx)`);
111
+ checks.push({
112
+ name: 'dbStats',
113
+ health: bloated.length ? 'warn' : 'ok',
114
+ detail: { totalSizeMB: stats.totalSizeMB, collectionCount: stats.collections.length, bloated: bloated },
115
+ fixHint: bloated.length
116
+ ? `Collections over ${warnMB}MB — add dataParams.retention (TTL) or prune: ${bloated.join(', ')}`
117
+ : undefined,
118
+ });
119
+ } catch (error) {
120
+ checks.push({ name: 'dbStats', health: 'warn', detail: String(error), fixHint: 'db-stats collection failed.' });
121
+ }
122
+
123
+ // 5) App-specifikus check-ek.
124
+ for (const customCheck of DyNTS_Diagnostics.customChecks) {
125
+ try {
126
+ checks.push(await customCheck());
127
+ } catch (error) {
128
+ DyFM_Log.warn('(DyNTS_Diagnostics) custom check threw (reported as fail)', error);
129
+ checks.push({ name: 'custom', health: 'fail', detail: String(error), fixHint: 'Custom check threw — see detail.' });
130
+ }
131
+ }
132
+
133
+ return {
134
+ health: DyNTS_Diagnostics.interpretHealth(checks),
135
+ checks: checks,
136
+ summary: DyNTS_Diagnostics.buildSummary(checks),
137
+ uptimeSec: Math.round(process.uptime()),
138
+ };
139
+ }
140
+
141
+ /** Aggregált health: bármely fail → fail; különben bármely warn → warn; különben ok. Pure (unit-tesztelt). */
142
+ static interpretHealth(checks: DyNTS_DoctorCheck[]): 'ok' | 'warn' | 'fail' {
143
+ if (checks.some((c: DyNTS_DoctorCheck): boolean => c.health === 'fail')) { return 'fail'; }
144
+ if (checks.some((c: DyNTS_DoctorCheck): boolean => c.health === 'warn')) { return 'warn'; }
145
+ return 'ok';
146
+ }
147
+
148
+ /** Ember-olvasható összegzés a nem-ok check-ekből. Pure (unit-tesztelt). */
149
+ static buildSummary(checks: DyNTS_DoctorCheck[]): string {
150
+ const notOk: DyNTS_DoctorCheck[] = checks.filter((c: DyNTS_DoctorCheck): boolean => c.health !== 'ok');
151
+ if (!notOk.length) {
152
+ return 'all checks ok';
153
+ }
154
+ return notOk
155
+ .map((c: DyNTS_DoctorCheck): string => `${c.name}: ${c.health}${c.fixHint ? ` — ${c.fixHint}` : ''}`)
156
+ .join(' | ');
157
+ }
158
+ }
package/src/index.ts CHANGED
@@ -58,6 +58,8 @@ export * from './_collections/security-headers.util';
58
58
  export * from './_collections/client-safe-error.util';
59
59
  export * from './_collections/load-shed.util';
60
60
  export * from './_services/core/event-loop-diag.service';
61
+ export * from './_services/core/db-stats.service';
62
+ export * from './_services/core/diagnostics.service';
61
63
 
62
64
  // models/CONTROL MODELS
63
65
  export * from './_models/control-models/api-call-params.control-model';