openvibers/openvibe.network Commits
Operator alerts read as System notices: humanized names, plain bodies, the fix command in rich_content (#378)
Changes
@@@ -343,11 +343,12 @@ class NotificationService {// ─── Update ────────────────────────────────────────────────/** Edit one of a user's notifications in place (an operator alert that resolves or fires again). */-- async revise(id, userId, { title, message, icon, is_read: isRead } = {}) {++ async revise(id, userId, { title, message, icon, is_read: isRead, rich_content: richContent } = {}) {const sets = []; const args = [];if (title != null) { sets.push('title = ?'); args.push(String(title).slice(0, 200)); }if (message != null) { sets.push('message = ?'); args.push(String(message)); }if (icon != null) { sets.push('icon = ?'); args.push(String(icon)); }++ if (richContent != null) { sets.push('rich_content = ?'); args.push(JSON.stringify(richContent)); }if (isRead != null) { sets.push('is_read = ?'); args.push(isRead ? 1 : 0); }if (!sets.length) return false;const r = await this.db.prepare(`UPDATE notifications SET ${sets.join(', ')} WHERE id = ? AND user_id = ?`).run(...args, id, userId);@@@ -42,6 +42,35 @@ function message(a) {const ago = (ms) => (ms < 3600e3 ? `${Math.max(1, Math.round(ms / 60e3))} min` : `${Math.round(ms / 3600e3)} h`);++/** OpenVibeBrowserCheckFailed → "Browser check failed": how an alert's name reads in a notification. */++function humanize(name) {++ const s = String(name || '').replace(/^OpenVibe(?=[A-Z])/, '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').trim();++ return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : 'Alert';++}++++/**++ * The parts of an alert a person reads, kept apart from the shell command its rule may carry (owner 10-28: raw++ * "run: sudo ovhost …" text in the notification body). `rich_content` gives Shared's notification UI the name,++ * state, severity, sites, release and the command for a copy button; the message stays plain words.++ */++function readable(a) {++ const text = [a.summary, a.description].filter(Boolean).join('. ');++ const command = (/run:\s*(sudo [^;]+?)(?:\.\s|;|\.$|$)/.exec(text) || [])[1] || null;++ const sites = [...new Set((text.match(/(?:^|\.\s+)([a-z0-9][a-z0-9.-]{1,30}):\s/g) || []).map((s) => s.replace(/^\.\s+/, '').replace(/:\s$/, '')))].slice(0, 6);++ const release = (/release ([0-9a-f]{7,40})/i.exec(text) || [])[1] || null;++ const failing = (/\((\d+) failing check/i.exec(text) || [])[1] || null;++ let summary = String(a.summary || '').replace(/run:\s*sudo [^;]+?(?:\.\s|;|\.$|$)/g, '').replace(/^[a-z0-9][a-z0-9.-]{1,30}:\s/, '').trim();++ if (release || failing) summary = [release ? `Release ${release.slice(0, 7)}` : null, failing ? `${failing} failing check${failing === '1' ? '' : 's'}` : null].filter(Boolean).join(' · ');++ const detail = String(a.description || '').replace(/run:\s*sudo [^;]+?(?:\.\s|;|\.$|$)/g, '').trim();++ const message = [sites.length ? sites.join(', ') : (a.service || null), summary || null, detail && detail !== summary ? detail : null].filter(Boolean).join(' · ');++ return { command, sites, release, summary, message: message.slice(0, 500) };++}++++function opsContent(a, state, r = readable(a)) {++ return { kind: 'ops_alert', name: a.name, state, severity: a.severity, sites: r.sites, release: r.release, summary: r.summary,++ command: r.command, service: a.service || null, since: a.started_at || null, runbook_url: STATUS_URL };++}++/*** Apply one report. `body` has passed the request contract. Answers the result document.* notify(userId, notification) creates one notification (the notification service's create(); `silent` skips@@@ -98,11 +127,13 @@ async function receive(db, body, { notify, revise = null, now = Date.now(), envconst critical = a.severity === 'critical';if (kind === 'opened' || kind === 'reminded') {if (typeof notify !== 'function') continue;++ const r = readable(a);const n = {type: 'OPERATOR_ALERT', category: 'admin', service: 'host', url: STATUS_URL,priority: critical ? 'critical' : 'high', icon: critical ? '🚨' : '⚠️', silent: !critical,-- title: `${kind === 'reminded' ? 'Still firing' : 'Alert'}: ${a.name}`.slice(0, 120),-- message: `${a.summary}. ${message(a)}`.slice(0, 500),++ title: `${kind === 'reminded' ? 'Still failing: ' : ''}${humanize(a.name)}`.slice(0, 120),++ message: r.message || `${a.summary}. ${message(a)}`.slice(0, 500),++ rich_content: opsContent(a, kind === 'reminded' ? 'reminded' : 'firing', r),};const kept = kind === 'reminded' ? notices(prev) : [];for (const uid of users) {@@@ -116,9 +147,12 @@ async function receive(db, body, { notify, revise = null, now = Date.now(), envcontinue;}if (typeof revise !== 'function') continue;++ const r = readable(a);const fields = kind === 'resolved'-- ? { title: `Resolved: ${a.name}`.slice(0, 120), icon: '✅', is_read: 1, message: `${a.summary} (resolved after ${ago(now - (prev.opened_at || now))}${prev.reopened ? `; it fired ${prev.reopened + 1} times` : ''}).`.slice(0, 500) }-- : { title: `Alert: ${a.name}`.slice(0, 120), icon: critical ? '🚨' : '⚠️', is_read: critical ? 0 : 1, message: `${a.summary}. ${message(a)} Fired again ${(prev.reopened || 0) + 1} time(s) within 12 h of resolving.`.slice(0, 500) };++ ? { title: `Resolved: ${humanize(a.name)}`.slice(0, 120), icon: '✅', is_read: 1, rich_content: opsContent(a, 'resolved', r),++ message: `${r.message || a.summary} (resolved after ${ago(now - (prev.opened_at || now))}${prev.reopened ? `; it fired ${prev.reopened + 1} times` : ''}).`.slice(0, 500) }++ : { title: humanize(a.name).slice(0, 120), icon: critical ? '🚨' : '⚠️', is_read: critical ? 0 : 1, rich_content: opsContent(a, 'firing', r),++ message: `${r.message || a.summary}. Fired again ${(prev.reopened || 0) + 1} time(s) within 12 h of resolving.`.slice(0, 500) };for (const [uid, id] of notices(prev)) {try { await revise(uid, id, fields); } catch (e) { console.warn('[operator-alerts] revise failed:', e.message); }}@@@ -132,4 +166,4 @@ async function list(db, { limit = 100 } = {}) {return await db.prepare("SELECT source, fingerprint, name, severity, summary, service, state, started_at, opened_at, last_seen_at, notified_at, resolved_at FROM operator_alerts ORDER BY state = 'firing' DESC, COALESCE(resolved_at, last_seen_at) DESC LIMIT ?").all(limit);}--module.exports = { ensureTables, receive, list, operatorUserIds, REMIND_MS, FLAP_MS };++module.exports = { ensureTables, receive, list, operatorUserIds, humanize, readable, REMIND_MS, FLAP_MS };@@@ -78,12 +78,14 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').assert.ok(validate('network.operator-alerts-result@1', r.body).valid);assert.deepStrictEqual(r.body, { ok: true, firing: 2, opened: 2, reminded: 0, resolved: 0, notified: 4 });assert.deepStrictEqual(sent.map((n) => n.user_id).sort(), [1, 1, 2, 2]);-- const backup = sent.find((n) => n.title === 'Alert: OpenVibeBackupMissed' && n.user_id === 1);++ const backup = sent.find((n) => n.title === 'Backup missed' && n.user_id === 1);assert.deepStrictEqual([backup.type, backup.category, backup.priority, backup.url], ['OPERATOR_ALERT', 'admin', 'critical', 'https://openvibe.network/status']);-- assert.match(backup.message, /No successful backup in 26 hours\. Service: host\. Firing since 2026-09-26T05:00:00Z\./);-- assert.strictEqual(sent.find((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed').priority, 'high', 'a warning pages at high');-- assert.deepStrictEqual([backup.silent, sent.find((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed').silent], [false, true], 'only a critical alert is pushed');-- const warnIds = sent.filter((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed').map((n, i) => n).length;++ assert.strictEqual(backup.message, 'host · OpenVibeBackupMissed summary · No successful backup in 26 hours.');++ assert.strictEqual(backup.rich_content.kind, 'ops_alert');++ assert.strictEqual(backup.rich_content.state, 'firing');++ assert.strictEqual(sent.find((n) => n.title === 'Browser check failed').priority, 'high', 'a warning pages at high');++ assert.deepStrictEqual([backup.silent, sent.find((n) => n.title === 'Browser check failed').silent], [false, true], 'only a critical alert is pushed');++ const warnIds = sent.filter((n) => n.title === 'Browser check failed').map((n, i) => n).length;assert.strictEqual(warnIds, 2);// The same set again: nothing new.@@@ -97,7 +99,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').assert.deepStrictEqual(r.body, { ok: true, firing: 1, opened: 0, reminded: 0, resolved: 1, notified: 0 });assert.strictEqual(sent.length, 0, 'no Resolved entry piles up');assert.strictEqual(revised.length, 2, 'one revision per operator');-- assert.ok(revised.every((x) => x.title === 'Resolved: OpenVibeBrowserCheckFailed' && x.icon === '✅' && x.is_read === 1), JSON.stringify(revised));++ assert.ok(revised.every((x) => x.title === 'Resolved: Browser check failed' && x.icon === '✅' && x.is_read === 1), JSON.stringify(revised));assert.match(revised[0].message, /\(resolved after \d+ min\)\./);const warnNotices = revised.map((x) => x.id).sort();@@@ -106,7 +108,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').const later = Date.now() + alerts.REMIND_MS + 1000;let out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: (u, n) => { sent.push({ user_id: u, ...n }); return { id: `r${sent.length}` }; }, now: later });assert.deepStrictEqual(out, { ok: true, firing: 1, opened: 0, reminded: 1, resolved: 0, notified: 2 });-- assert.strictEqual(sent[0].title, 'Still firing: OpenVibeBackupMissed');++ assert.strictEqual(sent[0].title, 'Still failing: Backup missed');out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: () => true, now: later + 60000 });assert.strictEqual(out.reminded, 0);@@@ -122,7 +124,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').assert.deepStrictEqual([r.body.opened, r.body.notified], [1, 0]);assert.strictEqual(sent.length, 0);assert.deepStrictEqual(revised.map((x) => x.id).sort(), warnNotices, 'the episode keeps its notifications');-- assert.ok(revised.every((x) => x.title === 'Alert: OpenVibeBrowserCheckFailed' && /Fired again 1 time\(s\) within 12 h/.test(x.message)));++ assert.ok(revised.every((x) => x.title === 'Browser check failed' && /Fired again 1 time\(s\) within 12 h/.test(x.message)));revised.length = 0;await post({ source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, auth);assert.match(revised[0].message, /it fired 2 times/);@@@ -139,7 +141,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').sent.length = 0;out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed'), alert('OpenVibeBrowserCheckFailed', { severity: 'warning' })] }, { notify: (u, n) => { sent.push(n); return { id: `x${sent.length}` }; }, now: much });assert.deepStrictEqual([out.opened, out.notified], [1, 2]);-- assert.ok(sent.every((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed'));++ assert.ok(sent.every((n) => n.title === 'Browser check failed'));await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: () => null, now: much + 1000 });// An empty set resolves everything from that source; the list keeps them.
@@ -343,11 +343,12 @@ class NotificationService {
@@ -343,11 +343,12 @@ class NotificationService {
// ─── Update ────────────────────────────────────────────────
// ─── Update ────────────────────────────────────────────────
/** Edit one of a user's notifications in place (an operator alert that resolves or fires again). */
/** Edit one of a user's notifications in place (an operator alert that resolves or fires again). */
- async revise(id, userId, { title, message, icon, is_read: isRead } = {}) {
+ async revise(id, userId, { title, message, icon, is_read: isRead, rich_content: richContent } = {}) {
const sets = []; const args = [];
const sets = []; const args = [];
if (title != null) { sets.push('title = ?'); args.push(String(title).slice(0, 200)); }
if (title != null) { sets.push('title = ?'); args.push(String(title).slice(0, 200)); }
if (message != null) { sets.push('message = ?'); args.push(String(message)); }
if (message != null) { sets.push('message = ?'); args.push(String(message)); }
if (icon != null) { sets.push('icon = ?'); args.push(String(icon)); }
if (icon != null) { sets.push('icon = ?'); args.push(String(icon)); }
+ if (richContent != null) { sets.push('rich_content = ?'); args.push(JSON.stringify(richContent)); }
if (isRead != null) { sets.push('is_read = ?'); args.push(isRead ? 1 : 0); }
if (isRead != null) { sets.push('is_read = ?'); args.push(isRead ? 1 : 0); }
if (!sets.length) return false;
if (!sets.length) return false;
const r = await this.db.prepare(`UPDATE notifications SET ${sets.join(', ')} WHERE id = ? AND user_id = ?`).run(...args, id, userId);
const r = await this.db.prepare(`UPDATE notifications SET ${sets.join(', ')} WHERE id = ? AND user_id = ?`).run(...args, id, userId);
@@ -42,6 +42,35 @@ function message(a) {
@@ -42,6 +42,35 @@ function message(a) {
const ago = (ms) => (ms < 3600e3 ? `${Math.max(1, Math.round(ms / 60e3))} min` : `${Math.round(ms / 3600e3)} h`);
const ago = (ms) => (ms < 3600e3 ? `${Math.max(1, Math.round(ms / 60e3))} min` : `${Math.round(ms / 3600e3)} h`);
+/** OpenVibeBrowserCheckFailed → "Browser check failed": how an alert's name reads in a notification. */
+function humanize(name) {
+ const s = String(name || '').replace(/^OpenVibe(?=[A-Z])/, '').replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[_-]+/g, ' ').trim();
+ return s ? s.charAt(0).toUpperCase() + s.slice(1).toLowerCase() : 'Alert';
+}
+
+/**
+ * The parts of an alert a person reads, kept apart from the shell command its rule may carry (owner 10-28: raw
+ * "run: sudo ovhost …" text in the notification body). `rich_content` gives Shared's notification UI the name,
+ * state, severity, sites, release and the command for a copy button; the message stays plain words.
+ */
+function readable(a) {
+ const text = [a.summary, a.description].filter(Boolean).join('. ');
+ const command = (/run:\s*(sudo [^;]+?)(?:\.\s|;|\.$|$)/.exec(text) || [])[1] || null;
+ const sites = [...new Set((text.match(/(?:^|\.\s+)([a-z0-9][a-z0-9.-]{1,30}):\s/g) || []).map((s) => s.replace(/^\.\s+/, '').replace(/:\s$/, '')))].slice(0, 6);
+ const release = (/release ([0-9a-f]{7,40})/i.exec(text) || [])[1] || null;
+ const failing = (/\((\d+) failing check/i.exec(text) || [])[1] || null;
+ let summary = String(a.summary || '').replace(/run:\s*sudo [^;]+?(?:\.\s|;|\.$|$)/g, '').replace(/^[a-z0-9][a-z0-9.-]{1,30}:\s/, '').trim();
+ if (release || failing) summary = [release ? `Release ${release.slice(0, 7)}` : null, failing ? `${failing} failing check${failing === '1' ? '' : 's'}` : null].filter(Boolean).join(' · ');
+ const detail = String(a.description || '').replace(/run:\s*sudo [^;]+?(?:\.\s|;|\.$|$)/g, '').trim();
+ const message = [sites.length ? sites.join(', ') : (a.service || null), summary || null, detail && detail !== summary ? detail : null].filter(Boolean).join(' · ');
+ return { command, sites, release, summary, message: message.slice(0, 500) };
+}
+
+function opsContent(a, state, r = readable(a)) {
+ return { kind: 'ops_alert', name: a.name, state, severity: a.severity, sites: r.sites, release: r.release, summary: r.summary,
+ command: r.command, service: a.service || null, since: a.started_at || null, runbook_url: STATUS_URL };
+}
+
/**
/**
* Apply one report. `body` has passed the request contract. Answers the result document.
* Apply one report. `body` has passed the request contract. Answers the result document.
* notify(userId, notification) creates one notification (the notification service's create(); `silent` skips
* notify(userId, notification) creates one notification (the notification service's create(); `silent` skips
@@ -98,11 +127,13 @@ async function receive(db, body, { notify, revise = null, now = Date.now(), env
@@ -98,11 +127,13 @@ async function receive(db, body, { notify, revise = null, now = Date.now(), env
const critical = a.severity === 'critical';
const critical = a.severity === 'critical';
if (kind === 'opened' || kind === 'reminded') {
if (kind === 'opened' || kind === 'reminded') {
if (typeof notify !== 'function') continue;
if (typeof notify !== 'function') continue;
+ const r = readable(a);
const n = {
const n = {
type: 'OPERATOR_ALERT', category: 'admin', service: 'host', url: STATUS_URL,
type: 'OPERATOR_ALERT', category: 'admin', service: 'host', url: STATUS_URL,
priority: critical ? 'critical' : 'high', icon: critical ? '🚨' : '⚠️', silent: !critical,
priority: critical ? 'critical' : 'high', icon: critical ? '🚨' : '⚠️', silent: !critical,
- title: `${kind === 'reminded' ? 'Still firing' : 'Alert'}: ${a.name}`.slice(0, 120),
- message: `${a.summary}. ${message(a)}`.slice(0, 500),
+ title: `${kind === 'reminded' ? 'Still failing: ' : ''}${humanize(a.name)}`.slice(0, 120),
+ message: r.message || `${a.summary}. ${message(a)}`.slice(0, 500),
+ rich_content: opsContent(a, kind === 'reminded' ? 'reminded' : 'firing', r),
};
};
const kept = kind === 'reminded' ? notices(prev) : [];
const kept = kind === 'reminded' ? notices(prev) : [];
for (const uid of users) {
for (const uid of users) {
@@ -116,9 +147,12 @@ async function receive(db, body, { notify, revise = null, now = Date.now(), env
@@ -116,9 +147,12 @@ async function receive(db, body, { notify, revise = null, now = Date.now(), env
continue;
continue;
}
}
if (typeof revise !== 'function') continue;
if (typeof revise !== 'function') continue;
+ const r = readable(a);
const fields = kind === 'resolved'
const fields = kind === 'resolved'
- ? { title: `Resolved: ${a.name}`.slice(0, 120), icon: '✅', is_read: 1, message: `${a.summary} (resolved after ${ago(now - (prev.opened_at || now))}${prev.reopened ? `; it fired ${prev.reopened + 1} times` : ''}).`.slice(0, 500) }
- : { title: `Alert: ${a.name}`.slice(0, 120), icon: critical ? '🚨' : '⚠️', is_read: critical ? 0 : 1, message: `${a.summary}. ${message(a)} Fired again ${(prev.reopened || 0) + 1} time(s) within 12 h of resolving.`.slice(0, 500) };
+ ? { title: `Resolved: ${humanize(a.name)}`.slice(0, 120), icon: '✅', is_read: 1, rich_content: opsContent(a, 'resolved', r),
+ message: `${r.message || a.summary} (resolved after ${ago(now - (prev.opened_at || now))}${prev.reopened ? `; it fired ${prev.reopened + 1} times` : ''}).`.slice(0, 500) }
+ : { title: humanize(a.name).slice(0, 120), icon: critical ? '🚨' : '⚠️', is_read: critical ? 0 : 1, rich_content: opsContent(a, 'firing', r),
+ message: `${r.message || a.summary}. Fired again ${(prev.reopened || 0) + 1} time(s) within 12 h of resolving.`.slice(0, 500) };
for (const [uid, id] of notices(prev)) {
for (const [uid, id] of notices(prev)) {
try { await revise(uid, id, fields); } catch (e) { console.warn('[operator-alerts] revise failed:', e.message); }
try { await revise(uid, id, fields); } catch (e) { console.warn('[operator-alerts] revise failed:', e.message); }
}
}
@@ -132,4 +166,4 @@ async function list(db, { limit = 100 } = {}) {
@@ -132,4 +166,4 @@ async function list(db, { limit = 100 } = {}) {
return await db.prepare("SELECT source, fingerprint, name, severity, summary, service, state, started_at, opened_at, last_seen_at, notified_at, resolved_at FROM operator_alerts ORDER BY state = 'firing' DESC, COALESCE(resolved_at, last_seen_at) DESC LIMIT ?").all(limit);
return await db.prepare("SELECT source, fingerprint, name, severity, summary, service, state, started_at, opened_at, last_seen_at, notified_at, resolved_at FROM operator_alerts ORDER BY state = 'firing' DESC, COALESCE(resolved_at, last_seen_at) DESC LIMIT ?").all(limit);
}
}
-module.exports = { ensureTables, receive, list, operatorUserIds, REMIND_MS, FLAP_MS };
+module.exports = { ensureTables, receive, list, operatorUserIds, humanize, readable, REMIND_MS, FLAP_MS };
@@ -78,12 +78,14 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
@@ -78,12 +78,14 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
assert.ok(validate('network.operator-alerts-result@1', r.body).valid);
assert.ok(validate('network.operator-alerts-result@1', r.body).valid);
assert.deepStrictEqual(r.body, { ok: true, firing: 2, opened: 2, reminded: 0, resolved: 0, notified: 4 });
assert.deepStrictEqual(r.body, { ok: true, firing: 2, opened: 2, reminded: 0, resolved: 0, notified: 4 });
assert.deepStrictEqual(sent.map((n) => n.user_id).sort(), [1, 1, 2, 2]);
assert.deepStrictEqual(sent.map((n) => n.user_id).sort(), [1, 1, 2, 2]);
- const backup = sent.find((n) => n.title === 'Alert: OpenVibeBackupMissed' && n.user_id === 1);
+ const backup = sent.find((n) => n.title === 'Backup missed' && n.user_id === 1);
assert.deepStrictEqual([backup.type, backup.category, backup.priority, backup.url], ['OPERATOR_ALERT', 'admin', 'critical', 'https://openvibe.network/status']);
assert.deepStrictEqual([backup.type, backup.category, backup.priority, backup.url], ['OPERATOR_ALERT', 'admin', 'critical', 'https://openvibe.network/status']);
- assert.match(backup.message, /No successful backup in 26 hours\. Service: host\. Firing since 2026-09-26T05:00:00Z\./);
- assert.strictEqual(sent.find((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed').priority, 'high', 'a warning pages at high');
- assert.deepStrictEqual([backup.silent, sent.find((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed').silent], [false, true], 'only a critical alert is pushed');
- const warnIds = sent.filter((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed').map((n, i) => n).length;
+ assert.strictEqual(backup.message, 'host · OpenVibeBackupMissed summary · No successful backup in 26 hours.');
+ assert.strictEqual(backup.rich_content.kind, 'ops_alert');
+ assert.strictEqual(backup.rich_content.state, 'firing');
+ assert.strictEqual(sent.find((n) => n.title === 'Browser check failed').priority, 'high', 'a warning pages at high');
+ assert.deepStrictEqual([backup.silent, sent.find((n) => n.title === 'Browser check failed').silent], [false, true], 'only a critical alert is pushed');
+ const warnIds = sent.filter((n) => n.title === 'Browser check failed').map((n, i) => n).length;
assert.strictEqual(warnIds, 2);
assert.strictEqual(warnIds, 2);
// The same set again: nothing new.
// The same set again: nothing new.
@@ -97,7 +99,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
@@ -97,7 +99,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
assert.deepStrictEqual(r.body, { ok: true, firing: 1, opened: 0, reminded: 0, resolved: 1, notified: 0 });
assert.deepStrictEqual(r.body, { ok: true, firing: 1, opened: 0, reminded: 0, resolved: 1, notified: 0 });
assert.strictEqual(sent.length, 0, 'no Resolved entry piles up');
assert.strictEqual(sent.length, 0, 'no Resolved entry piles up');
assert.strictEqual(revised.length, 2, 'one revision per operator');
assert.strictEqual(revised.length, 2, 'one revision per operator');
- assert.ok(revised.every((x) => x.title === 'Resolved: OpenVibeBrowserCheckFailed' && x.icon === '✅' && x.is_read === 1), JSON.stringify(revised));
+ assert.ok(revised.every((x) => x.title === 'Resolved: Browser check failed' && x.icon === '✅' && x.is_read === 1), JSON.stringify(revised));
assert.match(revised[0].message, /\(resolved after \d+ min\)\./);
assert.match(revised[0].message, /\(resolved after \d+ min\)\./);
const warnNotices = revised.map((x) => x.id).sort();
const warnNotices = revised.map((x) => x.id).sort();
@@ -106,7 +108,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
@@ -106,7 +108,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
const later = Date.now() + alerts.REMIND_MS + 1000;
const later = Date.now() + alerts.REMIND_MS + 1000;
let out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: (u, n) => { sent.push({ user_id: u, ...n }); return { id: `r${sent.length}` }; }, now: later });
let out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: (u, n) => { sent.push({ user_id: u, ...n }); return { id: `r${sent.length}` }; }, now: later });
assert.deepStrictEqual(out, { ok: true, firing: 1, opened: 0, reminded: 1, resolved: 0, notified: 2 });
assert.deepStrictEqual(out, { ok: true, firing: 1, opened: 0, reminded: 1, resolved: 0, notified: 2 });
- assert.strictEqual(sent[0].title, 'Still firing: OpenVibeBackupMissed');
+ assert.strictEqual(sent[0].title, 'Still failing: Backup missed');
out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: () => true, now: later + 60000 });
out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: () => true, now: later + 60000 });
assert.strictEqual(out.reminded, 0);
assert.strictEqual(out.reminded, 0);
@@ -122,7 +124,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
@@ -122,7 +124,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
assert.deepStrictEqual([r.body.opened, r.body.notified], [1, 0]);
assert.deepStrictEqual([r.body.opened, r.body.notified], [1, 0]);
assert.strictEqual(sent.length, 0);
assert.strictEqual(sent.length, 0);
assert.deepStrictEqual(revised.map((x) => x.id).sort(), warnNotices, 'the episode keeps its notifications');
assert.deepStrictEqual(revised.map((x) => x.id).sort(), warnNotices, 'the episode keeps its notifications');
- assert.ok(revised.every((x) => x.title === 'Alert: OpenVibeBrowserCheckFailed' && /Fired again 1 time\(s\) within 12 h/.test(x.message)));
+ assert.ok(revised.every((x) => x.title === 'Browser check failed' && /Fired again 1 time\(s\) within 12 h/.test(x.message)));
revised.length = 0;
revised.length = 0;
await post({ source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, auth);
await post({ source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, auth);
assert.match(revised[0].message, /it fired 2 times/);
assert.match(revised[0].message, /it fired 2 times/);
@@ -139,7 +141,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
@@ -139,7 +141,7 @@ const alert = (name, extra = {}) => ({ fingerprint: crypto.createHash('sha256').
sent.length = 0;
sent.length = 0;
out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed'), alert('OpenVibeBrowserCheckFailed', { severity: 'warning' })] }, { notify: (u, n) => { sent.push(n); return { id: `x${sent.length}` }; }, now: much });
out = await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed'), alert('OpenVibeBrowserCheckFailed', { severity: 'warning' })] }, { notify: (u, n) => { sent.push(n); return { id: `x${sent.length}` }; }, now: much });
assert.deepStrictEqual([out.opened, out.notified], [1, 2]);
assert.deepStrictEqual([out.opened, out.notified], [1, 2]);
- assert.ok(sent.every((n) => n.title === 'Alert: OpenVibeBrowserCheckFailed'));
+ assert.ok(sent.every((n) => n.title === 'Browser check failed'));
await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: () => null, now: much + 1000 });
await alerts.receive(db, { source: 'prometheus', alerts: [alert('OpenVibeBackupMissed')] }, { notify: () => null, now: much + 1000 });
// An empty set resolves everything from that source; the list keeps them.
// An empty set resolves everything from that source; the list keeps them.