|
1 | | -# webhook-spark ⚡ |
| 1 | +# webhook-spark |
2 | 2 |
|
3 | | -**Send minimalist homelab alerts with ASCII sparklines to Discord/Slack** |
| 3 | +**ASCII sparklines, gauges, dashboards & threshold alerts for Discord/Slack/Telegram, LCD screens, IoT & AI agents.** |
4 | 4 |
|
5 | | -## ✨ Features |
| 5 | +Zero dependencies. TypeScript first. Under 15KB. |
6 | 6 |
|
7 | | -- 📊 **Beautiful ASCII sparklines** – Turn boring number arrays into visual trends |
8 | | -- 🔌 **Webhook support** – Discord and Slack out of the box |
9 | | -- 🚀 **Zero dependencies** – Uses only Node.js/Bun built-ins |
10 | | -- 📦 **Tiny footprint** – Less than 10KB minified |
11 | | -- 🛡️ **TypeScript first** – Full type safety and autocomplete |
12 | | -- 🎨 **Customizable** – Multiple character sets and styling options |
| 7 | +## Who is this for? |
13 | 8 |
|
14 | | -## 📦 Installation |
| 9 | +- **Homelab / DevOps** -- server monitoring alerts with sparklines and threshold status |
| 10 | +- **LCD / OLED hackers** -- fixed-width output that fits 16x2, 20x4, and SSD1306 displays |
| 11 | +- **DIY / IoT makers** -- multi-sensor dashboards for greenhouses, aquariums, server racks |
| 12 | +- **AI agent builders** -- compact metric summaries that fit in LLM context windows |
| 13 | + |
| 14 | +## Installation |
15 | 15 |
|
16 | 16 | ```bash |
17 | | -# Using Bun (recommended) |
18 | | -bun add webhook-spark |
| 17 | +bun add @adametherzlab/webhook-spark |
| 18 | +# or: npm install @adametherzlab/webhook-spark |
| 19 | +``` |
19 | 20 |
|
20 | | -# Using npm |
21 | | -npm install webhook-spark |
| 21 | +## Quick Start |
22 | 22 |
|
23 | | -# Using yarn |
24 | | -yarn add webhook-spark |
| 23 | +```typescript |
| 24 | +import { spark, gauge, stats, sparkWithStatus, dashboard, sendWebhook } from '@adametherzlab/webhook-spark'; |
| 25 | + |
| 26 | +// Sparkline from numbers |
| 27 | +spark([10, 25, 60, 85, 90, 45, 30]); |
| 28 | +// => "▁▂▅▇█▃▂" |
| 29 | + |
| 30 | +// Progress gauge (battery, tank level, task completion) |
| 31 | +gauge(75, 100); |
| 32 | +// => "████████████████░░░░ 75%" |
| 33 | + |
| 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% ▅▅▅▅ ✅ |
25 | 52 | ``` |
26 | 53 |
|
27 | | -## 🚀 Quick Start |
| 54 | +## API Reference |
| 55 | + |
| 56 | +### `spark(values)` -- Simple Sparkline |
28 | 57 |
|
29 | 58 | ```typescript |
30 | | -// REMOVED external import: import { generateSparkline, sendWebhook } from 'webhook-spark'; |
31 | | - |
32 | | -// Create a sparkline from your metrics |
33 | | -const cpuUsage = [10, 25, 60, 85, 90, 45, 30]; |
34 | | -const sparkline = generateSparkline(cpuUsage); |
35 | | - |
36 | | -// Send to Discord |
37 | | -await sendWebhook({ |
38 | | - url: 'https://discord.com/api/webhooks/your-webhook-id', |
39 | | - provider: 'discord', |
40 | | - content: `CPU usage: ${sparkline}`, |
41 | | - username: 'Server Monitor' |
42 | | -}); |
43 | | - |
44 | | -console.log(`📈 Sparkline sent: ${sparkline}`); |
45 | | -// Output: 📈 Sparkline sent: ▁▂▅▇██▃▂ |
| 59 | +spark([1, 5, 2, 8, 3, 7]); // => "▁▅▂█▃▆" |
46 | 60 | ``` |
47 | 61 |
|
48 | | -## 📖 API Reference |
| 62 | +### `gauge(value, max, options?)` -- Progress / Level Gauge |
| 63 | + |
| 64 | +```typescript |
| 65 | +gauge(75, 100) // => "████████████████░░░░ 75%" |
| 66 | +gauge(3.7, 4.2, { label: "BATT" }) // => "BATT ██████████████████░░ 88%" |
| 67 | +gauge(6, 20, { width: 10, fill: "#", empty: "." }) // => "###....... 30%" |
| 68 | + |
| 69 | +// Threshold alerts |
| 70 | +gauge(92, 100, { thresholds: { warning: 70, critical: 90 } }) |
| 71 | +// => "████████████████████ 92% CRITICAL" |
| 72 | +``` |
49 | 73 |
|
50 | | -### Sparkline Generation |
| 74 | +**LCD use case:** `lcd.print(gauge(sensorVal, 1023, { width: 16 }))` -- fits a 16-char LCD line. |
| 75 | + |
| 76 | +Options: `width` (default 20), `fill` (default `█`), `empty` (default `░`), `showPercent` (default true), `showValue`, `label`, `thresholds`. |
| 77 | + |
| 78 | +### `stats(values, options?)` -- Summary Statistics |
51 | 79 |
|
52 | 80 | ```typescript |
53 | | -// REMOVED external import: import { generateSparkline, generateSparklineWithOutliers } from 'webhook-spark'; |
54 | | - |
55 | | -// Basic sparkline |
56 | | -const sparkline = generateSparkline([1, 2, 3, 4, 5]); |
57 | | -// Returns: ▁▂▃▄▅ |
58 | | - |
59 | | -// With custom options |
60 | | -const custom = generateSparkline([10, 50, 90], { |
61 | | - characterSet: '·∙●○◉◎', |
62 | | - minValue: 0, |
63 | | - maxValue: 100 |
64 | | -}); |
65 | | - |
66 | | -// Handle outliers |
67 | | -const withOutliers = generateSparklineWithOutliers( |
68 | | - [1, 1000, 2, 3, 4], |
69 | | - { threshold: 2 } // Values > 2 standard deviations marked |
70 | | -); |
| 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" |
| 85 | + |
| 86 | +// Custom percentiles |
| 87 | +stats(data, { percentiles: [50, 90, 99], decimals: 1 }); |
71 | 88 | ``` |
72 | 89 |
|
73 | | -### Webhook Delivery |
| 90 | +**AI agent use case:** `stats([...tokenCosts]).summary` -- one-line data summary an LLM can reason about. |
| 91 | + |
| 92 | +### `sparkWithStatus(values, thresholds)` -- Threshold-Aware Sparkline |
74 | 93 |
|
75 | 94 | ```typescript |
76 | | -// REMOVED external import: import { sendWebhook } from 'webhook-spark'; |
77 | | - |
78 | | -// Discord example |
79 | | -await sendWebhook({ |
80 | | - url: 'DISCORD_WEBHOOK_URL', |
81 | | - provider: 'discord', |
82 | | - content: 'Server alert!', |
83 | | - embeds: [{ |
84 | | - title: 'CPU Usage', |
85 | | - description: generateSparkline([10, 25, 60, 85]), |
86 | | - color: 0xff0000, |
87 | | - timestamp: new Date().toISOString() |
88 | | - }] |
89 | | -}); |
90 | | - |
91 | | -// Slack example |
92 | | -await sendWebhook({ |
93 | | - url: 'SLACK_WEBHOOK_URL', |
94 | | - provider: 'slack', |
95 | | - content: 'Daily metrics', |
96 | | - blocks: [{ |
97 | | - type: 'section', |
98 | | - text: { type: 'mrkdwn', text: `*Memory usage:* ${generateSparkline([30, 45, 60])}` } |
99 | | - }] |
100 | | -}); |
| 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 | +// } |
| 104 | + |
| 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" |
101 | 108 | ``` |
102 | 109 |
|
103 | | -### Type Utilities |
| 110 | +Discord embed color auto-maps: green (ok) / yellow (warning) / red (critical). |
| 111 | + |
| 112 | +### `dashboard(metrics, options?)` -- Multi-Metric Display |
104 | 113 |
|
105 | 114 | ```typescript |
106 | | -// REMOVED external import: import { isNumericArray, isWebhookConfig } from 'webhook-spark'; |
107 | | - |
108 | | -// Type guards for validation |
109 | | -if (isNumericArray(data)) { |
110 | | - // data is now typed as readonly number[] |
111 | | - const sparkline = generateSparkline(data); |
112 | | -} |
113 | | - |
114 | | -const config = { url: '...', provider: 'discord' }; |
115 | | -if (isWebhookConfig(config)) { |
116 | | - // config is valid WebhookConfig |
117 | | - await sendWebhook(config); |
118 | | -} |
| 115 | +// Full mode (with sparklines) |
| 116 | +dashboard([ |
| 117 | + { name: 'CPU', values: [45,50,62,78], unit: '%', thresholds: { warning: 70, critical: 90 } }, |
| 118 | + { name: 'MEM', values: [78,80,82,85], unit: '%', thresholds: { warning: 80, critical: 95 } }, |
| 119 | + { 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 } }, |
| 121 | +]); |
| 122 | +// CPU 78% ▂▃▅█ ⚠️ |
| 123 | +// MEM 85% ▅▆▇█ ⚠️ |
| 124 | +// 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 ✅ |
119 | 133 | ``` |
120 | 134 |
|
121 | | -## 🧪 Examples |
| 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. |
122 | 137 |
|
123 | | -### Homelab CPU Monitor |
| 138 | +### `barChart(entries, options?)` -- Horizontal Bar Chart |
124 | 139 |
|
125 | 140 | ```typescript |
126 | | -// REMOVED external import: import { generateSparkline, sendWebhook } from 'webhook-spark'; |
127 | | -import os from 'os'; |
128 | | - |
129 | | -// Simulate collecting CPU metrics |
130 | | -const cpuMetrics = [45, 60, 75, 85, 90, 80, 65]; |
131 | | -const sparkline = generateSparkline(cpuMetrics); |
132 | | - |
133 | | -// Send alert if high usage |
134 | | -if (cpuMetrics[cpuMetrics.length - 1] > 80) { |
135 | | - await sendWebhook({ |
136 | | - url: process.env.DISCORD_WEBHOOK!, |
137 | | - provider: 'discord', |
138 | | - content: `🚨 High CPU usage detected!`, |
139 | | - embeds: [{ |
140 | | - title: 'CPU Trend', |
141 | | - description: `\`${sparkline}\``, |
142 | | - fields: [ |
143 | | - { name: 'Current', value: `${cpuMetrics[cpuMetrics.length - 1]}%`, inline: true }, |
144 | | - { name: 'Peak', value: `${Math.max(...cpuMetrics)}%`, inline: true } |
145 | | - ], |
146 | | - color: 0xff5500 |
147 | | - }] |
148 | | - }); |
149 | | -} |
| 141 | +barChart([ |
| 142 | + { label: 'GET', value: 150 }, |
| 143 | + { label: 'POST', value: 80 }, |
| 144 | + { label: 'PUT', value: 30 }, |
| 145 | +], { maxBarWidth: 15 }); |
| 146 | +// GET ███████████████ 150 |
| 147 | +// POST ████████ 80 |
| 148 | +// PUT ███ 30 |
150 | 149 | ``` |
151 | 150 |
|
152 | | -### Daily Health Report |
| 151 | +### `trend(values, window?)` -- Trend Arrow |
153 | 152 |
|
154 | 153 | ```typescript |
155 | | -// REMOVED external import: import { generateSparkline, generateASCIIArt } from 'webhook-spark'; |
156 | | - |
157 | | -// Create a dashboard-like report |
158 | | -const report = ` |
159 | | -📊 **Daily System Report** |
160 | | -━━━━━━━━━━━━━━━━━━━━ |
161 | | -CPU: ${generateSparkline([10, 25, 40, 60, 45, 30])} |
162 | | -Memory: ${generateSparkline([50, 55, 60, 65, 70, 68])} |
163 | | -Disk: ${generateSparkline([85, 86, 87, 88, 89, 90])} |
164 | | -
|
165 | | -${generateASCIIArt('HEALTHY', { font: 'block' })} |
166 | | -`; |
167 | | - |
168 | | -await sendWebhook({ |
169 | | - url: process.env.SLACK_WEBHOOK!, |
170 | | - provider: 'slack', |
171 | | - content: report |
172 | | -}); |
| 154 | +trend([10, 20, 30]); // => "↑" |
| 155 | +trend([30, 20, 10]); // => "↓" |
| 156 | +trend([10, 10, 10]); // => "→" |
173 | 157 | ``` |
174 | 158 |
|
175 | | -## 🔧 Configuration |
| 159 | +### `sendWebhook(payload, config)` -- Webhook Delivery |
| 160 | + |
| 161 | +Supports Discord, Slack, and Telegram webhooks with validation, retry, and timeout. |
176 | 162 |
|
177 | | -```json |
178 | | -{ |
179 | | - "defaultWebhook": "https://discord.com/api/webhooks/your-id", |
180 | | - "defaultProvider": "discord", |
181 | | - "dataDir": "~/.webhook-spark/data", |
182 | | - "maxEntries": 1000 |
183 | | -} |
| 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 |
| 175 | + |
| 176 | +### IoT Greenhouse Dashboard |
| 177 | + |
| 178 | +```typescript |
| 179 | +const sensors = [ |
| 180 | + { 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: '%' }, |
| 183 | +]; |
| 184 | +console.log(dashboard(sensors)); |
184 | 185 | ``` |
185 | 186 |
|
186 | | -## 🤝 Contributing |
| 187 | +### AI Agent System Prompt |
187 | 188 |
|
188 | | -Found a bug? Have an idea for a new feature? Contributions are welcome! |
| 189 | +```typescript |
| 190 | +const status = dashboard([ |
| 191 | + { name: 'Tokens', values: tokenHistory, unit: 'K', thresholds: { warning: 80, critical: 95 } }, |
| 192 | + { name: 'Tasks', values: taskCounts, thresholds: { warning: 50, critical: 100 } }, |
| 193 | +], { compact: true }); |
| 194 | +// Inject into system prompt: 2 lines, minimal tokens |
| 195 | +``` |
189 | 196 |
|
190 | | -1. Fork the repository |
191 | | -2. Create a feature branch (`git checkout -b cool-new-feature`) |
192 | | -3. Commit your changes (`git commit -am 'Add cool feature'`) |
193 | | -4. Push to the branch (`git push origin cool-new-feature`) |
194 | | -5. Open a Pull Request |
| 197 | +### Arduino LCD (16x2) |
| 198 | + |
| 199 | +```typescript |
| 200 | +const line1 = gauge(analogRead(A0), 1023, { width: 16, showPercent: false }); |
| 201 | +const line2 = `T:${temp}C ${trend(tempHistory)}`; |
| 202 | +lcd.print(line1 + '\n' + line2); |
| 203 | +``` |
195 | 204 |
|
196 | | -## 📄 License |
| 205 | +## License |
197 | 206 |
|
198 | | -MIT © AdametherzLab |
| 207 | +MIT |
199 | 208 |
|
200 | 209 | --- |
201 | 210 |
|
202 | | -Made with ⚡ by homelab enthusiasts for homelab enthusiasts. Keep your servers happy and your alerts beautiful! |
| 211 | +Built for homelabs, hackerspaces, and AI agents. |
0 commit comments