Files
daniel e327437f2c feat: add daily-health-report Cloudflare Worker with Telegram digest
Adds a proactive daily heartbeat, since the existing Telegram flows
(docs-monitor, pulse) only notify on change/error/weekly schedule and never
confirm "everything's fine." Runs at 14:00 UTC (10:00 AM EDT) and reports:

- Dashboard site health (reuses the existing /api/health-check endpoint)
- Unresolved Sentry issues from the last 24h across all 3 Sentry projects
  (aitmpl-workers, aitmpl-dashboard, aitmpl-cli)

Does NOT poll the other 3 workers' /status endpoints directly — Cloudflare
blocks Worker-to-Worker fetches over the public *.workers.dev hostname
within the same account (error 1042, confirmed via wrangler dev --remote).
Their health is covered by their own Sentry Cron Monitor check-ins instead.

Also adds a /status endpoint to aitmpl-crons (cloudflare-workers/crons/index.js)
for consistency with the other workers, even though it isn't polled here.

Verified end-to-end: manual /trigger call returns a clean report and
successfully delivers to Telegram (message_id confirmed). Sentry summary
section requires SENTRY_AUTH_TOKEN + SENTRY_ORG_SLUG secrets (pending —
degrades gracefully to "not configured" until set).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 18:54:27 -04:00

92 lines
2.8 KiB
JavaScript

/**
* aitmpl-crons — Cloudflare Worker
*
* Replaces Vercel cron jobs by calling the dashboard API endpoints
* on a schedule. Cron: every 30 minutes (claude-code-check) and hourly (health-check).
*
* Secrets (wrangler secret put):
* DASHBOARD_URL — e.g. https://www.aitmpl.com
* TRIGGER_SECRET — shared secret sent as Authorization header
* SENTRY_DSN — DSN from the "aitmpl-workers" Sentry project (error tracking)
*/
import { reportError, checkIn } from './sentry.js';
const MONITORS = {
'*/30 * * * *': 'claude-code-check',
'0 * * * *': 'health-check',
};
export default {
async scheduled(event, env, ctx) {
const base = env.DASHBOARD_URL || 'https://www.aitmpl.com';
const headers = {
'Authorization': `Bearer ${env.TRIGGER_SECRET || ''}`,
'User-Agent': 'aitmpl-crons/1.0',
};
const cron = event.cron;
const endpoint = cron === '*/30 * * * *' ? '/api/claude-code-check'
: cron === '0 * * * *' ? '/api/health-check'
: null;
if (!endpoint) {
console.log(`Unknown cron schedule: ${cron}`);
return;
}
const monitorSlug = MONITORS[cron];
const checkInId = await checkIn(env, monitorSlug, 'in_progress');
try {
const res = await fetch(`${base}${endpoint}`, { headers });
console.log(`${endpoint}: ${res.status}`);
if (!res.ok) {
await reportError(env, `${endpoint} returned ${res.status}`, {
worker: 'aitmpl-crons',
cron,
endpoint,
status: res.status,
});
await checkIn(env, monitorSlug, 'error', checkInId);
} else {
await checkIn(env, monitorSlug, 'ok', checkInId);
}
} catch (error) {
console.error(`${endpoint} failed:`, error.message);
await reportError(env, error, { worker: 'aitmpl-crons', cron, endpoint });
await checkIn(env, monitorSlug, 'error', checkInId);
}
},
// Manual trigger for testing: GET /trigger?cron=*/30+*+*+*+*
async fetch(request, env) {
const url = new URL(request.url);
if (url.pathname === '/status') {
return new Response(JSON.stringify({
status: 'running',
worker: 'aitmpl-crons',
schedules: {
'*/30 * * * *': '/api/claude-code-check',
'0 * * * *': '/api/health-check',
},
}), { headers: { 'Content-Type': 'application/json' } });
}
if (url.pathname !== '/trigger') {
return new Response('aitmpl-crons worker', { status: 200 });
}
const auth = request.headers.get('Authorization');
if (!env.TRIGGER_SECRET || auth !== `Bearer ${env.TRIGGER_SECRET}`) {
return new Response('Unauthorized', { status: 401 });
}
const cron = url.searchParams.get('cron') || '*/30 * * * *';
await this.scheduled({ cron }, env, {});
return new Response(`Triggered: ${cron}`, { status: 200 });
},
};