Shell 1.11.0: alerts — a module flashes every dashboard listening on one of six channels
A module that sees something a tech must act on now (a timer overrun, a meter past its limit, timecode gone) raises an alert on a channel, 1–6, and every dashboard listening to that channel flashes: the menu bar — brought out for the duration if it was hidden — and a frame round the screen alternate between the level's two colours, with the alert's text bold across the bar. Low fades slowly, medium faster, critical snaps with no fade and floods the board behind the tiles too, in a muted cut that shows faintly through the tile panels without costing their content any contrast. Server: the hub. One alert per channel (a new one replaces the last), timed out server-side (1–600 s, 15 by default, 0 = held until cleared), broadcast over the shell stream as `alert` / `alert-clear`; GET /api/alerts for a dashboard that opens or reconnects mid-alert; POST /api/alerts and DELETE /api/alerts/<n>, open like a layout save. Modules raise from their server entry with shell.alert() / shell.clearAlert() or from a tile with moduleApi.alert() / moduleApi.clearAlert(); a module's alerts end when it stops. Colours live as alerts.colors in the data directory's proddash.json. Dashboard: the Layouts menu gains a folded Alerts row with six numbered toggles; the channel list saves with the layout, local and named (the server keeps it in a saved layout). Admin: a folded Alerts section with two colour pickers per level, a swatch running each level's cadence, and a Test row. Docs: docs/ALERTS.md (the feature), a "Raising alerts" section in the module guide, README. Tools: tools/alert.js raises/clears/lists from the command line; tools/alerts.test.js drives it all end to end. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
a3500f98b1
commit
5ca0a672fa
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Raise, clear or list ProdDash alerts from the command line — for trying a
|
||||
* dashboard's channels and the admin colours, or from a script on the
|
||||
* machine running ProdDash (anything that can POST JSON can raise one).
|
||||
*
|
||||
* node tools/alert.js "Service over by 5:00" # low, channel 1, 15 s
|
||||
* node tools/alert.js -c 2 -l critical -d 0 "LTC lost" # held until cleared
|
||||
* node tools/alert.js -c 2 --clear
|
||||
* node tools/alert.js --list
|
||||
*
|
||||
* Options: -c/--channel 1–6 (default 1), -l/--level low|medium|critical
|
||||
* (default low), -d/--duration seconds, 0 to hold until cleared (default the
|
||||
* server's 15), --url http://host:port (default http://127.0.0.1:24500, or
|
||||
* $PRODDASH_URL), --clear, --list. Node 18+, no dependencies.
|
||||
*/
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const opts = { channel: 1, level: 'low', duration: undefined, url: process.env.PRODDASH_URL || 'http://127.0.0.1:24500', clear: false, list: false };
|
||||
const words = [];
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const a = args[i];
|
||||
const next = () => args[++i];
|
||||
if (a === '-c' || a === '--channel') opts.channel = Number(next());
|
||||
else if (a === '-l' || a === '--level') opts.level = String(next() || '');
|
||||
else if (a === '-d' || a === '--duration') opts.duration = Number(next());
|
||||
else if (a === '--url') opts.url = String(next() || opts.url);
|
||||
else if (a === '--clear') opts.clear = true;
|
||||
else if (a === '--list') opts.list = true;
|
||||
else if (a === '-h' || a === '--help') {
|
||||
console.log('usage: node tools/alert.js [-c 1-6] [-l low|medium|critical] [-d seconds] [--url http://host:port] "text"\n'
|
||||
+ ' node tools/alert.js -c <n> --clear\n'
|
||||
+ ' node tools/alert.js --list');
|
||||
process.exit(0);
|
||||
} else words.push(a);
|
||||
}
|
||||
const base = opts.url.replace(/\/+$/, '');
|
||||
|
||||
async function call(path, init) {
|
||||
const res = await fetch(base + path, init);
|
||||
const body = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(body.error || `HTTP ${res.status}`);
|
||||
return body;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
if (opts.list) {
|
||||
const body = await call('/api/alerts');
|
||||
const active = body.active || [];
|
||||
console.log(`${active.length} active alert${active.length === 1 ? '' : 's'}; colours:`,
|
||||
Object.entries(body.colors || {}).map(([k, v]) => `${k} ${v.join('→')}`).join(', '));
|
||||
for (const a of active) {
|
||||
console.log(` channel ${a.channel} ${a.level.padEnd(8)} "${a.text}" from ${a.source}`
|
||||
+ (a.remainingMs === null ? ' (until cleared)' : ` (${Math.ceil(a.remainingMs / 1000)} s left)`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (opts.clear) {
|
||||
const body = await call(`/api/alerts/${opts.channel}`, { method: 'DELETE' });
|
||||
console.log(body.cleared ? `cleared channel ${opts.channel}` : `nothing was on channel ${opts.channel}`);
|
||||
return;
|
||||
}
|
||||
const text = words.join(' ').trim();
|
||||
if (!text) {
|
||||
console.error('say what happened: node tools/alert.js -c 1 -l critical "LTC lost"');
|
||||
process.exit(2);
|
||||
}
|
||||
const payload = { channel: opts.channel, level: opts.level, text, source: 'cli' };
|
||||
if (opts.duration !== undefined) payload.duration = opts.duration;
|
||||
const body = await call('/api/alerts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
const a = body.alert;
|
||||
console.log(`raised ${a.level} on channel ${a.channel}: "${a.text}"`
|
||||
+ (a.remainingMs === null ? ' (until cleared)' : ` (${a.duration} s)`));
|
||||
})().catch((err) => {
|
||||
console.error(err.message || String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user