plugin-cluster-manager 1.1.10 → 1.1.13

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 (119) hide show
  1. package/client-v2.d.ts +2 -0
  2. package/client-v2.js +1 -0
  3. package/client.js +1 -0
  4. package/dist/client/index.js +1 -1
  5. package/dist/client-v2/914.5dc1105cf3ada6a6.js +10 -0
  6. package/dist/client-v2/index.js +10 -0
  7. package/dist/externalVersion.js +6 -5
  8. package/dist/locale/en-US.json +138 -28
  9. package/dist/locale/vi-VN.json +139 -28
  10. package/dist/locale/zh-CN.json +140 -28
  11. package/dist/server/actions/cache-monitor.js +301 -0
  12. package/dist/server/actions/cluster-nodes.js +391 -11
  13. package/dist/server/actions/doctor.js +1246 -0
  14. package/dist/server/actions/orchestrator.js +37 -0
  15. package/dist/server/actions/queue-mappings.js +107 -0
  16. package/dist/server/collections/cluster-manager-doctor-runs.js +52 -0
  17. package/dist/server/collections/cluster-manager-doctor.js +44 -0
  18. package/dist/server/collections/worker-queue-mappings.js +106 -0
  19. package/dist/server/hooks/cacheInvalidationHooks.js +81 -0
  20. package/dist/server/middlewares/listMetaCacheMiddleware.js +79 -0
  21. package/dist/server/orchestrator/PackageManager.js +21 -24
  22. package/dist/server/orchestrator/docker-adapter.js +49 -27
  23. package/dist/server/plugin.js +71 -16
  24. package/dist/server/queue-scanner.js +141 -0
  25. package/dist/server/utils/node.js +30 -2
  26. package/dist/server/utils/versionManager.js +91 -0
  27. package/package.json +9 -5
  28. package/server.js +1 -0
  29. package/src/client/AclCacheManager.tsx +292 -287
  30. package/src/client/CacheMonitor.tsx +166 -179
  31. package/src/client/ClusterManagerLayout.tsx +54 -42
  32. package/src/client/ClusterNodes.tsx +698 -418
  33. package/src/client/ContainerOrchestrator.tsx +184 -102
  34. package/src/client/Doctor.tsx +559 -0
  35. package/src/client/NginxCacheManager.tsx +415 -0
  36. package/src/client/PluginOperations.tsx +234 -234
  37. package/src/client/QueueAssignment.tsx +355 -0
  38. package/src/client/TaskManager.tsx +194 -187
  39. package/src/client/WorkflowExecutions.tsx +243 -238
  40. package/src/client/index.tsx +22 -14
  41. package/src/client/utils/clientSafeCache.ts +41 -0
  42. package/src/client/utils/requestDedupInterceptor.ts +213 -0
  43. package/src/client-v2/plugin.tsx +24 -0
  44. package/src/locale/en-US.json +138 -28
  45. package/src/locale/vi-VN.json +139 -28
  46. package/src/locale/zh-CN.json +140 -28
  47. package/src/server/__tests__/doctor.test.ts +53 -0
  48. package/src/server/actions/acl-cache.ts +272 -272
  49. package/src/server/actions/cache-monitor.ts +453 -116
  50. package/src/server/actions/cluster-nodes.ts +878 -378
  51. package/src/server/actions/doctor.ts +1536 -0
  52. package/src/server/actions/orchestrator.ts +54 -2
  53. package/src/server/actions/queue-mappings.ts +94 -0
  54. package/src/server/collections/cluster-manager-doctor-runs.ts +23 -0
  55. package/src/server/collections/cluster-manager-doctor.ts +19 -0
  56. package/src/server/collections/worker-queue-mappings.ts +85 -0
  57. package/src/server/hooks/cacheInvalidationHooks.ts +58 -0
  58. package/src/server/middlewares/listMetaCacheMiddleware.ts +55 -0
  59. package/src/server/orchestrator/PackageManager.ts +20 -24
  60. package/src/server/orchestrator/docker-adapter.ts +74 -37
  61. package/src/server/plugin.ts +347 -270
  62. package/src/server/queue-scanner.ts +154 -0
  63. package/src/server/utils/node.ts +48 -0
  64. package/src/server/utils/versionManager.ts +69 -0
  65. package/dist/client/AclCacheManager.d.ts +0 -2
  66. package/dist/client/CacheMonitor.d.ts +0 -2
  67. package/dist/client/ClusterManagerLayout.d.ts +0 -2
  68. package/dist/client/ClusterNodes.d.ts +0 -2
  69. package/dist/client/ContainerOrchestrator.d.ts +0 -2
  70. package/dist/client/EventQueueMonitor.d.ts +0 -2
  71. package/dist/client/LockMonitor.d.ts +0 -2
  72. package/dist/client/PackageInstaller.d.ts +0 -2
  73. package/dist/client/PluginOperations.d.ts +0 -2
  74. package/dist/client/RedisMonitor.d.ts +0 -2
  75. package/dist/client/TaskManager.d.ts +0 -2
  76. package/dist/client/WorkflowExecutions.d.ts +0 -2
  77. package/dist/client/index.d.ts +0 -5
  78. package/dist/client/utils.d.ts +0 -12
  79. package/dist/index.d.ts +0 -2
  80. package/dist/server/actions/acl-cache.d.ts +0 -53
  81. package/dist/server/actions/cache-monitor.d.ts +0 -23
  82. package/dist/server/actions/cluster-nodes.d.ts +0 -49
  83. package/dist/server/actions/event-queue-monitor.d.ts +0 -13
  84. package/dist/server/actions/lock-monitor.d.ts +0 -19
  85. package/dist/server/actions/orchestrator.d.ts +0 -58
  86. package/dist/server/actions/package-manager.d.ts +0 -6
  87. package/dist/server/actions/plugin-operations.d.ts +0 -6
  88. package/dist/server/actions/redis-monitor.d.ts +0 -12
  89. package/dist/server/actions/tasks.d.ts +0 -7
  90. package/dist/server/actions/workflow-executions.d.ts +0 -7
  91. package/dist/server/adapters/redis-lock-adapter.d.ts +0 -15
  92. package/dist/server/adapters/redis-node-registry.d.ts +0 -12
  93. package/dist/server/adapters/redis-pubsub-adapter.d.ts +0 -16
  94. package/dist/server/collections/app.d.ts +0 -8
  95. package/dist/server/collections/cluster-manager-acl-cache.d.ts +0 -22
  96. package/dist/server/collections/cluster-manager-cache-mgr.d.ts +0 -22
  97. package/dist/server/collections/cluster-manager-cluster.d.ts +0 -22
  98. package/dist/server/collections/cluster-manager-lock.d.ts +0 -22
  99. package/dist/server/collections/cluster-manager-plugins.d.ts +0 -18
  100. package/dist/server/collections/cluster-manager-queue.d.ts +0 -22
  101. package/dist/server/collections/cluster-manager-redis.d.ts +0 -22
  102. package/dist/server/collections/cluster-manager-workflow.d.ts +0 -22
  103. package/dist/server/collections/cluster-manager.d.ts +0 -22
  104. package/dist/server/collections/orchestrator-settings.d.ts +0 -59
  105. package/dist/server/collections/orchestrator-stacks.d.ts +0 -102
  106. package/dist/server/collections/worker-orchestrator.d.ts +0 -22
  107. package/dist/server/collections/worker-packages-configs.d.ts +0 -3
  108. package/dist/server/collections/worker-packages.d.ts +0 -22
  109. package/dist/server/orchestrator/PackageManager.d.ts +0 -39
  110. package/dist/server/orchestrator/docker-adapter.d.ts +0 -41
  111. package/dist/server/orchestrator/index.d.ts +0 -4
  112. package/dist/server/orchestrator/k8s-adapter.d.ts +0 -50
  113. package/dist/server/orchestrator/leader-election.d.ts +0 -48
  114. package/dist/server/orchestrator/types.d.ts +0 -84
  115. package/dist/server/plugin.d.ts +0 -26
  116. package/dist/server/utils/node.d.ts +0 -6
  117. package/dist/server/utils/redis.d.ts +0 -29
  118. package/dist/shared/packages.d.ts +0 -23
  119. /package/{dist/server/index.d.ts → src/client-v2/index.tsx} +0 -0
@@ -40,7 +40,6 @@ __export(cluster_nodes_exports, {
40
40
  readLocalLogs: () => readLocalLogs
41
41
  });
42
42
  module.exports = __toCommonJS(cluster_nodes_exports);
43
- var import_server = require("@nocobase/server");
44
43
  var import_os = __toESM(require("os"));
45
44
  var import_fs = require("fs");
46
45
  var import_path = __toESM(require("path"));
@@ -48,11 +47,158 @@ var import_crypto = __toESM(require("crypto"));
48
47
  var import_redis_node_registry = require("../adapters/redis-node-registry");
49
48
  var import_redis = require("../utils/redis");
50
49
  var import_node = require("../utils/node");
50
+ var import_packages = require("../../shared/packages");
51
51
  const LOG_RESPONSE_KEY_PREFIX = "cluster-manager:log-response:";
52
- const LOG_RESPONSE_TTL = 30;
52
+ const LEGACY_MULTI_APP_PLUGINS = ["multi-app-manager", "multi-app-share-collection"];
53
53
  function sleep(ms) {
54
54
  return new Promise((resolve) => setTimeout(resolve, ms));
55
55
  }
56
+ function normalizeList(value) {
57
+ if (!Array.isArray(value)) return [];
58
+ return Array.from(
59
+ new Set(
60
+ value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
61
+ )
62
+ );
63
+ }
64
+ function normalizePackageMap(packages) {
65
+ return {
66
+ apt: normalizeList(packages == null ? void 0 : packages.apt),
67
+ npm: normalizeList(packages == null ? void 0 : packages.npm),
68
+ python: normalizeList(packages == null ? void 0 : packages.python)
69
+ };
70
+ }
71
+ function parseCustomPackages(value) {
72
+ if (!value) {
73
+ return { python: [], node: [], npm: [] };
74
+ }
75
+ let customValue = value;
76
+ if (typeof customValue === "string") {
77
+ try {
78
+ customValue = JSON.parse(customValue);
79
+ } catch {
80
+ return { python: [], node: [], npm: [] };
81
+ }
82
+ }
83
+ if (!customValue || typeof customValue !== "object" || Array.isArray(customValue)) {
84
+ return { python: [], node: [], npm: [] };
85
+ }
86
+ const custom = customValue;
87
+ return {
88
+ python: normalizeList(custom.python),
89
+ node: normalizeList(custom.node),
90
+ npm: normalizeList(custom.npm)
91
+ };
92
+ }
93
+ function parsePackageWhitelist(status) {
94
+ if (!(status == null ? void 0 : status.packageWhitelist)) {
95
+ return { apt: [], npm: [], python: [] };
96
+ }
97
+ let whitelistValue = status.packageWhitelist;
98
+ if (typeof whitelistValue === "string") {
99
+ try {
100
+ whitelistValue = JSON.parse(whitelistValue);
101
+ } catch {
102
+ return { apt: [], npm: [], python: [] };
103
+ }
104
+ }
105
+ if (!whitelistValue || typeof whitelistValue !== "object" || Array.isArray(whitelistValue)) {
106
+ return { apt: [], npm: [], python: [] };
107
+ }
108
+ const whitelist = whitelistValue;
109
+ const npmPackages = [
110
+ ...Array.isArray(whitelist.npm) ? whitelist.npm : [],
111
+ ...Array.isArray(whitelist.node) ? whitelist.node : []
112
+ ];
113
+ return {
114
+ apt: normalizeList(whitelist.apt),
115
+ npm: normalizeList(npmPackages),
116
+ python: normalizeList(whitelist.python)
117
+ };
118
+ }
119
+ function diffPackages(expected, installed) {
120
+ return {
121
+ apt: expected.apt.filter((pkg) => !installed.apt.includes(pkg)),
122
+ npm: expected.npm.filter((pkg) => !installed.npm.includes(pkg)),
123
+ python: expected.python.filter((pkg) => !installed.python.includes(pkg))
124
+ };
125
+ }
126
+ function hasMissingPackages(packages) {
127
+ return packages.apt.length > 0 || packages.npm.length > 0 || packages.python.length > 0;
128
+ }
129
+ function getErrorMessage(error) {
130
+ return error instanceof Error ? error.message : String(error);
131
+ }
132
+ function getNodeRole(node) {
133
+ return (0, import_node.getNodeRoleFrom)({ workerMode: node.workerMode, isSandbox: node.isSandbox });
134
+ }
135
+ function getReferenceVersion(nodes) {
136
+ var _a;
137
+ const appNode = nodes.find((node) => getNodeRole(node) === "app" && node.appVersion);
138
+ if (appNode == null ? void 0 : appNode.appVersion) {
139
+ return appNode.appVersion;
140
+ }
141
+ const counts = /* @__PURE__ */ new Map();
142
+ for (const node of nodes) {
143
+ if (!node.appVersion) continue;
144
+ counts.set(node.appVersion, (counts.get(node.appVersion) || 0) + 1);
145
+ }
146
+ return ((_a = [...counts.entries()].sort((a, b) => b[1] - a[1])[0]) == null ? void 0 : _a[0]) || null;
147
+ }
148
+ async function getClusterNodes(ctx) {
149
+ var _a, _b;
150
+ const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
151
+ const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
152
+ return registry.getNodes();
153
+ }
154
+ async function getExpectedPackages(ctx) {
155
+ var _a;
156
+ const repo = ctx.db.getRepository("workerPackagesConfigs");
157
+ const config = await ((_a = repo == null ? void 0 : repo.findOne) == null ? void 0 : _a.call(repo));
158
+ if (!config) {
159
+ return normalizePackageMap((0, import_packages.packagesFromConfig)({}));
160
+ }
161
+ const configured = (0, import_packages.packagesFromConfig)({
162
+ aptPackages: config.get("aptPackages"),
163
+ pythonPackages: config.get("pythonPackages"),
164
+ npmPackages: config.get("npmPackages")
165
+ });
166
+ const custom = parseCustomPackages(config.get("customPackages"));
167
+ return normalizePackageMap({
168
+ apt: configured.apt,
169
+ npm: [...configured.npm || [], ...custom.node || [], ...custom.npm || []],
170
+ python: [...configured.python || [], ...custom.python || []]
171
+ });
172
+ }
173
+ async function readPackageStatus(ctx, node) {
174
+ const redis = (0, import_redis.getRedis)(ctx);
175
+ if (!redis) return null;
176
+ const keys = [
177
+ node.id ? `cluster-manager:pkg-status:${node.id}` : null,
178
+ node.hostname ? `orchestrator:pkg-status:${node.hostname}` : null,
179
+ node.name ? `orchestrator:pkg-status:${node.name}` : null
180
+ ].filter(Boolean);
181
+ for (const key of keys) {
182
+ try {
183
+ const raw = await redis.sendCommand(["GET", key]);
184
+ if (raw && typeof raw === "string") {
185
+ return JSON.parse(raw);
186
+ }
187
+ } catch {
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+ async function getApplicationPluginRows(ctx) {
193
+ const repo = ctx.db.getRepository("applicationPlugins");
194
+ if (!repo) return [];
195
+ const rows = await repo.find({ sort: ["name"] });
196
+ return rows.map((row) => row.toJSON());
197
+ }
198
+ function getPayload(ctx) {
199
+ var _a, _b, _c;
200
+ return ctx.action.params.values || ((_b = (_a = ctx.request) == null ? void 0 : _a.body) == null ? void 0 : _b.values) || ((_c = ctx.request) == null ? void 0 : _c.body) || {};
201
+ }
56
202
  async function readLocalLogs(app, maxLines) {
57
203
  const logBasePath = process.env.LOGGER_BASE_PATH || import_path.default.resolve(process.cwd(), "storage", "logs");
58
204
  const appName = process.env.APP_NAME || app.name || "main";
@@ -96,7 +242,7 @@ const clusterActions = {
96
242
  async current(ctx, next) {
97
243
  var _a, _b;
98
244
  const currentMode = process.env.WORKER_MODE || "main";
99
- const isApp = currentMode === "main" || currentMode === "" || currentMode === "app";
245
+ const isApp = !(0, import_node.isWorkerMode)(process.env.WORKER_MODE);
100
246
  if (isApp) {
101
247
  const mem = process.memoryUsage();
102
248
  ctx.body = {
@@ -129,9 +275,7 @@ const clusterActions = {
129
275
  const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
130
276
  const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
131
277
  const nodes = await registry.getNodes();
132
- const appNode = nodes.find(
133
- (n) => n.workerMode === "main" || n.workerMode === "" || n.workerMode === "app"
134
- );
278
+ const appNode = nodes.find((n) => n.workerMode === "main" || n.workerMode === "" || n.workerMode === "app");
135
279
  if (appNode == null ? void 0 : appNode.nodeDetails) {
136
280
  ctx.body = appNode.nodeDetails;
137
281
  } else {
@@ -173,12 +317,8 @@ const clusterActions = {
173
317
  * Returns all known cluster environments/nodes (if discovery adapter supports it)
174
318
  */
175
319
  async list(ctx, next) {
176
- var _a, _b;
177
- const supervisor = import_server.AppSupervisor.getInstance();
178
320
  const environments = [];
179
- const plugin = (_b = (_a = ctx.app.pm) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "plugin-cluster-manager");
180
- const registry = (plugin == null ? void 0 : plugin.nodeRegistry) ?? new import_redis_node_registry.RedisNodeRegistry(ctx.app);
181
- const nodes = await registry.getNodes();
321
+ const nodes = await getClusterNodes(ctx);
182
322
  if (nodes && nodes.length > 0) {
183
323
  for (const env of nodes) {
184
324
  environments.push({
@@ -210,6 +350,168 @@ const clusterActions = {
210
350
  ctx.body = { data: environments, meta: { count: environments.length } };
211
351
  await next();
212
352
  },
353
+ /**
354
+ * GET /clusterManagerCluster:drift
355
+ * Reports version/runtime/package drift across active cluster nodes.
356
+ */
357
+ async drift(ctx, next) {
358
+ var _a, _b;
359
+ const nodes = await getClusterNodes(ctx);
360
+ const referenceVersion = getReferenceVersion(nodes);
361
+ const expectedPackages = await getExpectedPackages(ctx);
362
+ const versionDrifts = nodes.filter((node) => node.status !== "offline").filter((node) => referenceVersion && node.appVersion && node.appVersion !== referenceVersion).map((node) => ({
363
+ id: node.id,
364
+ name: node.name,
365
+ hostname: node.hostname,
366
+ role: getNodeRole(node),
367
+ expectedVersion: referenceVersion,
368
+ actualVersion: node.appVersion
369
+ }));
370
+ const runtimeReference = (_b = (_a = nodes.find((node) => getNodeRole(node) === "app")) == null ? void 0 : _a.nodeDetails) == null ? void 0 : _b.node;
371
+ const runtimeDrifts = runtimeReference ? nodes.filter((node) => node.status !== "offline").filter((node) => {
372
+ var _a2;
373
+ const runtime = (_a2 = node.nodeDetails) == null ? void 0 : _a2.node;
374
+ if (!runtime) return false;
375
+ return runtime.nodeVersion !== runtimeReference.nodeVersion || runtime.platform !== runtimeReference.platform || runtime.arch !== runtimeReference.arch;
376
+ }).map((node) => {
377
+ var _a2, _b2, _c, _d, _e, _f;
378
+ return {
379
+ id: node.id,
380
+ name: node.name,
381
+ hostname: node.hostname,
382
+ role: getNodeRole(node),
383
+ expected: {
384
+ nodeVersion: runtimeReference.nodeVersion,
385
+ platform: runtimeReference.platform,
386
+ arch: runtimeReference.arch
387
+ },
388
+ actual: {
389
+ nodeVersion: (_b2 = (_a2 = node.nodeDetails) == null ? void 0 : _a2.node) == null ? void 0 : _b2.nodeVersion,
390
+ platform: (_d = (_c = node.nodeDetails) == null ? void 0 : _c.node) == null ? void 0 : _d.platform,
391
+ arch: (_f = (_e = node.nodeDetails) == null ? void 0 : _e.node) == null ? void 0 : _f.arch
392
+ }
393
+ };
394
+ }) : [];
395
+ const packageDrifts = [];
396
+ for (const node of nodes.filter((item) => item.status !== "offline" && getNodeRole(item) !== "app")) {
397
+ const status = await readPackageStatus(ctx, node);
398
+ const installedPackages = parsePackageWhitelist(status);
399
+ const missingPackages = diffPackages(expectedPackages, installedPackages);
400
+ const hasPackageStatus = Boolean(status);
401
+ const statusOk = (status == null ? void 0 : status.initStatus) === "succeeded";
402
+ if (!hasPackageStatus || !statusOk || hasMissingPackages(missingPackages)) {
403
+ packageDrifts.push({
404
+ id: node.id,
405
+ name: node.name,
406
+ hostname: node.hostname,
407
+ role: getNodeRole(node),
408
+ status: (status == null ? void 0 : status.initStatus) || "unknown",
409
+ lastInitAt: (status == null ? void 0 : status.lastInitAt) || null,
410
+ missingPackages,
411
+ installedPackages,
412
+ initProgressLog: (status == null ? void 0 : status.initProgressLog) || ""
413
+ });
414
+ }
415
+ }
416
+ ctx.body = {
417
+ healthy: versionDrifts.length === 0 && runtimeDrifts.length === 0 && packageDrifts.length === 0,
418
+ referenceVersion,
419
+ expectedPackages,
420
+ versionDrifts,
421
+ runtimeDrifts,
422
+ packageDrifts,
423
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
424
+ summary: {
425
+ nodes: nodes.length,
426
+ versionDrifts: versionDrifts.length,
427
+ runtimeDrifts: runtimeDrifts.length,
428
+ packageDrifts: packageDrifts.length
429
+ }
430
+ };
431
+ await next();
432
+ },
433
+ /**
434
+ * GET /clusterManagerCluster:legacyDiagnostics
435
+ * Detects deprecated legacy multi-app plugins and leftover application records.
436
+ */
437
+ async legacyDiagnostics(ctx, next) {
438
+ var _a, _b;
439
+ const rows = await getApplicationPluginRows(ctx);
440
+ const plugins = LEGACY_MULTI_APP_PLUGINS.map((name) => {
441
+ var _a2, _b2, _c, _d;
442
+ const row = rows.find((item) => item.name === name || item.packageName === `@nocobase/plugin-${name}`);
443
+ const loaded = Boolean(
444
+ ((_b2 = (_a2 = ctx.app.pm) == null ? void 0 : _a2.get) == null ? void 0 : _b2.call(_a2, name)) || ((_d = (_c = ctx.app.pm) == null ? void 0 : _c.get) == null ? void 0 : _d.call(_c, `@nocobase/plugin-${name}`))
445
+ );
446
+ return {
447
+ name,
448
+ packageName: `@nocobase/plugin-${name}`,
449
+ installed: Boolean(row),
450
+ enabled: Boolean(row == null ? void 0 : row.enabled),
451
+ loaded,
452
+ version: row == null ? void 0 : row.version
453
+ };
454
+ });
455
+ let legacyApplicationCount = 0;
456
+ if ((_b = (_a = ctx.db).hasCollection) == null ? void 0 : _b.call(_a, "applications")) {
457
+ try {
458
+ legacyApplicationCount = await ctx.db.getRepository("applications").count();
459
+ } catch {
460
+ legacyApplicationCount = 0;
461
+ }
462
+ }
463
+ const findings = [];
464
+ const manager = plugins.find((plugin) => plugin.name === "multi-app-manager");
465
+ const shareCollection = plugins.find((plugin) => plugin.name === "multi-app-share-collection");
466
+ const appSupervisor = rows.find(
467
+ (item) => item.name === "app-supervisor" || item.packageName === "@nocobase/plugin-app-supervisor"
468
+ );
469
+ if ((manager == null ? void 0 : manager.enabled) || (manager == null ? void 0 : manager.loaded)) {
470
+ findings.push({
471
+ level: "warning",
472
+ code: "legacy_multi_app_manager_active",
473
+ messageKey: "Deprecated multi-app manager is active. It runs apps in shared process memory and should not be used for production cluster isolation.",
474
+ message: "Deprecated multi-app manager is active. It runs apps in shared process memory and should not be used for production cluster isolation."
475
+ });
476
+ }
477
+ if ((shareCollection == null ? void 0 : shareCollection.enabled) || (shareCollection == null ? void 0 : shareCollection.loaded)) {
478
+ findings.push({
479
+ level: "warning",
480
+ code: "legacy_share_collection_active",
481
+ messageKey: "Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments.",
482
+ message: "Deprecated multi-app share collection is active. Avoid schema/table sharing for new cluster deployments."
483
+ });
484
+ }
485
+ if (legacyApplicationCount > 0) {
486
+ findings.push({
487
+ level: "warning",
488
+ code: "legacy_app_records_found",
489
+ messageKey: "{count} legacy application record(s) were found in the applications collection.",
490
+ messageArgs: { count: legacyApplicationCount },
491
+ message: `${legacyApplicationCount} legacy application record(s) were found in the applications collection.`
492
+ });
493
+ }
494
+ if (!(appSupervisor == null ? void 0 : appSupervisor.enabled)) {
495
+ findings.push({
496
+ level: "info",
497
+ code: "app_supervisor_not_enabled",
498
+ messageKey: "App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins.",
499
+ message: "App Supervisor is not enabled. Use it for new multi-application management instead of deprecated multi-app plugins."
500
+ });
501
+ }
502
+ ctx.body = {
503
+ healthy: findings.every((finding) => finding.level !== "warning"),
504
+ plugins,
505
+ appSupervisor: appSupervisor ? {
506
+ installed: true,
507
+ enabled: Boolean(appSupervisor.enabled),
508
+ version: appSupervisor.version
509
+ } : { installed: false, enabled: false },
510
+ legacyApplicationCount,
511
+ findings
512
+ };
513
+ await next();
514
+ },
213
515
  /**
214
516
  * GET /clusterManagerCluster:health
215
517
  * Health check for all subsystems
@@ -290,6 +592,84 @@ const clusterActions = {
290
592
  }
291
593
  await next();
292
594
  },
595
+ /**
596
+ * POST /clusterManagerCluster:rollingRestart
597
+ * Restarts online nodes one-by-one, optionally filtered by role.
598
+ */
599
+ async rollingRestart(ctx, next) {
600
+ const payload = getPayload(ctx);
601
+ const mode = payload.mode === "soft" ? "soft" : "hard";
602
+ const role = payload.role || "worker";
603
+ const delayMs = Math.min(Math.max(Number(payload.delayMs) || 5e3, 1e3), 6e4);
604
+ const requestedNodeIds = Array.isArray(payload.nodeIds) ? payload.nodeIds.map(String) : [];
605
+ const pubSub = ctx.app.pubSubManager;
606
+ if (!pubSub) {
607
+ ctx.throw(500, "PubSub manager is not initialized. HA requires PUBSUB_ADAPTER_REDIS_URL to be set.");
608
+ }
609
+ const nodes = (await getClusterNodes(ctx)).filter((node) => {
610
+ if (node.status === "offline") return false;
611
+ if (requestedNodeIds.length > 0) return node.id && requestedNodeIds.includes(node.id);
612
+ if (role === "all") return true;
613
+ return getNodeRole(node) === role;
614
+ });
615
+ if (nodes.length === 0) {
616
+ ctx.throw(404, "No online nodes match the rolling restart target.");
617
+ }
618
+ const myNodeId = (0, import_node.getLocalNodeId)(ctx.app);
619
+ const sortedNodes = nodes.sort((a, b) => {
620
+ if (a.id === myNodeId) return 1;
621
+ if (b.id === myNodeId) return -1;
622
+ return String(a.name || a.id).localeCompare(String(b.name || b.id));
623
+ });
624
+ const restartId = import_crypto.default.randomBytes(8).toString("hex");
625
+ const startedAt = Date.now();
626
+ const logger = ctx.app.logger;
627
+ const published = sortedNodes.map((node, index) => ({
628
+ id: node.id,
629
+ name: node.name,
630
+ hostname: node.hostname,
631
+ role: getNodeRole(node),
632
+ mode,
633
+ order: index + 1,
634
+ scheduledDelayMs: index * delayMs,
635
+ scheduledAt: new Date(startedAt + index * delayMs).toISOString()
636
+ }));
637
+ sortedNodes.forEach((node, index) => {
638
+ setTimeout(() => {
639
+ try {
640
+ const publishResult = pubSub.publish(
641
+ "cluster-manager:restart",
642
+ JSON.stringify({
643
+ restartId,
644
+ targetNodeId: node.id,
645
+ hostname: node.hostname,
646
+ mode
647
+ })
648
+ );
649
+ Promise.resolve(publishResult).catch((error) => {
650
+ logger.error(
651
+ `[ClusterManager] Failed to publish rolling restart ${restartId} for ${node.id || node.hostname}: ${getErrorMessage(error)}`
652
+ );
653
+ });
654
+ } catch (error) {
655
+ logger.error(
656
+ `[ClusterManager] Failed to schedule rolling restart ${restartId} for ${node.id || node.hostname}: ${getErrorMessage(error)}`
657
+ );
658
+ }
659
+ }, index * delayMs);
660
+ });
661
+ ctx.body = {
662
+ success: true,
663
+ restartId,
664
+ mode,
665
+ role,
666
+ delayMs,
667
+ scheduled: true,
668
+ estimatedDurationMs: Math.max(0, (sortedNodes.length - 1) * delayMs),
669
+ published
670
+ };
671
+ await next();
672
+ },
293
673
  /**
294
674
  * GET /clusterManagerCluster:logs?targetNodeId=xxx&lines=200
295
675
  *