fix(notifications): neutralize satellite-local node names in alert bodies (#1640)

* fix(notifications): neutralize satellite-local node names in alert bodies

Fleet-aggregated alerts embedded each instance seed name (often Local)
while the hub badge already named the remote. Drop identity prefixes and
use type-aware local wording so attribution stays on the badge.

* fix(docs): correct image-update default check cadence

Operator docs still said six-hour polling; the seeded default is two
hours in interval mode, and the cadence is configurable or cron-based.

* test(notifications): assert hub stamps roster name on neutral remote bodies

Cover the fan-in path that attaches hub roster identity while leaving the
satellite message body unchanged.
This commit is contained in:
Anso
2026-07-16 15:51:52 -04:00
committed by GitHub
parent b91025dc8b
commit d8e4ede94f
12 changed files with 288 additions and 48 deletions
+8 -3
View File
@@ -29,6 +29,11 @@ function diagnosticLog(message: string, fields: Record<string, string | number |
console.info(`[BlueprintReconciler:diag] ${message}`, safeFields);
}
/** Local seed name must not reach fleet alerts; remote roster names stay. */
function nodeLocationClause(node: Node): string {
return node.type === 'local' ? 'on this node' : `on node "${node.name}"`;
}
export interface ReconcileDecision {
deploy: Node[];
withdraw: Node[];
@@ -287,7 +292,7 @@ export class BlueprintReconciler {
notifications.dispatchAlert(
'warning',
'blueprint_drift_detected',
`Blueprint "${blueprint.name}" drifted on node "${node.name}": ${reason}`,
`Blueprint "${blueprint.name}" drifted ${nodeLocationClause(node)}: ${reason}`,
{ stackName: blueprint.name, actor: 'system:blueprint' },
);
return;
@@ -301,7 +306,7 @@ export class BlueprintReconciler {
notifications.dispatchAlert(
'warning',
'blueprint_drift_detected',
`Blueprint "${blueprint.name}" lost its marker on node "${node.name}"; auto-fix declined to avoid stomping unowned data. Reason: ${reason}`,
`Blueprint "${blueprint.name}" lost its marker ${nodeLocationClause(node)}; auto-fix declined to avoid stomping unowned data. Reason: ${reason}`,
{ stackName: blueprint.name, actor: 'system:blueprint' },
);
return;
@@ -318,7 +323,7 @@ export class BlueprintReconciler {
notifications.dispatchAlert(
'error',
'blueprint_drift_correction_failed',
`Auto-fix for "${blueprint.name}" on node "${node.name}" failed: ${result.error ?? 'unknown error'}`,
`Auto-fix for "${blueprint.name}" ${nodeLocationClause(node)} failed: ${result.error ?? 'unknown error'}`,
{ stackName: blueprint.name, actor: 'system:blueprint' },
);
}
+4 -8
View File
@@ -721,7 +721,7 @@ export class DockerEventService {
}
// ========================================================================
// Notification wrappers (prefix with node name for multi-node clarity)
// Notification wrappers (node-neutral bodies; hub bell badges remotes)
// ========================================================================
private buildAlertOptions(
@@ -735,19 +735,15 @@ export class DockerEventService {
}
private async emitError(category: NotificationCategory, message: string, stackName?: string, containerName?: string, systemOnly = false): Promise<void> {
return this.notifier.dispatchAlert('error', category, this.prefix(message), this.buildAlertOptions(stackName, containerName, systemOnly));
return this.notifier.dispatchAlert('error', category, message, this.buildAlertOptions(stackName, containerName, systemOnly));
}
private async emitWarning(category: NotificationCategory, message: string, stackName?: string, containerName?: string): Promise<void> {
return this.notifier.dispatchAlert('warning', category, this.prefix(message), this.buildAlertOptions(stackName, containerName));
return this.notifier.dispatchAlert('warning', category, message, this.buildAlertOptions(stackName, containerName));
}
private async emitInfo(category: NotificationCategory, message: string, stackName?: string, containerName?: string): Promise<void> {
return this.notifier.dispatchAlert('info', category, this.prefix(message), this.buildAlertOptions(stackName, containerName));
}
private prefix(message: string): string {
return `[Node: ${this.nodeName}] ${message}`;
return this.notifier.dispatchAlert('info', category, message, this.buildAlertOptions(stackName, containerName));
}
// ========================================================================
+5 -4
View File
@@ -523,7 +523,7 @@ export class ImageUpdateService {
for (const node of db.getNodes()) {
if (node.type !== 'local' || !node.id) continue;
try {
await this.checkNode(node.id, node.name, db);
await this.checkNode(node.id, db);
} catch (e) {
console.error(`[ImageUpdateService] Error on node ${node.name}:`, e);
}
@@ -536,7 +536,7 @@ export class ImageUpdateService {
}
}
private async checkNode(nodeId: number, nodeName: string, db: DatabaseService) {
private async checkNode(nodeId: number, db: DatabaseService) {
const docker = DockerController.getInstance(nodeId);
const fs = FileSystemService.getInstance(nodeId);
const composeDir = path.resolve(NodeRegistry.getInstance().getComposeDir(nodeId));
@@ -708,7 +708,8 @@ export class ImageUpdateService {
await notifier.dispatchAlert(
'info',
'image_update_available',
`[Node: ${nodeName}] Stack "${stackName}" has image updates available.`,
// Node-neutral body: the hub bell badge attributes remotes.
`Stack "${stackName}" has image updates available.`,
{ stackName, actor: 'system:image-update' },
);
} catch (e) {
@@ -722,7 +723,7 @@ export class ImageUpdateService {
level: 'error',
category: 'system',
message: sanitizeNotificationMessage(
`[Node: ${nodeName}] Failed to notify about image updates for stack "${stackName}": ${getErrorMessage(e, String(e))}`,
`Failed to notify about image updates for stack "${stackName}": ${getErrorMessage(e, String(e))}`,
{ composeDir: NodeRegistry.getInstance().getComposeDir(localNodeId) },
),
timestamp: Date.now(),
+2 -1
View File
@@ -765,7 +765,8 @@ export class MonitorService {
const safeCurrent = typeof currentValue === 'number' ? Number(currentValue.toFixed(2)) : currentValue;
const safeThreshold = typeof rule.threshold === 'number' ? Number(rule.threshold.toFixed(2)) : rule.threshold;
const message = `[Node: ${node.name}] The **${metricName}** for **${rule.stack_name}** ${operatorPhrase} **${safeThreshold}${unit}** (Currently: ${safeCurrent}${unit}).`;
// Node-neutral body: the hub bell badge attributes remotes.
const message = `The **${metricName}** for **${rule.stack_name}** ${operatorPhrase} **${safeThreshold}${unit}** (Currently: ${safeCurrent}${unit}).`;
console.log(`[MonitorService] Alert fired: rule ${ruleId} on stack "${rule.stack_name}": ${metricName} ${operatorPhrase} ${safeThreshold}${unit}`);
await NotificationService.getInstance().dispatchAlert(
+15 -3
View File
@@ -298,7 +298,15 @@ export class SchedulerService {
if (task.node_id != null && task.action !== 'snapshot') {
const node = db.getNode(task.node_id);
if (!node) throw new Error(`Target node (id=${task.node_id}) no longer exists`);
if (node.status === 'offline') throw new Error(`Target node "${node.name}" is offline`);
if (node.status === 'offline') {
// Local seed name ("Local") must not reach fleet-aggregated alerts;
// remote roster names stay for diagnosis.
throw new Error(
node.type === 'local'
? 'Target node is offline'
: `Target node "${node.name}" is offline`,
);
}
}
if (isDebugEnabled()) console.log(`[SchedulerService:debug] Task ${task.id} pre-checks passed, executing ${task.action}`);
@@ -813,8 +821,12 @@ export class SchedulerService {
* segment.
*/
private containerNotFoundMessage(containerName: string, nodeId: number): string {
const nodeName = NodeRegistry.getInstance().getNode(nodeId)?.name ?? String(nodeId);
return `Container "${containerName}" not found on node "${nodeName}". It may have been renamed or removed.`;
const node = NodeRegistry.getInstance().getNode(nodeId);
// Only an explicit local node is neutralized; missing lookup keeps the id fallback.
const location = node?.type === 'local'
? 'on this node'
: `on node "${node?.name ?? String(nodeId)}"`;
return `Container "${containerName}" not found ${location}. It may have been renamed or removed.`;
}
/** Hub target tag used for startsWith rethrow guards (no trailing detail). */