@camstack/addon-pipeline-orchestrator 1.2.636 → 1.2.637
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.
- package/CHANGELOG.md +6 -0
- package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-DwR2APoE.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-3S_aIDgV.mjs} +1 -1
- package/dist/{hostInit-2ejULZGw.mjs → hostInit-DgrSAcMC.mjs} +1 -1
- package/dist/index.js +296 -2
- package/dist/index.mjs +296 -2
- package/dist/remoteEntry.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.637] - 2026-10-28
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Raises an alert when a node joining or leaving leaves cameras that a manual rebalance would move, with a one-click Rebalance now
|
|
15
|
+
|
|
10
16
|
## [1.2.636] - 2026-10-28
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/index.js
CHANGED
|
@@ -17661,9 +17661,21 @@ var AlertSeveritySchema = _enum([
|
|
|
17661
17661
|
"error",
|
|
17662
17662
|
"critical"
|
|
17663
17663
|
]);
|
|
17664
|
+
/**
|
|
17665
|
+
* An action that RUNS something instead of going somewhere: the admin UI calls
|
|
17666
|
+
* the addon's custom action (`addons.custom`) with exactly these fields. The
|
|
17667
|
+
* producer owns the meaning; the UI knows nothing about the addon or the kind.
|
|
17668
|
+
*/
|
|
17669
|
+
var AlertActionInvokeSchema = object({
|
|
17670
|
+
addonId: string().min(1),
|
|
17671
|
+
action: string().min(1),
|
|
17672
|
+
input: record(string(), unknown()).optional()
|
|
17673
|
+
});
|
|
17674
|
+
/** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
|
|
17664
17675
|
var AlertActionSchema = object({
|
|
17665
17676
|
label: string(),
|
|
17666
17677
|
href: string().optional(),
|
|
17678
|
+
invoke: AlertActionInvokeSchema.optional(),
|
|
17667
17679
|
/**
|
|
17668
17680
|
* How `href` is followed. Absent / `app`: an admin-ui route (client-side).
|
|
17669
17681
|
* `server`: a route the HUB serves (an addon HTTP route such as an OAuth
|
|
@@ -96457,8 +96469,17 @@ var NodeStressStatsOutputSchema = object({
|
|
|
96457
96469
|
at: number()
|
|
96458
96470
|
})).readonly()
|
|
96459
96471
|
});
|
|
96472
|
+
var RebalanceNowOutputSchema = object({
|
|
96473
|
+
migrated: number(),
|
|
96474
|
+
skipped: number()
|
|
96475
|
+
});
|
|
96460
96476
|
var pipelineOrchestratorActions = defineCustomActions({
|
|
96461
96477
|
dumpState: customAction(_void(), OrchestratorDiagnosticsSchema),
|
|
96478
|
+
/**
|
|
96479
|
+
* A manual rebalance (margin 0) — the action behind the "Camera placement can
|
|
96480
|
+
* be rebalanced" alert. Closes that alert as completed, or marks it failed.
|
|
96481
|
+
*/
|
|
96482
|
+
rebalanceNow: customAction(_void(), RebalanceNowOutputSchema, { auth: "admin" }),
|
|
96462
96483
|
nodeStressStats: customAction(NodeStressStatsInputSchema, NodeStressStatsOutputSchema, { auth: "admin" })
|
|
96463
96484
|
});
|
|
96464
96485
|
/**
|
|
@@ -99150,6 +99171,8 @@ var PlacementService = class {
|
|
|
99150
99171
|
const talliedLoads = () => loads.map((load) => tallyLoad(load, attachedDelta.get(load.nodeId) ?? 0, deviceDelta.get(load.nodeId)));
|
|
99151
99172
|
let migrated = 0;
|
|
99152
99173
|
const skipped = [];
|
|
99174
|
+
const moves = [];
|
|
99175
|
+
const dryRun = opts?.dryRun === true;
|
|
99153
99176
|
for (const [deviceId, config] of this.deps.ledger.configEntries()) {
|
|
99154
99177
|
if (opts?.onlyDeviceIds !== void 0 && !opts.onlyDeviceIds.has(deviceId)) continue;
|
|
99155
99178
|
if (this.deps.isIdleSessionCamera(deviceId)) continue;
|
|
@@ -99168,7 +99191,7 @@ var PlacementService = class {
|
|
|
99168
99191
|
});
|
|
99169
99192
|
if (!decision) continue;
|
|
99170
99193
|
if (decision.kind === "pending") {
|
|
99171
|
-
this.handlePendingDecision(deviceId, decision.reason);
|
|
99194
|
+
if (!dryRun) this.handlePendingDecision(deviceId, decision.reason);
|
|
99172
99195
|
continue;
|
|
99173
99196
|
}
|
|
99174
99197
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
@@ -99178,6 +99201,16 @@ var PlacementService = class {
|
|
|
99178
99201
|
const targetLoad = tallied.find((l) => l.nodeId === decision.agentNodeId);
|
|
99179
99202
|
if (currentLoad && targetLoad && capacityImprovement(currentLoad, targetLoad, nodeWeights) < minImprovement) continue;
|
|
99180
99203
|
}
|
|
99204
|
+
if (dryRun) {
|
|
99205
|
+
if (current) bumpAttached(current.agentNodeId, -1, config.deviceKey);
|
|
99206
|
+
bumpAttached(decision.agentNodeId, 1, config.deviceKey);
|
|
99207
|
+
moves.push({
|
|
99208
|
+
deviceId,
|
|
99209
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99210
|
+
toNodeId: decision.agentNodeId
|
|
99211
|
+
});
|
|
99212
|
+
continue;
|
|
99213
|
+
}
|
|
99181
99214
|
const placement = await this.resolvePlacement({
|
|
99182
99215
|
deviceId,
|
|
99183
99216
|
nodeId: decision.agentNodeId,
|
|
@@ -99220,11 +99253,28 @@ var PlacementService = class {
|
|
|
99220
99253
|
dispatchGuard.dispose();
|
|
99221
99254
|
}
|
|
99222
99255
|
migrated++;
|
|
99256
|
+
moves.push({
|
|
99257
|
+
deviceId,
|
|
99258
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99259
|
+
toNodeId: decision.agentNodeId
|
|
99260
|
+
});
|
|
99261
|
+
}
|
|
99262
|
+
if (dryRun) {
|
|
99263
|
+
ctx.logger.debug("rebalance: plan", { meta: {
|
|
99264
|
+
minImprovement,
|
|
99265
|
+
wouldMove: moves.length
|
|
99266
|
+
} });
|
|
99267
|
+
return {
|
|
99268
|
+
migrated: 0,
|
|
99269
|
+
skipped,
|
|
99270
|
+
moves
|
|
99271
|
+
};
|
|
99223
99272
|
}
|
|
99224
99273
|
ctx.logger.info("rebalance: migrated cameras", { meta: { migrated } });
|
|
99225
99274
|
return {
|
|
99226
99275
|
migrated,
|
|
99227
|
-
skipped
|
|
99276
|
+
skipped,
|
|
99277
|
+
moves
|
|
99228
99278
|
};
|
|
99229
99279
|
}
|
|
99230
99280
|
getPipelineAssignments() {
|
|
@@ -106915,6 +106965,152 @@ async function buildOrchestratorControllers(deps) {
|
|
|
106915
106965
|
};
|
|
106916
106966
|
}
|
|
106917
106967
|
//#endregion
|
|
106968
|
+
//#region src/rebalance-suggestion.ts
|
|
106969
|
+
var REBALANCE_SUGGESTED_ALERT_ID = "pipeline-orchestrator:rebalance-suggested";
|
|
106970
|
+
var NODE_NOT_USED_ALERT_PREFIX = "pipeline-orchestrator:node-not-used:";
|
|
106971
|
+
/** The alert category the Alert Center files both alerts under. */
|
|
106972
|
+
var ALERT_CATEGORY = "system";
|
|
106973
|
+
var DEFAULT_SUGGESTION_DEBOUNCE_MS = 9e4;
|
|
106974
|
+
var VERB = {
|
|
106975
|
+
joined: "joined",
|
|
106976
|
+
left: "left",
|
|
106977
|
+
capabilities: "changed its pipeline roles"
|
|
106978
|
+
};
|
|
106979
|
+
var RebalanceSuggestion = class {
|
|
106980
|
+
deps;
|
|
106981
|
+
timer = null;
|
|
106982
|
+
lastNote = null;
|
|
106983
|
+
notUsedRaised = /* @__PURE__ */ new Set();
|
|
106984
|
+
disposed = false;
|
|
106985
|
+
constructor(deps) {
|
|
106986
|
+
this.deps = deps;
|
|
106987
|
+
}
|
|
106988
|
+
/** A node event: (re)arm the single evaluation. Forked-process ids are not nodes. */
|
|
106989
|
+
noteNodeEvent(note) {
|
|
106990
|
+
if (this.disposed || note.nodeId.includes("/")) return;
|
|
106991
|
+
this.lastNote = note;
|
|
106992
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
106993
|
+
this.timer = setTimeout(() => {
|
|
106994
|
+
this.timer = null;
|
|
106995
|
+
this.evaluate();
|
|
106996
|
+
}, this.deps.debounceMs);
|
|
106997
|
+
}
|
|
106998
|
+
dispose() {
|
|
106999
|
+
this.disposed = true;
|
|
107000
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
107001
|
+
this.timer = null;
|
|
107002
|
+
}
|
|
107003
|
+
async evaluate() {
|
|
107004
|
+
await this.evaluateSuggestion();
|
|
107005
|
+
await this.evaluateUnusedNodes();
|
|
107006
|
+
}
|
|
107007
|
+
async evaluateSuggestion() {
|
|
107008
|
+
const { logger } = this.deps;
|
|
107009
|
+
let manual;
|
|
107010
|
+
let sweep;
|
|
107011
|
+
try {
|
|
107012
|
+
manual = await this.deps.plan(0);
|
|
107013
|
+
sweep = await this.deps.plan(AUTO_REBALANCE_MIN_IMPROVEMENT);
|
|
107014
|
+
} catch (err) {
|
|
107015
|
+
logger.warn("rebalance suggestion: plan failed — alert left as it is", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
107016
|
+
return;
|
|
107017
|
+
}
|
|
107018
|
+
const sweepIds = new Set(sweep.moves.map((m) => m.deviceId));
|
|
107019
|
+
const onlyManual = manual.moves.filter((m) => !sweepIds.has(m.deviceId));
|
|
107020
|
+
if (onlyManual.length === 0) {
|
|
107021
|
+
logger.debug("rebalance suggestion: nothing beyond the auto sweep — closing the alert", { meta: {
|
|
107022
|
+
manualMoves: manual.moves.length,
|
|
107023
|
+
sweepMoves: sweep.moves.length
|
|
107024
|
+
} });
|
|
107025
|
+
await this.complete(REBALANCE_SUGGESTED_ALERT_ID);
|
|
107026
|
+
return;
|
|
107027
|
+
}
|
|
107028
|
+
const note = this.lastNote;
|
|
107029
|
+
const cause = note === null ? "The cluster changed" : `Node ${note.nodeId} ${VERB[note.kind]}`;
|
|
107030
|
+
const count = manual.moves.length;
|
|
107031
|
+
const now = this.deps.now();
|
|
107032
|
+
logger.info("rebalance suggestion: raising the alert", { meta: {
|
|
107033
|
+
manualMoves: count,
|
|
107034
|
+
beyondSweep: onlyManual.length
|
|
107035
|
+
} });
|
|
107036
|
+
await this.emit({
|
|
107037
|
+
id: REBALANCE_SUGGESTED_ALERT_ID,
|
|
107038
|
+
category: ALERT_CATEGORY,
|
|
107039
|
+
severity: "warning",
|
|
107040
|
+
title: "Camera placement can be rebalanced",
|
|
107041
|
+
message: `${cause}. ${String(count)} ${count === 1 ? "camera" : "cameras"} would move to even out the load; the automatic balancing will not do it on its own.`,
|
|
107042
|
+
action: {
|
|
107043
|
+
label: "Rebalance now",
|
|
107044
|
+
invoke: {
|
|
107045
|
+
addonId: this.deps.addonId,
|
|
107046
|
+
action: "rebalanceNow"
|
|
107047
|
+
}
|
|
107048
|
+
},
|
|
107049
|
+
status: "active",
|
|
107050
|
+
read: false,
|
|
107051
|
+
createdAt: now,
|
|
107052
|
+
updatedAt: now,
|
|
107053
|
+
source: {
|
|
107054
|
+
type: "addon",
|
|
107055
|
+
id: this.deps.addonId
|
|
107056
|
+
}
|
|
107057
|
+
});
|
|
107058
|
+
}
|
|
107059
|
+
async evaluateUnusedNodes() {
|
|
107060
|
+
const enabled = new Set(this.deps.enabledNodes());
|
|
107061
|
+
const unused = new Set(this.deps.runnerNodes().filter((id) => !enabled.has(id)));
|
|
107062
|
+
for (const nodeId of unused) {
|
|
107063
|
+
const id = `${NODE_NOT_USED_ALERT_PREFIX}${nodeId}`;
|
|
107064
|
+
if (this.notUsedRaised.has(id)) continue;
|
|
107065
|
+
this.notUsedRaised.add(id);
|
|
107066
|
+
this.deps.logger.info("rebalance suggestion: node online but not used for detection", { tags: { nodeId } });
|
|
107067
|
+
const now = this.deps.now();
|
|
107068
|
+
await this.emit({
|
|
107069
|
+
id,
|
|
107070
|
+
category: ALERT_CATEGORY,
|
|
107071
|
+
severity: "info",
|
|
107072
|
+
title: "Node is not used for detection",
|
|
107073
|
+
message: `Node ${nodeId} is online but detection is not enabled on it, so no camera will be placed there.`,
|
|
107074
|
+
action: {
|
|
107075
|
+
label: "Open node",
|
|
107076
|
+
href: `/system/pipeline?view=nodes&node=${encodeURIComponent(nodeId)}`,
|
|
107077
|
+
navigation: "app"
|
|
107078
|
+
},
|
|
107079
|
+
status: "active",
|
|
107080
|
+
read: false,
|
|
107081
|
+
createdAt: now,
|
|
107082
|
+
updatedAt: now,
|
|
107083
|
+
source: {
|
|
107084
|
+
type: "node",
|
|
107085
|
+
id: nodeId
|
|
107086
|
+
}
|
|
107087
|
+
});
|
|
107088
|
+
}
|
|
107089
|
+
for (const id of [...this.notUsedRaised]) {
|
|
107090
|
+
const nodeId = id.slice(36);
|
|
107091
|
+
if (unused.has(nodeId)) continue;
|
|
107092
|
+
this.notUsedRaised.delete(id);
|
|
107093
|
+
await this.complete(id);
|
|
107094
|
+
}
|
|
107095
|
+
}
|
|
107096
|
+
async emit(alert) {
|
|
107097
|
+
await this.deps.alerts.emit(alert).catch((err) => {
|
|
107098
|
+
this.deps.logger.warn("rebalance suggestion: could not raise an alert", { meta: {
|
|
107099
|
+
alertId: alert.id,
|
|
107100
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107101
|
+
} });
|
|
107102
|
+
});
|
|
107103
|
+
}
|
|
107104
|
+
async complete(alertId) {
|
|
107105
|
+
await this.deps.alerts.complete(alertId).catch((err) => {
|
|
107106
|
+
this.deps.logger.debug("rebalance suggestion: close failed (alert may not exist)", { meta: {
|
|
107107
|
+
alertId,
|
|
107108
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107109
|
+
} });
|
|
107110
|
+
});
|
|
107111
|
+
}
|
|
107112
|
+
};
|
|
107113
|
+
//#endregion
|
|
106918
107114
|
//#region src/settings-ui-schemas.ts
|
|
106919
107115
|
/** Build the addon-level schema sections (cluster roles + crop + balancer + failover). */
|
|
106920
107116
|
function buildGlobalSettingsSections(options) {
|
|
@@ -108711,6 +108907,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108711
108907
|
* Constructed right after `loadShed`. Nulled at the end of `onShutdown`.
|
|
108712
108908
|
*/
|
|
108713
108909
|
placement = null;
|
|
108910
|
+
/** The debounced "placement can be rebalanced" evaluation; see `rebalance-suggestion.ts`. */
|
|
108911
|
+
rebalanceSuggestion = null;
|
|
108912
|
+
unsubSuggestionEvents = [];
|
|
108714
108913
|
/** EventBus unsubscribes for agent online/offline events. */
|
|
108715
108914
|
unsubAgentOnline = null;
|
|
108716
108915
|
unsubAgentOffline = null;
|
|
@@ -108984,6 +109183,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108984
109183
|
this.localNodeId = controllers.localNodeId;
|
|
108985
109184
|
this.unsubAgentOnline = controllers.unsubAgentOnline;
|
|
108986
109185
|
this.unsubAgentOffline = controllers.unsubAgentOffline;
|
|
109186
|
+
this.startRebalanceSuggestion();
|
|
108987
109187
|
this.unsubOrchestratorSubscriptions = controllers.unsubOrchestratorSubscriptions;
|
|
108988
109188
|
this.pendingRetryTimer = controllers.pendingRetryTimer;
|
|
108989
109189
|
this.autoRebalanceTimer = controllers.autoRebalanceTimer;
|
|
@@ -109027,6 +109227,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109027
109227
|
customActions: pipelineOrchestratorActions,
|
|
109028
109228
|
actionHandlers: {
|
|
109029
109229
|
dumpState: async () => this.dumpDiagnostics(),
|
|
109230
|
+
rebalanceNow: async () => this.rebalanceNow(),
|
|
109030
109231
|
nodeStressStats: async (input) => {
|
|
109031
109232
|
const lts = this.nodeStressLts;
|
|
109032
109233
|
const moves = this.nodeStress?.historyView() ?? [];
|
|
@@ -109109,6 +109310,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109109
109310
|
this.detectionWiring = null;
|
|
109110
109311
|
this.unsubAgentOffline?.();
|
|
109111
109312
|
this.unsubAgentOffline = null;
|
|
109313
|
+
this.stopRebalanceSuggestion();
|
|
109112
109314
|
this.effectiveSharesService?.stop();
|
|
109113
109315
|
this.unsubAgentOnline?.();
|
|
109114
109316
|
this.unsubAgentOnline = null;
|
|
@@ -109175,6 +109377,94 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109175
109377
|
async rebalance(opts) {
|
|
109176
109378
|
return this.placement.rebalance(opts);
|
|
109177
109379
|
}
|
|
109380
|
+
/**
|
|
109381
|
+
* The `rebalanceNow` custom action: a manual rebalance, and the verdict on
|
|
109382
|
+
* the suggestion alert that offered it. Every branch is logged.
|
|
109383
|
+
*/
|
|
109384
|
+
async rebalanceNow() {
|
|
109385
|
+
const alertId = REBALANCE_SUGGESTED_ALERT_ID;
|
|
109386
|
+
try {
|
|
109387
|
+
const result = await this.rebalance({ minImprovement: 0 });
|
|
109388
|
+
const message = `${String(result.migrated)} ${result.migrated === 1 ? "camera" : "cameras"} moved`;
|
|
109389
|
+
this.ctx.logger.info("rebalanceNow: done", { meta: {
|
|
109390
|
+
migrated: result.migrated,
|
|
109391
|
+
skipped: result.skipped.length
|
|
109392
|
+
} });
|
|
109393
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109394
|
+
alertId,
|
|
109395
|
+
patch: {
|
|
109396
|
+
status: "completed",
|
|
109397
|
+
message,
|
|
109398
|
+
updatedAt: Date.now()
|
|
109399
|
+
}
|
|
109400
|
+
}).catch((err) => this.ctx.logger.warn("rebalanceNow: could not close the suggestion alert", { meta: { error: errMsg$2(err) } }));
|
|
109401
|
+
return {
|
|
109402
|
+
migrated: result.migrated,
|
|
109403
|
+
skipped: result.skipped.length
|
|
109404
|
+
};
|
|
109405
|
+
} catch (err) {
|
|
109406
|
+
const reason = errMsg$2(err);
|
|
109407
|
+
this.ctx.logger.error("rebalanceNow: failed", { meta: { error: reason } });
|
|
109408
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109409
|
+
alertId,
|
|
109410
|
+
patch: {
|
|
109411
|
+
status: "failed",
|
|
109412
|
+
message: `Rebalance failed: ${reason}`,
|
|
109413
|
+
updatedAt: Date.now()
|
|
109414
|
+
}
|
|
109415
|
+
}).catch((e) => this.ctx.logger.warn("rebalanceNow: could not mark the suggestion alert failed", { meta: { error: errMsg$2(e) } }));
|
|
109416
|
+
throw err;
|
|
109417
|
+
}
|
|
109418
|
+
}
|
|
109419
|
+
startRebalanceSuggestion() {
|
|
109420
|
+
const ctx = this.ctx;
|
|
109421
|
+
const suggestion = new RebalanceSuggestion({
|
|
109422
|
+
addonId: "pipeline-orchestrator",
|
|
109423
|
+
logger: ctx.logger,
|
|
109424
|
+
debounceMs: DEFAULT_SUGGESTION_DEBOUNCE_MS,
|
|
109425
|
+
now: () => Date.now(),
|
|
109426
|
+
plan: (minImprovement) => this.rebalance({
|
|
109427
|
+
minImprovement,
|
|
109428
|
+
dryRun: true
|
|
109429
|
+
}),
|
|
109430
|
+
enabledNodes: () => this.topology?.enabledNodes ?? [],
|
|
109431
|
+
runnerNodes: () => this.topology?.collectRunnerNodeIds() ?? [],
|
|
109432
|
+
alerts: {
|
|
109433
|
+
emit: (alert) => ctx.api.alerts.emit.mutate(alert),
|
|
109434
|
+
complete: (alertId) => ctx.api.alerts.update.mutate({
|
|
109435
|
+
alertId,
|
|
109436
|
+
patch: {
|
|
109437
|
+
status: "completed",
|
|
109438
|
+
updatedAt: Date.now()
|
|
109439
|
+
}
|
|
109440
|
+
})
|
|
109441
|
+
}
|
|
109442
|
+
});
|
|
109443
|
+
this.rebalanceSuggestion = suggestion;
|
|
109444
|
+
this.unsubSuggestionEvents = [
|
|
109445
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOnline }, (event) => suggestion.noteNodeEvent({
|
|
109446
|
+
kind: "joined",
|
|
109447
|
+
nodeId: event.data.agentId
|
|
109448
|
+
})),
|
|
109449
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOffline }, (event) => suggestion.noteNodeEvent({
|
|
109450
|
+
kind: "left",
|
|
109451
|
+
nodeId: event.data.agentId
|
|
109452
|
+
})),
|
|
109453
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentUnregistered }, (event) => {
|
|
109454
|
+
const agentId = event.data["agentId"];
|
|
109455
|
+
if (typeof agentId === "string") suggestion.noteNodeEvent({
|
|
109456
|
+
kind: "left",
|
|
109457
|
+
nodeId: agentId
|
|
109458
|
+
});
|
|
109459
|
+
})
|
|
109460
|
+
];
|
|
109461
|
+
}
|
|
109462
|
+
stopRebalanceSuggestion() {
|
|
109463
|
+
for (const unsub of this.unsubSuggestionEvents) unsub();
|
|
109464
|
+
this.unsubSuggestionEvents = [];
|
|
109465
|
+
this.rebalanceSuggestion?.dispose();
|
|
109466
|
+
this.rebalanceSuggestion = null;
|
|
109467
|
+
}
|
|
109178
109468
|
getPipelineAssignments() {
|
|
109179
109469
|
return this.placement.getPipelineAssignments();
|
|
109180
109470
|
}
|
|
@@ -109530,6 +109820,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109530
109820
|
this.topology.addKnownRunnerNode(input.agentNodeId);
|
|
109531
109821
|
this.reconcile.schedulePendingRetry();
|
|
109532
109822
|
if (input.detect !== void 0) this.noteShareChange(`node-detect:${input.agentNodeId}`);
|
|
109823
|
+
this.rebalanceSuggestion?.noteNodeEvent({
|
|
109824
|
+
kind: "capabilities",
|
|
109825
|
+
nodeId: input.agentNodeId
|
|
109826
|
+
});
|
|
109533
109827
|
return { success: true };
|
|
109534
109828
|
}
|
|
109535
109829
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -17633,9 +17633,21 @@ var AlertSeveritySchema = _enum([
|
|
|
17633
17633
|
"error",
|
|
17634
17634
|
"critical"
|
|
17635
17635
|
]);
|
|
17636
|
+
/**
|
|
17637
|
+
* An action that RUNS something instead of going somewhere: the admin UI calls
|
|
17638
|
+
* the addon's custom action (`addons.custom`) with exactly these fields. The
|
|
17639
|
+
* producer owns the meaning; the UI knows nothing about the addon or the kind.
|
|
17640
|
+
*/
|
|
17641
|
+
var AlertActionInvokeSchema = object({
|
|
17642
|
+
addonId: string().min(1),
|
|
17643
|
+
action: string().min(1),
|
|
17644
|
+
input: record(string(), unknown()).optional()
|
|
17645
|
+
});
|
|
17646
|
+
/** An action has `href` (a link) OR `invoke` (a button that runs a custom action). */
|
|
17636
17647
|
var AlertActionSchema = object({
|
|
17637
17648
|
label: string(),
|
|
17638
17649
|
href: string().optional(),
|
|
17650
|
+
invoke: AlertActionInvokeSchema.optional(),
|
|
17639
17651
|
/**
|
|
17640
17652
|
* How `href` is followed. Absent / `app`: an admin-ui route (client-side).
|
|
17641
17653
|
* `server`: a route the HUB serves (an addon HTTP route such as an OAuth
|
|
@@ -96429,8 +96441,17 @@ var NodeStressStatsOutputSchema = object({
|
|
|
96429
96441
|
at: number()
|
|
96430
96442
|
})).readonly()
|
|
96431
96443
|
});
|
|
96444
|
+
var RebalanceNowOutputSchema = object({
|
|
96445
|
+
migrated: number(),
|
|
96446
|
+
skipped: number()
|
|
96447
|
+
});
|
|
96432
96448
|
var pipelineOrchestratorActions = defineCustomActions({
|
|
96433
96449
|
dumpState: customAction(_void(), OrchestratorDiagnosticsSchema),
|
|
96450
|
+
/**
|
|
96451
|
+
* A manual rebalance (margin 0) — the action behind the "Camera placement can
|
|
96452
|
+
* be rebalanced" alert. Closes that alert as completed, or marks it failed.
|
|
96453
|
+
*/
|
|
96454
|
+
rebalanceNow: customAction(_void(), RebalanceNowOutputSchema, { auth: "admin" }),
|
|
96434
96455
|
nodeStressStats: customAction(NodeStressStatsInputSchema, NodeStressStatsOutputSchema, { auth: "admin" })
|
|
96435
96456
|
});
|
|
96436
96457
|
/**
|
|
@@ -99122,6 +99143,8 @@ var PlacementService = class {
|
|
|
99122
99143
|
const talliedLoads = () => loads.map((load) => tallyLoad(load, attachedDelta.get(load.nodeId) ?? 0, deviceDelta.get(load.nodeId)));
|
|
99123
99144
|
let migrated = 0;
|
|
99124
99145
|
const skipped = [];
|
|
99146
|
+
const moves = [];
|
|
99147
|
+
const dryRun = opts?.dryRun === true;
|
|
99125
99148
|
for (const [deviceId, config] of this.deps.ledger.configEntries()) {
|
|
99126
99149
|
if (opts?.onlyDeviceIds !== void 0 && !opts.onlyDeviceIds.has(deviceId)) continue;
|
|
99127
99150
|
if (this.deps.isIdleSessionCamera(deviceId)) continue;
|
|
@@ -99140,7 +99163,7 @@ var PlacementService = class {
|
|
|
99140
99163
|
});
|
|
99141
99164
|
if (!decision) continue;
|
|
99142
99165
|
if (decision.kind === "pending") {
|
|
99143
|
-
this.handlePendingDecision(deviceId, decision.reason);
|
|
99166
|
+
if (!dryRun) this.handlePendingDecision(deviceId, decision.reason);
|
|
99144
99167
|
continue;
|
|
99145
99168
|
}
|
|
99146
99169
|
if (current && current.agentNodeId === decision.agentNodeId) continue;
|
|
@@ -99150,6 +99173,16 @@ var PlacementService = class {
|
|
|
99150
99173
|
const targetLoad = tallied.find((l) => l.nodeId === decision.agentNodeId);
|
|
99151
99174
|
if (currentLoad && targetLoad && capacityImprovement(currentLoad, targetLoad, nodeWeights) < minImprovement) continue;
|
|
99152
99175
|
}
|
|
99176
|
+
if (dryRun) {
|
|
99177
|
+
if (current) bumpAttached(current.agentNodeId, -1, config.deviceKey);
|
|
99178
|
+
bumpAttached(decision.agentNodeId, 1, config.deviceKey);
|
|
99179
|
+
moves.push({
|
|
99180
|
+
deviceId,
|
|
99181
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99182
|
+
toNodeId: decision.agentNodeId
|
|
99183
|
+
});
|
|
99184
|
+
continue;
|
|
99185
|
+
}
|
|
99153
99186
|
const placement = await this.resolvePlacement({
|
|
99154
99187
|
deviceId,
|
|
99155
99188
|
nodeId: decision.agentNodeId,
|
|
@@ -99192,11 +99225,28 @@ var PlacementService = class {
|
|
|
99192
99225
|
dispatchGuard.dispose();
|
|
99193
99226
|
}
|
|
99194
99227
|
migrated++;
|
|
99228
|
+
moves.push({
|
|
99229
|
+
deviceId,
|
|
99230
|
+
fromNodeId: current?.agentNodeId ?? null,
|
|
99231
|
+
toNodeId: decision.agentNodeId
|
|
99232
|
+
});
|
|
99233
|
+
}
|
|
99234
|
+
if (dryRun) {
|
|
99235
|
+
ctx.logger.debug("rebalance: plan", { meta: {
|
|
99236
|
+
minImprovement,
|
|
99237
|
+
wouldMove: moves.length
|
|
99238
|
+
} });
|
|
99239
|
+
return {
|
|
99240
|
+
migrated: 0,
|
|
99241
|
+
skipped,
|
|
99242
|
+
moves
|
|
99243
|
+
};
|
|
99195
99244
|
}
|
|
99196
99245
|
ctx.logger.info("rebalance: migrated cameras", { meta: { migrated } });
|
|
99197
99246
|
return {
|
|
99198
99247
|
migrated,
|
|
99199
|
-
skipped
|
|
99248
|
+
skipped,
|
|
99249
|
+
moves
|
|
99200
99250
|
};
|
|
99201
99251
|
}
|
|
99202
99252
|
getPipelineAssignments() {
|
|
@@ -106887,6 +106937,152 @@ async function buildOrchestratorControllers(deps) {
|
|
|
106887
106937
|
};
|
|
106888
106938
|
}
|
|
106889
106939
|
//#endregion
|
|
106940
|
+
//#region src/rebalance-suggestion.ts
|
|
106941
|
+
var REBALANCE_SUGGESTED_ALERT_ID = "pipeline-orchestrator:rebalance-suggested";
|
|
106942
|
+
var NODE_NOT_USED_ALERT_PREFIX = "pipeline-orchestrator:node-not-used:";
|
|
106943
|
+
/** The alert category the Alert Center files both alerts under. */
|
|
106944
|
+
var ALERT_CATEGORY = "system";
|
|
106945
|
+
var DEFAULT_SUGGESTION_DEBOUNCE_MS = 9e4;
|
|
106946
|
+
var VERB = {
|
|
106947
|
+
joined: "joined",
|
|
106948
|
+
left: "left",
|
|
106949
|
+
capabilities: "changed its pipeline roles"
|
|
106950
|
+
};
|
|
106951
|
+
var RebalanceSuggestion = class {
|
|
106952
|
+
deps;
|
|
106953
|
+
timer = null;
|
|
106954
|
+
lastNote = null;
|
|
106955
|
+
notUsedRaised = /* @__PURE__ */ new Set();
|
|
106956
|
+
disposed = false;
|
|
106957
|
+
constructor(deps) {
|
|
106958
|
+
this.deps = deps;
|
|
106959
|
+
}
|
|
106960
|
+
/** A node event: (re)arm the single evaluation. Forked-process ids are not nodes. */
|
|
106961
|
+
noteNodeEvent(note) {
|
|
106962
|
+
if (this.disposed || note.nodeId.includes("/")) return;
|
|
106963
|
+
this.lastNote = note;
|
|
106964
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
106965
|
+
this.timer = setTimeout(() => {
|
|
106966
|
+
this.timer = null;
|
|
106967
|
+
this.evaluate();
|
|
106968
|
+
}, this.deps.debounceMs);
|
|
106969
|
+
}
|
|
106970
|
+
dispose() {
|
|
106971
|
+
this.disposed = true;
|
|
106972
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
106973
|
+
this.timer = null;
|
|
106974
|
+
}
|
|
106975
|
+
async evaluate() {
|
|
106976
|
+
await this.evaluateSuggestion();
|
|
106977
|
+
await this.evaluateUnusedNodes();
|
|
106978
|
+
}
|
|
106979
|
+
async evaluateSuggestion() {
|
|
106980
|
+
const { logger } = this.deps;
|
|
106981
|
+
let manual;
|
|
106982
|
+
let sweep;
|
|
106983
|
+
try {
|
|
106984
|
+
manual = await this.deps.plan(0);
|
|
106985
|
+
sweep = await this.deps.plan(AUTO_REBALANCE_MIN_IMPROVEMENT);
|
|
106986
|
+
} catch (err) {
|
|
106987
|
+
logger.warn("rebalance suggestion: plan failed — alert left as it is", { meta: { error: err instanceof Error ? err.message : String(err) } });
|
|
106988
|
+
return;
|
|
106989
|
+
}
|
|
106990
|
+
const sweepIds = new Set(sweep.moves.map((m) => m.deviceId));
|
|
106991
|
+
const onlyManual = manual.moves.filter((m) => !sweepIds.has(m.deviceId));
|
|
106992
|
+
if (onlyManual.length === 0) {
|
|
106993
|
+
logger.debug("rebalance suggestion: nothing beyond the auto sweep — closing the alert", { meta: {
|
|
106994
|
+
manualMoves: manual.moves.length,
|
|
106995
|
+
sweepMoves: sweep.moves.length
|
|
106996
|
+
} });
|
|
106997
|
+
await this.complete(REBALANCE_SUGGESTED_ALERT_ID);
|
|
106998
|
+
return;
|
|
106999
|
+
}
|
|
107000
|
+
const note = this.lastNote;
|
|
107001
|
+
const cause = note === null ? "The cluster changed" : `Node ${note.nodeId} ${VERB[note.kind]}`;
|
|
107002
|
+
const count = manual.moves.length;
|
|
107003
|
+
const now = this.deps.now();
|
|
107004
|
+
logger.info("rebalance suggestion: raising the alert", { meta: {
|
|
107005
|
+
manualMoves: count,
|
|
107006
|
+
beyondSweep: onlyManual.length
|
|
107007
|
+
} });
|
|
107008
|
+
await this.emit({
|
|
107009
|
+
id: REBALANCE_SUGGESTED_ALERT_ID,
|
|
107010
|
+
category: ALERT_CATEGORY,
|
|
107011
|
+
severity: "warning",
|
|
107012
|
+
title: "Camera placement can be rebalanced",
|
|
107013
|
+
message: `${cause}. ${String(count)} ${count === 1 ? "camera" : "cameras"} would move to even out the load; the automatic balancing will not do it on its own.`,
|
|
107014
|
+
action: {
|
|
107015
|
+
label: "Rebalance now",
|
|
107016
|
+
invoke: {
|
|
107017
|
+
addonId: this.deps.addonId,
|
|
107018
|
+
action: "rebalanceNow"
|
|
107019
|
+
}
|
|
107020
|
+
},
|
|
107021
|
+
status: "active",
|
|
107022
|
+
read: false,
|
|
107023
|
+
createdAt: now,
|
|
107024
|
+
updatedAt: now,
|
|
107025
|
+
source: {
|
|
107026
|
+
type: "addon",
|
|
107027
|
+
id: this.deps.addonId
|
|
107028
|
+
}
|
|
107029
|
+
});
|
|
107030
|
+
}
|
|
107031
|
+
async evaluateUnusedNodes() {
|
|
107032
|
+
const enabled = new Set(this.deps.enabledNodes());
|
|
107033
|
+
const unused = new Set(this.deps.runnerNodes().filter((id) => !enabled.has(id)));
|
|
107034
|
+
for (const nodeId of unused) {
|
|
107035
|
+
const id = `${NODE_NOT_USED_ALERT_PREFIX}${nodeId}`;
|
|
107036
|
+
if (this.notUsedRaised.has(id)) continue;
|
|
107037
|
+
this.notUsedRaised.add(id);
|
|
107038
|
+
this.deps.logger.info("rebalance suggestion: node online but not used for detection", { tags: { nodeId } });
|
|
107039
|
+
const now = this.deps.now();
|
|
107040
|
+
await this.emit({
|
|
107041
|
+
id,
|
|
107042
|
+
category: ALERT_CATEGORY,
|
|
107043
|
+
severity: "info",
|
|
107044
|
+
title: "Node is not used for detection",
|
|
107045
|
+
message: `Node ${nodeId} is online but detection is not enabled on it, so no camera will be placed there.`,
|
|
107046
|
+
action: {
|
|
107047
|
+
label: "Open node",
|
|
107048
|
+
href: `/system/pipeline?view=nodes&node=${encodeURIComponent(nodeId)}`,
|
|
107049
|
+
navigation: "app"
|
|
107050
|
+
},
|
|
107051
|
+
status: "active",
|
|
107052
|
+
read: false,
|
|
107053
|
+
createdAt: now,
|
|
107054
|
+
updatedAt: now,
|
|
107055
|
+
source: {
|
|
107056
|
+
type: "node",
|
|
107057
|
+
id: nodeId
|
|
107058
|
+
}
|
|
107059
|
+
});
|
|
107060
|
+
}
|
|
107061
|
+
for (const id of [...this.notUsedRaised]) {
|
|
107062
|
+
const nodeId = id.slice(36);
|
|
107063
|
+
if (unused.has(nodeId)) continue;
|
|
107064
|
+
this.notUsedRaised.delete(id);
|
|
107065
|
+
await this.complete(id);
|
|
107066
|
+
}
|
|
107067
|
+
}
|
|
107068
|
+
async emit(alert) {
|
|
107069
|
+
await this.deps.alerts.emit(alert).catch((err) => {
|
|
107070
|
+
this.deps.logger.warn("rebalance suggestion: could not raise an alert", { meta: {
|
|
107071
|
+
alertId: alert.id,
|
|
107072
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107073
|
+
} });
|
|
107074
|
+
});
|
|
107075
|
+
}
|
|
107076
|
+
async complete(alertId) {
|
|
107077
|
+
await this.deps.alerts.complete(alertId).catch((err) => {
|
|
107078
|
+
this.deps.logger.debug("rebalance suggestion: close failed (alert may not exist)", { meta: {
|
|
107079
|
+
alertId,
|
|
107080
|
+
error: err instanceof Error ? err.message : String(err)
|
|
107081
|
+
} });
|
|
107082
|
+
});
|
|
107083
|
+
}
|
|
107084
|
+
};
|
|
107085
|
+
//#endregion
|
|
106890
107086
|
//#region src/settings-ui-schemas.ts
|
|
106891
107087
|
/** Build the addon-level schema sections (cluster roles + crop + balancer + failover). */
|
|
106892
107088
|
function buildGlobalSettingsSections(options) {
|
|
@@ -108683,6 +108879,9 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108683
108879
|
* Constructed right after `loadShed`. Nulled at the end of `onShutdown`.
|
|
108684
108880
|
*/
|
|
108685
108881
|
placement = null;
|
|
108882
|
+
/** The debounced "placement can be rebalanced" evaluation; see `rebalance-suggestion.ts`. */
|
|
108883
|
+
rebalanceSuggestion = null;
|
|
108884
|
+
unsubSuggestionEvents = [];
|
|
108686
108885
|
/** EventBus unsubscribes for agent online/offline events. */
|
|
108687
108886
|
unsubAgentOnline = null;
|
|
108688
108887
|
unsubAgentOffline = null;
|
|
@@ -108956,6 +109155,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108956
109155
|
this.localNodeId = controllers.localNodeId;
|
|
108957
109156
|
this.unsubAgentOnline = controllers.unsubAgentOnline;
|
|
108958
109157
|
this.unsubAgentOffline = controllers.unsubAgentOffline;
|
|
109158
|
+
this.startRebalanceSuggestion();
|
|
108959
109159
|
this.unsubOrchestratorSubscriptions = controllers.unsubOrchestratorSubscriptions;
|
|
108960
109160
|
this.pendingRetryTimer = controllers.pendingRetryTimer;
|
|
108961
109161
|
this.autoRebalanceTimer = controllers.autoRebalanceTimer;
|
|
@@ -108999,6 +109199,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
108999
109199
|
customActions: pipelineOrchestratorActions,
|
|
109000
109200
|
actionHandlers: {
|
|
109001
109201
|
dumpState: async () => this.dumpDiagnostics(),
|
|
109202
|
+
rebalanceNow: async () => this.rebalanceNow(),
|
|
109002
109203
|
nodeStressStats: async (input) => {
|
|
109003
109204
|
const lts = this.nodeStressLts;
|
|
109004
109205
|
const moves = this.nodeStress?.historyView() ?? [];
|
|
@@ -109081,6 +109282,7 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109081
109282
|
this.detectionWiring = null;
|
|
109082
109283
|
this.unsubAgentOffline?.();
|
|
109083
109284
|
this.unsubAgentOffline = null;
|
|
109285
|
+
this.stopRebalanceSuggestion();
|
|
109084
109286
|
this.effectiveSharesService?.stop();
|
|
109085
109287
|
this.unsubAgentOnline?.();
|
|
109086
109288
|
this.unsubAgentOnline = null;
|
|
@@ -109147,6 +109349,94 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109147
109349
|
async rebalance(opts) {
|
|
109148
109350
|
return this.placement.rebalance(opts);
|
|
109149
109351
|
}
|
|
109352
|
+
/**
|
|
109353
|
+
* The `rebalanceNow` custom action: a manual rebalance, and the verdict on
|
|
109354
|
+
* the suggestion alert that offered it. Every branch is logged.
|
|
109355
|
+
*/
|
|
109356
|
+
async rebalanceNow() {
|
|
109357
|
+
const alertId = REBALANCE_SUGGESTED_ALERT_ID;
|
|
109358
|
+
try {
|
|
109359
|
+
const result = await this.rebalance({ minImprovement: 0 });
|
|
109360
|
+
const message = `${String(result.migrated)} ${result.migrated === 1 ? "camera" : "cameras"} moved`;
|
|
109361
|
+
this.ctx.logger.info("rebalanceNow: done", { meta: {
|
|
109362
|
+
migrated: result.migrated,
|
|
109363
|
+
skipped: result.skipped.length
|
|
109364
|
+
} });
|
|
109365
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109366
|
+
alertId,
|
|
109367
|
+
patch: {
|
|
109368
|
+
status: "completed",
|
|
109369
|
+
message,
|
|
109370
|
+
updatedAt: Date.now()
|
|
109371
|
+
}
|
|
109372
|
+
}).catch((err) => this.ctx.logger.warn("rebalanceNow: could not close the suggestion alert", { meta: { error: errMsg$2(err) } }));
|
|
109373
|
+
return {
|
|
109374
|
+
migrated: result.migrated,
|
|
109375
|
+
skipped: result.skipped.length
|
|
109376
|
+
};
|
|
109377
|
+
} catch (err) {
|
|
109378
|
+
const reason = errMsg$2(err);
|
|
109379
|
+
this.ctx.logger.error("rebalanceNow: failed", { meta: { error: reason } });
|
|
109380
|
+
await this.ctx.api.alerts.update.mutate({
|
|
109381
|
+
alertId,
|
|
109382
|
+
patch: {
|
|
109383
|
+
status: "failed",
|
|
109384
|
+
message: `Rebalance failed: ${reason}`,
|
|
109385
|
+
updatedAt: Date.now()
|
|
109386
|
+
}
|
|
109387
|
+
}).catch((e) => this.ctx.logger.warn("rebalanceNow: could not mark the suggestion alert failed", { meta: { error: errMsg$2(e) } }));
|
|
109388
|
+
throw err;
|
|
109389
|
+
}
|
|
109390
|
+
}
|
|
109391
|
+
startRebalanceSuggestion() {
|
|
109392
|
+
const ctx = this.ctx;
|
|
109393
|
+
const suggestion = new RebalanceSuggestion({
|
|
109394
|
+
addonId: "pipeline-orchestrator",
|
|
109395
|
+
logger: ctx.logger,
|
|
109396
|
+
debounceMs: DEFAULT_SUGGESTION_DEBOUNCE_MS,
|
|
109397
|
+
now: () => Date.now(),
|
|
109398
|
+
plan: (minImprovement) => this.rebalance({
|
|
109399
|
+
minImprovement,
|
|
109400
|
+
dryRun: true
|
|
109401
|
+
}),
|
|
109402
|
+
enabledNodes: () => this.topology?.enabledNodes ?? [],
|
|
109403
|
+
runnerNodes: () => this.topology?.collectRunnerNodeIds() ?? [],
|
|
109404
|
+
alerts: {
|
|
109405
|
+
emit: (alert) => ctx.api.alerts.emit.mutate(alert),
|
|
109406
|
+
complete: (alertId) => ctx.api.alerts.update.mutate({
|
|
109407
|
+
alertId,
|
|
109408
|
+
patch: {
|
|
109409
|
+
status: "completed",
|
|
109410
|
+
updatedAt: Date.now()
|
|
109411
|
+
}
|
|
109412
|
+
})
|
|
109413
|
+
}
|
|
109414
|
+
});
|
|
109415
|
+
this.rebalanceSuggestion = suggestion;
|
|
109416
|
+
this.unsubSuggestionEvents = [
|
|
109417
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOnline }, (event) => suggestion.noteNodeEvent({
|
|
109418
|
+
kind: "joined",
|
|
109419
|
+
nodeId: event.data.agentId
|
|
109420
|
+
})),
|
|
109421
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentOffline }, (event) => suggestion.noteNodeEvent({
|
|
109422
|
+
kind: "left",
|
|
109423
|
+
nodeId: event.data.agentId
|
|
109424
|
+
})),
|
|
109425
|
+
ctx.eventBus.subscribe({ category: EventCategory.AgentUnregistered }, (event) => {
|
|
109426
|
+
const agentId = event.data["agentId"];
|
|
109427
|
+
if (typeof agentId === "string") suggestion.noteNodeEvent({
|
|
109428
|
+
kind: "left",
|
|
109429
|
+
nodeId: agentId
|
|
109430
|
+
});
|
|
109431
|
+
})
|
|
109432
|
+
];
|
|
109433
|
+
}
|
|
109434
|
+
stopRebalanceSuggestion() {
|
|
109435
|
+
for (const unsub of this.unsubSuggestionEvents) unsub();
|
|
109436
|
+
this.unsubSuggestionEvents = [];
|
|
109437
|
+
this.rebalanceSuggestion?.dispose();
|
|
109438
|
+
this.rebalanceSuggestion = null;
|
|
109439
|
+
}
|
|
109150
109440
|
getPipelineAssignments() {
|
|
109151
109441
|
return this.placement.getPipelineAssignments();
|
|
109152
109442
|
}
|
|
@@ -109502,6 +109792,10 @@ var PipelineOrchestratorAddon = class PipelineOrchestratorAddon extends BaseAddo
|
|
|
109502
109792
|
this.topology.addKnownRunnerNode(input.agentNodeId);
|
|
109503
109793
|
this.reconcile.schedulePendingRetry();
|
|
109504
109794
|
if (input.detect !== void 0) this.noteShareChange(`node-detect:${input.agentNodeId}`);
|
|
109795
|
+
this.rebalanceSuggestion?.noteNodeEvent({
|
|
109796
|
+
kind: "capabilities",
|
|
109797
|
+
nodeId: input.agentNodeId
|
|
109798
|
+
});
|
|
109505
109799
|
return { success: true };
|
|
109506
109800
|
}
|
|
109507
109801
|
/**
|
package/dist/remoteEntry.js
CHANGED
|
@@ -30,7 +30,7 @@ async function d(e) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
async function f() {
|
|
33
|
-
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-
|
|
33
|
+
return l ||= d(() => import("./_virtual_mf-localSharedImportMap___mfe_internal__addon_pipeline_orchestrator_widgets-3S_aIDgV.mjs")).catch((e) => {
|
|
34
34
|
throw l = void 0, e;
|
|
35
35
|
}), l;
|
|
36
36
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-pipeline-orchestrator",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.637",
|
|
4
4
|
"description": "Hub-side camera-to-agent load balancer — tracks runner capacity and dispatches attachCamera calls to the optimal pipeline-runner instance",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|