11# webhook-spark
22
3- ** ASCII sparklines, gauges, dashboards & threshold alerts for Discord/Slack/Telegram, LCD screens, IoT & AI agents.**
3+ ** Zero-dep terminal sparklines, gauges, kaomoji, heatmaps, tables, histograms & dashboards for Discord/Slack/Telegram, LCD screens, IoT & AI agents.**
44
5- Zero dependencies. TypeScript first. Under 15KB .
5+ Zero dependencies. TypeScript first. Under 20KB .
66
77## Who is this for?
88
9- - ** Homelab / DevOps** -- server monitoring alerts with sparklines and threshold status
9+ - ** Homelab / DevOps** -- server monitoring alerts with sparklines, heatmaps, and threshold status
1010- ** LCD / OLED hackers** -- fixed-width output that fits 16x2, 20x4, and SSD1306 displays
1111- ** DIY / IoT makers** -- multi-sensor dashboards for greenhouses, aquariums, server racks
1212- ** AI agent builders** -- compact metric summaries that fit in LLM context windows
13+ - ** Dashboard addicts** -- kaomoji status faces, histograms, comparison charts, all in pure text
1314
1415## Installation
1516
@@ -21,34 +22,32 @@ bun add @adametherzlab/webhook-spark
2122## Quick Start
2223
2324``` typescript
24- import { spark , gauge , stats , sparkWithStatus , dashboard , sendWebhook } from ' @adametherzlab/webhook-spark' ;
25+ import { spark , gauge , kaomoji , kaomojiStatus , heatmap , dashboard } from ' @adametherzlab/webhook-spark' ;
2526
2627// Sparkline from numbers
2728spark ([10 , 25 , 60 , 85 , 90 , 45 , 30 ]);
2829// => "▁▂▅▇█▃▂"
2930
30- // Progress gauge (battery, tank level, task completion)
31+ // Progress gauge
3132gauge (75 , 100 );
3233// => "████████████████░░░░ 75%"
3334
34- // Summary statistics
35- stats ([10 , 20 , 30 , 40 , 50 ]).summary ;
36- // => "min=10 max=50 avg=30 p95=48"
37-
38- // Threshold-aware sparkline
39- sparkWithStatus ([45 , 50 , 62 , 78 , 95 ], { warning: 70 , critical: 90 });
40- // => { sparkline: "▂▃▅▆█", status: "critical", emoji: "🔴", ... }
41-
42- // Multi-metric dashboard in one call
43- dashboard ([
44- { name: ' CPU' , values: [45 ,50 ,62 ,78 ], unit: ' %' , thresholds: { warning: 70 , critical: 90 } },
45- { name: ' MEM' , values: [78 ,80 ,82 ,85 ], unit: ' %' , thresholds: { warning: 80 , critical: 95 } },
46- { name: ' DISK' , values: [62 ,63 ,63 ,64 ], unit: ' %' , thresholds: { warning: 85 , critical: 95 } },
47- ]);
48- // =>
49- // CPU 78% ▂▃▅█ ⚠️
50- // MEM 85% ▅▆▇█ ⚠️
51- // DISK 64% ▅▅▅▅ ✅
35+ // Kaomoji mood faces
36+ kaomoji (" happy" ); // => "(*^▽^*)"
37+ kaomoji (" happy" , { theme: " cats" }); // => "ᓚᘏᗢ"
38+
39+ // Status kaomoji from a value
40+ kaomojiStatus (95 , 100 , { thresholds: { warning: 70 , critical: 90 } });
41+ // => { face: "(╥_╥)", mood: "critical" }
42+
43+ // Heatmap grid
44+ heatmap ([[2 ,5 ,8 ],[1 ,7 ,9 ],[3 ,6 ,7 ]], {
45+ showLabels: true , rowLabels: [" Mon" ," Tue" ," Wed" ], colLabels: [" AM" ," PM" ," NT" ]
46+ });
47+ // => AM PM NT
48+ // Mon ░ ▒ ▓
49+ // Tue ░ ▓ █
50+ // Wed ░ ▒ ▓
5251```
5352
5453## API Reference
@@ -64,134 +63,222 @@ spark([1, 5, 2, 8, 3, 7]); // => "▁▅▂█▃▆"
6463``` typescript
6564gauge (75 , 100 ) // => "████████████████░░░░ 75%"
6665gauge (3.7 , 4.2 , { label: " BATT" }) // => "BATT ██████████████████░░ 88%"
67- gauge (6 , 20 , { width: 10 , fill: " #" , empty: " ." }) // => "###....... 30%"
68-
69- // Threshold alerts
7066gauge (92 , 100 , { thresholds: { warning: 70 , critical: 90 } })
7167// => "████████████████████ 92% CRITICAL"
7268```
7369
74- ** LCD use case: ** ` lcd.print(gauge(sensorVal, 1023, { width: 16 })) ` -- fits a 16-char LCD line .
70+ Options: ` width ` , ` fill ` , ` empty ` , ` showPercent ` , ` showValue ` , ` label ` , ` thresholds ` .
7571
76- Options: ` width ` (default 20), ` fill ` (default ` █ ` ), ` empty ` (default ` ░ ` ), ` showPercent ` (default true), ` showValue ` , ` label ` , ` thresholds ` .
72+ ### ` kaomoji(mood, options?) ` -- Mood Kaomoji
7773
78- ### ` stats(values, options?) ` -- Summary Statistics
74+ Returns a kaomoji face for the given mood. 5 themes, 13 moods.
7975
8076``` typescript
81- const s = stats ([10 , 20 , 30 , 40 , 50 ]);
82- // s.min=10, s.max=50, s.avg=30, s.median=30, s.stdDev=14.14
83- // s.percentiles = { 95: 48 }
84- // s.summary = "min=10 max=50 avg=30 p95=48"
77+ kaomoji (" happy" ) // => "(*^▽^*)"
78+ kaomoji (" happy" , { theme: " cats" }) // => "ᓚᘏᗢ"
79+ kaomoji (" critical" , { theme: " bears" }) // => "ʕ×ᴥ×ʔ"
80+ kaomoji (" celebrating" ) // => "☆*:.。.o(≧▽≦)o.。.:*☆"
81+ kaomoji (" sleeping" , { theme: " minimal" }) // => "-_-zzz"
82+ ```
83+
84+ ** Moods:** ` happy ` , ` ok ` , ` warning ` , ` critical ` , ` sad ` , ` angry ` , ` love ` , ` surprised ` , ` sleeping ` , ` working ` , ` celebrating ` , ` confused ` , ` dead `
85+
86+ ** Themes:** ` classic ` , ` cats ` , ` bears ` , ` stars ` , ` minimal `
8587
86- // Custom percentiles
87- stats (data , { percentiles: [50 , 90 , 99 ], decimals: 1 });
88+ ### ` kaomojiAll(mood, options?) ` -- All Faces for a Mood
89+
90+ ``` typescript
91+ kaomojiAll (" happy" );
92+ // => ["(*^▽^*)", "(´。• ᵕ •。`)", "(✿◠‿◠)", "(❁´◡`❁)"]
8893```
8994
90- ** AI agent use case: ** ` stats([...tokenCosts]).summary ` -- one-line data summary an LLM can reason about.
95+ ### ` kaomojiStatus(value, max, options?) ` -- Value-to-Mood Mapping
9196
92- ### ` sparkWithStatus(values, thresholds) ` -- Threshold-Aware Sparkline
97+ Maps a numeric value to a mood face based on thresholds. Compose with dashboard output.
98+
99+ ``` typescript
100+ kaomojiStatus (50 , 100 ) // => { face: "(・_・)", mood: "ok" }
101+ kaomojiStatus (95 , 100 , { thresholds: { warning: 70 , critical: 90 } })
102+ // => { face: "(╥_╥)", mood: "critical" }
103+
104+ // Compose with other metrics:
105+ ` CPU 85% ${kaomojiStatus (85 , 100 ).face } ` // => "CPU 85% (・_・;)"
106+ ```
107+
108+ ### ` kaomojiThemes() ` -- List Available Themes
109+
110+ ``` typescript
111+ kaomojiThemes (); // => ["classic", "cats", "bears", "stars", "minimal"]
112+ ```
113+
114+ ### ` heatmap(data, options?) ` -- 2D Grid Heatmap
115+
116+ GitHub contribution graph style, pure text. Rows x columns of shade characters.
117+
118+ ``` typescript
119+ heatmap ([[2 ,5 ,8 ,3 ],[1 ,7 ,9 ,4 ],[3 ,6 ,7 ,2 ]], {
120+ showLabels: true ,
121+ rowLabels: [" Mon" ," Tue" ," Wed" ],
122+ colLabels: [" 00" ," 06" ," 12" ," 18" ]
123+ });
124+ // => 00 06 12 18
125+ // Mon ░ ▒ ▓ ░
126+ // Tue ░ ▓ █ ▒
127+ // Wed ░ ▒ ▓ ░
128+ ```
129+
130+ Options: ` chars ` (default ` [" ","░","▒","▓","█"] ` ), ` showLabels ` , ` rowLabels ` , ` colLabels ` , ` min ` , ` max ` .
131+
132+ ### ` miniTable(rows, options?) ` -- Compact Box-Drawing Table
133+
134+ 4 border styles: ` single ` , ` double ` , ` rounded ` , ` none ` .
135+
136+ ``` typescript
137+ miniTable ([[" Metric" ," Value" ," Status" ],[" CPU" ," 78%" ," OK" ]], { header: true , border: " rounded" });
138+ // => ╭────────┬───────┬────────╮
139+ // │ Metric │ Value │ Status │
140+ // ├────────┼───────┼────────┤
141+ // │ CPU │ 78% │ OK │
142+ // ╰────────┴───────┴────────╯
143+ ```
144+
145+ Options: ` border ` , ` align ` (per-column), ` header ` , ` compact ` , ` maxWidth ` .
146+
147+ ### ` kvTable(entries) ` -- Key-Value Table
148+
149+ ``` typescript
150+ kvTable ([{key:" CPU" ,value:" 78%" },{key:" MEM" ,value:" 4.2GB" }]);
151+ // => ┌─────┬───────┐
152+ // │ CPU │ 78% │
153+ // │ MEM │ 4.2GB │
154+ // └─────┴───────┘
155+ ```
156+
157+ ### ` histogram(values, options?) ` -- Frequency Distribution
158+
159+ Groups values into bins, renders horizontal bar chart of frequencies.
160+
161+ ``` typescript
162+ histogram ([1 ,1 ,2 ,2 ,2 ,3 ,3 ,3 ,3 ,4 ,5 ,5 ,5 ,5 ,5 ], { bins: 5 });
163+ // => 1.0-1.8 ████████ 2
164+ // 1.8-2.6 ████████████ 3
165+ // 2.6-3.4 ████████████████ 4
166+ // 3.4-4.2 ████ 1
167+ // 4.2-5.0 ████████████████████ 5
168+
169+ histogram (latencies , { bins: 10 , percentages: true , fill: " #" });
170+ ```
171+
172+ Options: ` bins ` (default 8), ` barWidth ` (default 20), ` showCounts ` , ` fill ` , ` showBounds ` , ` percentages ` .
173+
174+ ### ` compare(label1, val1, label2, val2, options?) ` -- Side-by-Side Comparison
175+
176+ Visual before/after with delta, percentage change, and direction arrow.
93177
94178``` typescript
95- sparkWithStatus ([45 , 50 , 62 , 78 , 95 ], { warning: 70 , critical: 90 })
96- // => {
97- // sparkline: "▂▃▅▆█",
98- // status: "critical",
99- // emoji: "🔴",
100- // color: 0xe74c3c, // Discord embed color
101- // breachCount: 2,
102- // breachPercent: 40
103- // }
179+ compare (" Before" , 45 , " After" , 78 );
180+ // => { display: "Before ██████████████████ 45\nAfter ██████████████████████████████ 78\n↑ +33 (+73.3%)",
181+ // delta: 33, deltaPercent: 73.3, direction: "up", arrow: "↑" }
104182
105- // Inverted mode: low values are bad (disk space, battery)
106- sparkWithStatus ([15 , 10 , 5 , 3 ], { warning: 10 , critical: 5 , invert: true })
107- // => status: "critical"
183+ compare (" Plan" , 100 , " Actual" , 87 , { mode: " compact" , unit: " %" });
184+ // => { display: "Plan 100% vs Actual 87% ↓-13 (-13.0%)", ... }
185+
186+ compare (" Week1" , [10 ,20 ,30 ], " Week2" , [15 ,25 ,35 ], { mode: " spark" });
187+ // => { display: "Week1 ▁▅█ avg=20.0\nWeek2 ▁▅█ avg=25.0\n↑ +5 (+25.0%)", ... }
188+ ```
189+
190+ Options: ` barWidth ` , ` showDelta ` , ` showPercent ` , ` unit ` , ` mode ` (` "bars" ` | ` "spark" ` | ` "compact" ` ).
191+
192+ ### ` stats(values, options?) ` -- Summary Statistics
193+
194+ ``` typescript
195+ stats ([10 , 20 , 30 , 40 , 50 ]).summary ;
196+ // => "min=10 max=50 avg=30 p95=48"
108197```
109198
110- Discord embed color auto-maps: green (ok) / yellow (warning) / red (critical).
199+ ### ` sparkWithStatus(values, thresholds) ` -- Threshold-Aware Sparkline
200+
201+ ``` typescript
202+ sparkWithStatus ([45 , 50 , 62 , 78 , 95 ], { warning: 70 , critical: 90 });
203+ // => { sparkline: "▂▃▅▆█", status: "critical", emoji: "🔴", color: 0xe74c3c, breachCount: 2 }
204+ ```
111205
112206### ` dashboard(metrics, options?) ` -- Multi-Metric Display
113207
114208``` typescript
115- // Full mode (with sparklines)
116209dashboard ([
117210 { name: ' CPU' , values: [45 ,50 ,62 ,78 ], unit: ' %' , thresholds: { warning: 70 , critical: 90 } },
118211 { name: ' MEM' , values: [78 ,80 ,82 ,85 ], unit: ' %' , thresholds: { warning: 80 , critical: 95 } },
119212 { name: ' DISK' , values: [62 ,63 ,63 ,64 ], unit: ' %' , thresholds: { warning: 85 , critical: 95 } },
120- { name: ' TEMP' , values: [42 ,44 ,45 ,43 ], unit: ' °C' , thresholds: { warning: 60 , critical: 75 } },
121213]);
122214// CPU 78% ▂▃▅█ ⚠️
123215// MEM 85% ▅▆▇█ ⚠️
124216// DISK 64% ▅▅▅▅ ✅
125- // TEMP 43°C ▃▄▅▃ ✅
126-
127- // Compact mode (for 20x4 LCD or AI context)
128- dashboard ([... ], { compact: true });
129- // CPU 78% ⚠️
130- // MEM 85% ⚠️
131- // DISK 64% ✅
132- // TEMP 43°C ✅
133217```
134218
135- ** LCD use case:** Render to 20x4 or 128x64 OLED in one call.
136- ** AI agent use case:** Paste entire system status into context in 4 lines.
137-
138219### ` barChart(entries, options?) ` -- Horizontal Bar Chart
139220
140221``` typescript
141222barChart ([
142223 { label: ' GET' , value: 150 },
143224 { label: ' POST' , value: 80 },
144- { label: ' PUT' , value: 30 },
145225], { maxBarWidth: 15 });
146- // GET ███████████████ 150
147- // POST ████████ 80
148- // PUT ███ 30
149226```
150227
151228### ` trend(values, window?) ` -- Trend Arrow
152229
153230``` typescript
154231trend ([10 , 20 , 30 ]); // => "↑"
155232trend ([30 , 20 , 10 ]); // => "↓"
156- trend ([10 , 10 , 10 ]); // => "→"
157233```
158234
159235### ` sendWebhook(payload, config) ` -- Webhook Delivery
160236
161237Supports Discord, Slack, and Telegram webhooks with validation, retry, and timeout.
162238
163- ``` typescript
164- await sendWebhook (
165- { timestamp: new Date (), metricName: ' cpu' , sparkline: spark (cpuData ), rawValues: cpuData },
166- { endpoint: ' https://discord.com/api/webhooks/...' , provider: ' discord' }
167- );
168- ```
169-
170- ### ` generateSparkline(data, options) ` -- Advanced Sparkline
171-
172- Full control: custom character sets, interpolation, axis, outlier detection.
173-
174- ## Use Case Examples
239+ ## Use Case Gallery
175240
176241### IoT Greenhouse Dashboard
177242
178243``` typescript
179244const sensors = [
180245 { name: ' SOIL' , values: moistureReadings , unit: ' %' , thresholds: { warning: 30 , critical: 15 , invert: true } },
181- { name: ' TEMP' , values: tempReadings , unit: ' °C' , thresholds: { warning: 35 , critical: 40 } },
182- { name: ' HUM' , values: humidityReadings , unit: ' %' },
246+ { name: ' TEMP' , values: tempReadings , unit: ' C' , thresholds: { warning: 35 , critical: 40 } },
183247];
184- console .log (dashboard (sensors ));
248+ const status = dashboard (sensors );
249+ const face = kaomojiStatus (moistureReadings .at (- 1 ), 100 , { theme: " cats" }).face ;
250+ console .log (` ${status }\n ${face } ` );
185251```
186252
187253### AI Agent System Prompt
188254
189255``` typescript
190256const status = dashboard ([
191257 { name: ' Tokens' , values: tokenHistory , unit: ' K' , thresholds: { warning: 80 , critical: 95 } },
192- { name: ' Tasks' , values: taskCounts , thresholds: { warning: 50 , critical: 100 } },
193258], { compact: true });
194- // Inject into system prompt: 2 lines, minimal tokens
259+ const face = kaomojiStatus (tokenHistory .at (- 1 ), 100 ).face ;
260+ // => "Tokens 82K ⚠️ (・_・;)" -- 1 line, minimal tokens
261+ ```
262+
263+ ### Server Monitoring with Heatmap
264+
265+ ``` typescript
266+ // 7-day x 24-hour load heatmap
267+ const weeklyLoad = [ /* 7 arrays of 24 hourly values */ ];
268+ console .log (heatmap (weeklyLoad , {
269+ showLabels: true ,
270+ rowLabels: [" Mon" ," Tue" ," Wed" ," Thu" ," Fri" ," Sat" ," Sun" ],
271+ }));
272+ ```
273+
274+ ### Performance Comparison
275+
276+ ``` typescript
277+ const result = compare (" v1.2" , responseTimesOld , " v1.3" , responseTimesNew , { mode: " spark" , unit: " ms" });
278+ console .log (result .display );
279+ // v1.2 ▃▅▇█▆▅ avg=245.0
280+ // v1.3 ▂▃▅▆▄▃ avg=189.0
281+ // ↓ -56 (-22.9%)
195282```
196283
197284### Arduino LCD (16x2)
@@ -202,6 +289,22 @@ const line2 = `T:${temp}C ${trend(tempHistory)}`;
202289lcd .print (line1 + ' \n ' + line2 );
203290```
204291
292+ ## Why webhook-spark?
293+
294+ | Feature | webhook-spark | blessed-contrib | cli-table3 | ink |
295+ | ---------| :---:| :---:| :---:| :---:|
296+ | Zero deps | Yes | No (17) | No (4) | No (11) |
297+ | Sparklines | Yes | Yes | No | No |
298+ | Gauges | Yes | Yes | No | No |
299+ | Kaomoji | Yes | No | No | No |
300+ | Heatmaps | Yes | Yes | No | No |
301+ | Tables | Yes | No | Yes | No |
302+ | Histograms | Yes | Yes | No | No |
303+ | Comparisons | Yes | No | No | No |
304+ | Webhooks | Yes | No | No | No |
305+ | Bundle size | <20KB | 2.5MB | 180KB | 400KB |
306+ | Maintained | Yes | No | Minimal | Yes |
307+
205308## License
206309
207310MIT
0 commit comments