mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
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:
@@ -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' },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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). */
|
||||
|
||||
Reference in New Issue
Block a user