wickchart-alerts-plus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/alerts-plus.mjs +213 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# wickchart-alerts-plus
|
|
2
|
+
|
|
3
|
+
The "pro" alert tier for [wickchart](https://github.com/benyblack/wickchart),
|
|
4
|
+
as an opt-in plugin — core alerts are runtime-only by design, so persistence,
|
|
5
|
+
desktop notifications and webhooks live here instead. Zero dependencies.
|
|
6
|
+
|
|
7
|
+
- **Persistence**: the chart's alert list is mirrored into `localStorage`
|
|
8
|
+
at save points (adds/removes, fires, `sync()`, `detach()`) and re-armed on
|
|
9
|
+
the next page load. Once-fired alerts drop out of storage automatically;
|
|
10
|
+
alerts added directly on the chart are picked up too.
|
|
11
|
+
- **Hidden-tab surfacing**: while the tab is hidden, a fired alert raises a
|
|
12
|
+
desktop notification (with the user's permission) and a short two-tone
|
|
13
|
+
WebAudio beep — no audio files, no dependencies.
|
|
14
|
+
- **Webhook**: an optional URL receives `POST { id, price, when, time, bar,
|
|
15
|
+
key }` on every fire, regardless of tab visibility. Fire-and-forget;
|
|
16
|
+
rejections are swallowed.
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
npm install wickchart wickchart-alerts-plus // the plugin is a separate package
|
|
20
|
+
|
|
21
|
+
import 'wickchart'; // the chart itself
|
|
22
|
+
import { attachAlertsPlus } from 'wickchart-alerts-plus';
|
|
23
|
+
|
|
24
|
+
const chart = document.querySelector('wick-chart');
|
|
25
|
+
const ap = attachAlertsPlus(chart, {
|
|
26
|
+
key: 'BTC:1h', // storage key — one per symbol+timeframe
|
|
27
|
+
notify: true, // desktop notification while hidden
|
|
28
|
+
sound: true, // beep while hidden
|
|
29
|
+
webhook: 'https://example.com/hook', // optional
|
|
30
|
+
});
|
|
31
|
+
await ap.requestNotify(); // ask for the notification permission once
|
|
32
|
+
|
|
33
|
+
ap.add({ price: 100, direction: 'above' }); // persisted, re-armed on reload
|
|
34
|
+
ap.add({ when: 'rsi(close,14) < 30' }); // scripted alerts persist too
|
|
35
|
+
ap.list(); // → serializable snapshots
|
|
36
|
+
ap.remove(id); ap.clear(); ap.sync(); ap.detach();
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Notes:
|
|
40
|
+
|
|
41
|
+
- `storage` and `fetch` are injectable (tests, SSR, custom backends);
|
|
42
|
+
without storage the plugin is memory-only and never throws — private
|
|
43
|
+
mode / quota failures are silently best-effort.
|
|
44
|
+
- Restored alerts re-arm (`fired` resets): a non-`once` alert that fired
|
|
45
|
+
before a reload can fire again after it.
|
|
46
|
+
- Everything on the chart keeps working: `addAlert` / `removeAlert` /
|
|
47
|
+
`clearAlerts` / `getState`, `wick:alert` events, `once` semantics.
|
|
48
|
+
|
|
49
|
+
Peer dependency: wickchart ≥ 1.4.0.
|
package/alerts-plus.mjs
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wickchart-alerts-plus — the "pro" alert tier as a wickchart plugin:
|
|
3
|
+
* core alerts are runtime-only by design, so this adds persistence
|
|
4
|
+
* (localStorage), desktop notifications + a beep while the tab is hidden,
|
|
5
|
+
* and an optional webhook — without the core growing any of it.
|
|
6
|
+
*
|
|
7
|
+
* import { attachAlertsPlus } from 'wickchart-alerts-plus';
|
|
8
|
+
* const ap = attachAlertsPlus(chart, {
|
|
9
|
+
* key: 'BTC:1h', // storage key (default 'wickchart-alerts')
|
|
10
|
+
* notify: true, // desktop notification when the tab is hidden
|
|
11
|
+
* sound: true, // short beep when the tab is hidden
|
|
12
|
+
* webhook: 'https://…', // POST { id, price, when, bar, time } on fire
|
|
13
|
+
* });
|
|
14
|
+
* ap.add({ price: 100, direction: 'above' }); // → persisted, re-armed
|
|
15
|
+
* ap.add({ when: 'rsi(close,14) < 30' }); // scripted alerts too
|
|
16
|
+
* ap.remove(id); ap.clear(); ap.list(); ap.sync(); ap.detach();
|
|
17
|
+
* ap.requestNotify(); // ask for the desktop-notification permission
|
|
18
|
+
*
|
|
19
|
+
* Everything the chart already does still works: addAlert/removeAlert/
|
|
20
|
+
* clearAlerts/getState, wick:alert events, once-semantics. The plugin
|
|
21
|
+
* mirrors the chart's live alert list into storage at save points (plugin
|
|
22
|
+
* ops, fires, detach, sync()) — so alerts added directly on the chart get
|
|
23
|
+
* persisted too, and once-fired ones drop out of storage automatically.
|
|
24
|
+
* Storage is injectable and optional: without it the plugin is in-memory.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const MAX_PERSISTED = 50;
|
|
28
|
+
|
|
29
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v);
|
|
30
|
+
|
|
31
|
+
/** Serializable snapshot of one chart alert (drops compiled/fired). */
|
|
32
|
+
function snapshot(a) {
|
|
33
|
+
const out = { id: String(a.id), once: a.once !== false };
|
|
34
|
+
if (a.when != null) out.when = String(a.when);
|
|
35
|
+
if (isNum(a.price)) {
|
|
36
|
+
out.price = a.price;
|
|
37
|
+
out.direction = a.direction || 'cross';
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function attachAlertsPlus(chart, opts = {}) {
|
|
43
|
+
return new AlertsPlus(chart, opts);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class AlertsPlus {
|
|
47
|
+
constructor(chart, opts = {}) {
|
|
48
|
+
if (!chart || typeof chart.addAlert !== 'function' || typeof chart.addEventListener !== 'function') {
|
|
49
|
+
throw new TypeError('attachAlertsPlus(chart): the chart element is required');
|
|
50
|
+
}
|
|
51
|
+
this._chart = chart;
|
|
52
|
+
this._storage = opts.storage != null ? opts.storage : globalThis.localStorage;
|
|
53
|
+
this._key = typeof opts.key === 'string' && opts.key ? opts.key : 'wickchart-alerts';
|
|
54
|
+
this._notify = opts.notify !== false;
|
|
55
|
+
this._sound = opts.sound !== false;
|
|
56
|
+
this._webhook = typeof opts.webhook === 'string' && opts.webhook ? opts.webhook : null;
|
|
57
|
+
this._fetch = typeof opts.fetch === 'function' ? opts.fetch : null;
|
|
58
|
+
this._audio = null;
|
|
59
|
+
this._onAlert = (e) => this._fire(e.detail);
|
|
60
|
+
chart.addEventListener('wick:alert', this._onAlert);
|
|
61
|
+
this._restore();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* ---------------- public API ---------------- */
|
|
65
|
+
|
|
66
|
+
/** Add an alert through the chart and persist it. Returns the id or null. */
|
|
67
|
+
add(alert) {
|
|
68
|
+
const id = this._chart.addAlert(alert);
|
|
69
|
+
if (id != null) this.sync();
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
remove(id) {
|
|
74
|
+
this._chart.removeAlert(id);
|
|
75
|
+
this.sync();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
clear() {
|
|
79
|
+
this._chart.clearAlerts();
|
|
80
|
+
this.sync();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Live alert list from the chart (read-only view). */
|
|
84
|
+
list() {
|
|
85
|
+
const st = this._chart.getState();
|
|
86
|
+
return (st && Array.isArray(st.alerts) ? st.alerts : []).map((a) => snapshot(a));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Snapshot the chart's current alerts into storage (all of them). */
|
|
90
|
+
sync() {
|
|
91
|
+
this._save(this.list());
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Ask the user for the desktop-notification permission (no-op elsewhere). */
|
|
96
|
+
async requestNotify() {
|
|
97
|
+
try {
|
|
98
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
|
|
99
|
+
return await Notification.requestPermission();
|
|
100
|
+
}
|
|
101
|
+
return typeof Notification !== 'undefined' ? Notification.permission : 'denied';
|
|
102
|
+
} catch (_) {
|
|
103
|
+
return 'denied';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
detach() {
|
|
108
|
+
this._chart.removeEventListener('wick:alert', this._onAlert);
|
|
109
|
+
this.sync(); // flush whatever the session changed
|
|
110
|
+
this._chart = null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* ---------------- internals ---------------- */
|
|
114
|
+
|
|
115
|
+
_read() {
|
|
116
|
+
if (!this._storage) return [];
|
|
117
|
+
try {
|
|
118
|
+
const raw = this._storage.getItem(this._key);
|
|
119
|
+
const list = raw ? JSON.parse(raw) : [];
|
|
120
|
+
return Array.isArray(list) ? list.filter((a) => a && a.id != null).slice(0, MAX_PERSISTED) : [];
|
|
121
|
+
} catch (_) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_save(list) {
|
|
127
|
+
if (!this._storage) return;
|
|
128
|
+
try {
|
|
129
|
+
this._storage.setItem(this._key, JSON.stringify(list.slice(0, MAX_PERSISTED)));
|
|
130
|
+
} catch (_) {
|
|
131
|
+
/* private mode / quota — persistence is best-effort */
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Re-arm every persisted alert on the chart (invalid entries dropped). */
|
|
136
|
+
_restore() {
|
|
137
|
+
for (const a of this._read()) {
|
|
138
|
+
this._chart.addAlert(a);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* A chart alert fired: persist (once-fired alerts drop out via the chart),
|
|
144
|
+
* and — while the tab is hidden — raise a desktop notification and a beep.
|
|
145
|
+
* The webhook fires regardless of visibility.
|
|
146
|
+
*/
|
|
147
|
+
_fire(detail) {
|
|
148
|
+
if (!this._chart) return;
|
|
149
|
+
const hidden = typeof document !== 'undefined' && document.hidden === true;
|
|
150
|
+
if (hidden && this._notify) this._notifyUser(detail);
|
|
151
|
+
if (hidden && this._sound) this._beep();
|
|
152
|
+
if (this._webhook) this._post(detail);
|
|
153
|
+
// the core removes once-fired alerts right after dispatching this event;
|
|
154
|
+
// save on a microtask so storage reflects the post-fire state
|
|
155
|
+
queueMicrotask(() => {
|
|
156
|
+
if (this._chart) this.sync();
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_notifyUser(detail) {
|
|
161
|
+
try {
|
|
162
|
+
if (typeof Notification === 'undefined' || Notification.permission !== 'granted') return;
|
|
163
|
+
const label =
|
|
164
|
+
detail && detail.when != null
|
|
165
|
+
? detail.when
|
|
166
|
+
: `price ${detail && isNum(detail.price) ? detail.price : '?'}`;
|
|
167
|
+
new Notification('WickChart alert', { body: `Alert fired: ${label}`, tag: detail && detail.id });
|
|
168
|
+
} catch (_) {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Short two-tone beep via WebAudio (created lazily; never throws). */
|
|
172
|
+
_beep() {
|
|
173
|
+
try {
|
|
174
|
+
const AC = globalThis.AudioContext || globalThis.webkitAudioContext;
|
|
175
|
+
if (!AC) return;
|
|
176
|
+
if (!this._audio) this._audio = new AC();
|
|
177
|
+
const t = this._audio.currentTime;
|
|
178
|
+
const gain = this._audio.createGain();
|
|
179
|
+
gain.gain.setValueAtTime(0.001, t);
|
|
180
|
+
gain.gain.exponentialRampToValueAtTime(0.2, t + 0.02);
|
|
181
|
+
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
|
|
182
|
+
gain.connect(this._audio.destination);
|
|
183
|
+
for (const [f, dt] of [[880, 0], [660, 0.12]]) {
|
|
184
|
+
const osc = this._audio.createOscillator();
|
|
185
|
+
osc.type = 'sine';
|
|
186
|
+
osc.frequency.setValueAtTime(f, t + dt);
|
|
187
|
+
osc.connect(gain);
|
|
188
|
+
osc.start(t + dt);
|
|
189
|
+
osc.stop(t + dt + 0.12);
|
|
190
|
+
}
|
|
191
|
+
} catch (_) {}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Fire-and-forget webhook POST; rejection is swallowed. */
|
|
195
|
+
_post(detail) {
|
|
196
|
+
const doFetch = this._fetch || globalThis.fetch;
|
|
197
|
+
if (typeof doFetch !== 'function') return;
|
|
198
|
+
try {
|
|
199
|
+
doFetch(this._webhook, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { 'content-type': 'application/json' },
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
id: detail && detail.id,
|
|
204
|
+
price: detail && detail.price,
|
|
205
|
+
when: detail && detail.when,
|
|
206
|
+
time: detail && detail.bar ? detail.bar.time : null,
|
|
207
|
+
bar: detail && detail.bar,
|
|
208
|
+
key: this._key,
|
|
209
|
+
}),
|
|
210
|
+
}).catch(() => {});
|
|
211
|
+
} catch (_) {}
|
|
212
|
+
}
|
|
213
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wickchart-alerts-plus",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Persistent alerts for wickchart — survives reloads via localStorage, desktop notifications + sound while the tab is hidden, optional webhook. Opt-in plugin, zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "alerts-plus.mjs",
|
|
7
|
+
"module": "alerts-plus.mjs",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./alerts-plus.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"alerts-plus.mjs",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"chart",
|
|
17
|
+
"charting",
|
|
18
|
+
"alerts",
|
|
19
|
+
"notifications",
|
|
20
|
+
"webhook",
|
|
21
|
+
"persistence",
|
|
22
|
+
"trading",
|
|
23
|
+
"web-component",
|
|
24
|
+
"wickchart"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"wickchart": ">=1.4.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependenciesMeta": {
|
|
31
|
+
"wickchart": {
|
|
32
|
+
"optional": true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|