-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathworker.js
More file actions
97 lines (86 loc) · 3.56 KB
/
worker.js
File metadata and controls
97 lines (86 loc) · 3.56 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// CloudScale Uptime Monitor — heartbeat watchdog
// WordPress pushes a POST heartbeat every 3 minutes via WP-Cron.
// If no heartbeat arrives for >8 minutes, the site is treated as down.
//
// Required environment bindings:
// SITE_URL — WordPress site URL (for alert messages)
// PING_TOKEN — shared secret (WordPress uses this to auth heartbeat pushes)
// NTFY_URL — ntfy.sh topic URL for down/recovery alerts
// STATE — KV namespace (stores last heartbeat ts + alert state)
//
// Cron trigger: * * * * * (every minute — checks for stale heartbeat)
// HTTP POST / with Authorization: Bearer <PING_TOKEN>:
// action=csdt_heartbeat — record a heartbeat from WordPress (WP-Cron calls this)
// (no action) — manual test, returns current watchdog state
const STALE_MS = 15 * 60 * 1000; // 15 min without heartbeat = site down (heartbeat every 10 min)
const ALERT_COOL = 30 * 60 * 1000; // cooldown between repeat down-alerts
async function watchdog(env, ctx) {
const now = Date.now();
const [hbStr, dsStr, laStr] = await Promise.all([
env.STATE.get('hb'),
env.STATE.get('ds'),
env.STATE.get('la'),
]);
const lastHb = hbStr ? parseInt(hbStr, 10) : 0;
const downSince = dsStr ? parseInt(dsStr, 10) : 0;
const lastAlert = laStr ? parseInt(laStr, 10) : 0;
const stale = !lastHb || (now - lastHb) > STALE_MS;
if (stale) {
const since = downSince || now;
const ops = [];
if (!downSince) ops.push(env.STATE.put('ds', String(now)));
if (now - lastAlert > ALERT_COOL) {
ops.push(notify(env, false, Math.round((now - since) / 1000)));
ops.push(env.STATE.put('la', String(now)));
}
ctx.waitUntil(Promise.all(ops));
} else if (downSince) {
const downSecs = Math.round((now - downSince) / 1000);
ctx.waitUntil(Promise.all([
notify(env, true, downSecs),
env.STATE.delete('ds'),
env.STATE.put('la', String(now)),
]));
}
return { stale, lastHb, downSince };
}
async function notify(env, recovered, downSecs) {
if (!env.NTFY_URL) return;
const dur = downSecs > 0 ? fmtSecs(downSecs) : null;
return fetch(env.NTFY_URL, {
method: 'POST',
headers: {
Title: (recovered ? 'Site Recovered: ' : 'Site Down: ') + env.SITE_URL,
Priority: recovered ? 'default' : 'urgent',
Tags: recovered ? 'white_check_mark' : 'rotating_light',
},
body: recovered
? 'Back online' + (dur ? ' — was down ' + dur : '')
: 'No heartbeat received for ' + (dur || '8m+'),
}).catch(() => {});
}
function fmtSecs(s) {
const m = Math.floor(s / 60);
return m > 0 ? m + 'm ' + (s % 60) + 's' : s + 's';
}
export default {
async scheduled(event, env, ctx) {
ctx.waitUntil(watchdog(env, ctx));
},
async fetch(request, env, ctx) {
if (request.method !== 'POST') return new Response('Method Not Allowed', { status: 405 });
const auth = request.headers.get('Authorization') || '';
if (auth !== 'Bearer ' + env.PING_TOKEN) return new Response('Unauthorized', { status: 401 });
const text = await request.text();
const params = new URLSearchParams(text);
if (params.get('action') === 'csdt_heartbeat') {
await env.STATE.put('hb', String(Date.now()));
return new Response(JSON.stringify({ ok: true }), { headers: { 'Content-Type': 'application/json' } });
}
// Manual test — run watchdog and return current state
const state = await watchdog(env, ctx);
return new Response(JSON.stringify({ ok: !state.stale, ...state, triggered: true }), {
headers: { 'Content-Type': 'application/json' },
});
},
};