getreadystack commited on
Commit
11fcbc8
·
verified ·
1 Parent(s): d5bb6ee

Upload index.html with huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +452 -18
index.html CHANGED
@@ -1,19 +1,453 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  </html>
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html><html lang="en"><meta charset="utf-8">
2
+ <meta name="viewport" content="width=device-width,initial-scale=1">
3
+ <title>Cron Schedule Lint for crontab, Kubernetes CronJob, GitHub Actions, Spring and EventBridge</title>
4
+ <meta name="description" content="Names every schedule line in the file you have open that fires at the wrong hour, twice, or never at all - and prints the next three times each one really fires, in the zone that actually applies.">
5
+ <script defer data-auto-init src="https://cdn.jsdelivr.net/npm/@polar-sh/checkout@latest/dist/embed.global.js"></script>
6
+ <style>
7
+ :root{color-scheme:light dark}
8
+ body{margin:0;font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;
9
+ background:#fbfbfa;color:#16161d}
10
+ @media(prefers-color-scheme:dark){body{background:#14141a;color:#e8e8ea}}
11
+ .wrap{max-width:880px;margin:0 auto;padding:28px 20px 64px}
12
+ h1{font-size:1.7rem;line-height:1.25;margin:.2em 0 .3em}
13
+ p.sub{margin:0 0 1.4em;opacity:.8}
14
+ textarea{width:100%;min-height:230px;font:13px/1.5 ui-monospace,Menlo,Consolas,monospace;
15
+ padding:12px;border:1px solid #c9c9d1;border-radius:10px;background:transparent;color:inherit;
16
+ box-sizing:border-box}
17
+ button{font:600 15px/1 inherit;padding:12px 22px;border:0;border-radius:10px;
18
+ background:#1d4ed8;color:#fff;cursor:pointer;margin:12px 0}
19
+ button:hover{background:#1a43bd}
20
+ table{border-collapse:collapse;width:100%;margin-top:14px;font-size:14px}
21
+ th,td{border-bottom:1px solid #d8d8e0;padding:8px 6px;text-align:left;vertical-align:top}
22
+ th{font-weight:600;opacity:.75}
23
+ td.n{width:64px;font:13px ui-monospace,monospace;opacity:.7}
24
+ .ok{padding:14px;border-radius:10px;background:#e8f5ec;color:#14532d;margin-top:14px}
25
+ @media(prefers-color-scheme:dark){.ok{background:#15321f;color:#bbf7d0}}
26
+ .next{margin-top:30px;padding:18px;border:1px solid #d8d8e0;border-radius:12px}
27
+ .cta{margin:4px 0 18px;padding:12px 14px;border-left:3px solid #1d4ed8;font-size:14px;line-height:1.55}
28
+ .cta a{font-weight:600}
29
+ .zone{margin:10px 0 0;font-size:14px;opacity:.85}
30
+ .zone select{font:inherit;padding:5px 8px;border-radius:8px;border:1px solid #c9c9d1;background:transparent;color:inherit}
31
+ .next h2{font-size:1.05rem;margin:0 0 .5em}
32
+ .next ul{margin:.3em 0 1em 1.1em;padding:0}
33
+ </style>
34
+ <div class="wrap">
35
+ <h1>Cron Schedule Lint for crontab, Kubernetes CronJob, GitHub Actions, Spring and EventBridge</h1>
36
+ <p class="sub">Names every schedule line in the file you have open that fires at the wrong hour, twice, or never at all - and prints the next three times each one really fires, in the zone that actually applies.</p>
37
+ <textarea id="src" placeholder="Paste your crontab, workflow, CronJob, wrangler.toml or beat file here"># crontab - the schedule an assistant wrote for our billing box
38
+ CRON_TZ=America/New_York
39
+ 0 0 31 * * /opt/billing/monthly_invoice.sh
40
+ 30 1 * * * /opt/etl/nightly_rollup.sh
41
+ 0 0 1 * 1 /opt/reports/board_pack.sh
42
+ */7 * * * * /usr/local/bin/queue_probe
43
+ 0 2 * * * python /opt/jobs/reconcile.py
44
+ 15 4 * * * /opt/backup/dump.sh &gt; /backup/db-$(date +%Y-%m-%d).sql
45
+ @reboot /opt/agent/start.sh
46
+ 0 0 30 2 * /opt/audit/leap_day_check.sh
47
+ </textarea>
48
+ <p class="zone"><label for="zone">Time zone used where the file does not name one (spec.timeZone, CRON_TZ and UTC-only platforms always win over this): </label><select id="zone"><option value="UTC">UTC</option><option value="America/New_York" selected>America/New_York</option><option value="America/Chicago">America/Chicago</option><option value="America/Denver">America/Denver</option><option value="America/Los_Angeles">America/Los_Angeles</option><option value="America/Sao_Paulo">America/Sao_Paulo</option><option value="Europe/London">Europe/London</option><option value="Europe/Berlin">Europe/Berlin</option><option value="Europe/Madrid">Europe/Madrid</option><option value="Europe/Warsaw">Europe/Warsaw</option><option value="Africa/Lagos">Africa/Lagos</option><option value="Asia/Kolkata">Asia/Kolkata</option><option value="Asia/Singapore">Asia/Singapore</option><option value="Asia/Tokyo">Asia/Tokyo</option><option value="Asia/Seoul">Asia/Seoul</option><option value="Australia/Sydney">Australia/Sydney</option><option value="Pacific/Auckland">Pacific/Auckland</option></select></p>
49
+ <button id="go">Find the schedules that misfire</button>
50
+ <p id="cta-top" class="cta">Free, with no key, for the file above: every rule and every run time. <a href="https://getreadystack.com" data-polar-checkout data-polar-checkout-theme="light">Get the full version - $29</a> for the whole repository as one calendar, the CSV/JSON/HTML export and the CI gate. $29 once - one licence key per person or team seat - 7-day full refund. Healthchecks.io Business, the ordinary cron monitor, is $20 every month and only tells you after a run was already missed.</p>
51
+ <div id="out"></div>
52
+ <div class="next"><h2>In the full version</h2><ul><li>Every schedule in the repository merged into one calendar instead of the single file you have open</li><li>The whole calendar as CSV, JSON or HTML for a change review</li><li>Machine output that exits non-zero so a pipeline can stop a merge that adds a schedule which never fires</li><li><a href="https://getreadystack.com" data-polar-checkout data-polar-checkout-theme="light">Get the full version - $29</a></li></ul></div>
53
+ </div>
54
+ <script>
55
+ /* ── CRONX ──────────────────────────────────────────────────────────────────
56
+ One brain. This exact block is injected, byte for byte, into ext/extension.js
57
+ and into index.html. It does the calendar arithmetic the regex rules cannot:
58
+ which real instants a schedule fires at, in which zone, and on which dates
59
+ that wall-clock time does not exist or happens twice.
60
+ No dependencies. Uses Intl time-zone data, which ships with Node and browsers.
61
+ ------------------------------------------------------------------------- */
62
+ /* ── CRONX ──────────────────────────────────────────────────────────────────
63
+ One brain. This exact block is injected, byte for byte, into ext/extension.js
64
+ and into index.html. It does the calendar arithmetic the regex rules cannot:
65
+ which real instants a schedule fires at, in which zone, and on which dates
66
+ that wall-clock time does not exist or happens twice.
67
+ No dependencies. Uses Intl time-zone data, which ships with Node and browsers.
68
+ ------------------------------------------------------------------------- */
69
+ /* ── CRONX ──────────────────────────────────────────────────────────────────
70
+ One brain. This exact block is injected, byte for byte, into ext/extension.js
71
+ and into index.html. It does the calendar arithmetic the regex rules cannot:
72
+ which real instants a schedule fires at, in which zone, and on which dates
73
+ that wall-clock time does not exist or happens twice.
74
+ No dependencies. Uses Intl time-zone data, which ships with Node and browsers.
75
+ ------------------------------------------------------------------------- */
76
+ var CRONX = (function () {
77
+ 'use strict';
78
+ var DAY = 86400000;
79
+ var DOW_NAMES = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6 };
80
+ var MON_NAMES = { JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6,
81
+ JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12 };
82
+ // dialect -> how many fields, where the 5 calendar fields sit, how Sunday is numbered
83
+ var DIALECT = {
84
+ crontab: { n: [5], off: 0, sunday: 0, label: 'crontab / CronJob / Actions' },
85
+ spring: { n: [6], off: 1, sunday: 0, label: 'Spring @Scheduled' },
86
+ quartz: { n: [6, 7], off: 1, sunday: 1, label: 'Quartz' },
87
+ eventbridge: { n: [6], off: 0, sunday: 1, label: 'EventBridge cron()' },
88
+ nodecron: { n: [5, 6], off: -1, sunday: 0, label: 'node-cron' }
89
+ };
90
+ var SHORTHAND = {
91
+ '@yearly': '0 0 1 1 *', '@annually': '0 0 1 1 *', '@monthly': '0 0 1 * *',
92
+ '@weekly': '0 0 * * 0', '@daily': '0 0 * * *', '@midnight': '0 0 * * *',
93
+ '@hourly': '0 * * * *'
94
+ };
95
+
96
+ /* ---- time zone maths -------------------------------------------------- */
97
+ var _fmt = {};
98
+ function validZone(z) {
99
+ if (!z || typeof z !== 'string') return false;
100
+ try { new Intl.DateTimeFormat('en-US', { timeZone: z }); return true; } catch (e) { return false; }
101
+ }
102
+ function fmt(zone) {
103
+ if (!_fmt[zone]) {
104
+ _fmt[zone] = new Intl.DateTimeFormat('en-US', {
105
+ timeZone: zone, hourCycle: 'h23', year: 'numeric', month: '2-digit',
106
+ day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'
107
+ });
108
+ }
109
+ return _fmt[zone];
110
+ }
111
+ function parts(zone, ms) {
112
+ var p = {}, a = fmt(zone).formatToParts(new Date(ms)), i;
113
+ for (i = 0; i < a.length; i++) {
114
+ if (a[i].type !== 'literal') p[a[i].type] = parseInt(a[i].value, 10);
115
+ }
116
+ if (p.hour === 24) p.hour = 0;
117
+ return p;
118
+ }
119
+ function offsetMin(zone, ms) {
120
+ var p = parts(zone, ms);
121
+ return (Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second)
122
+ - Math.floor(ms / 1000) * 1000) / 60000;
123
+ }
124
+ function localToUtc(zone, y, mo, d, h, mi) {
125
+ var naive = Date.UTC(y, mo - 1, d, h, mi, 0);
126
+ var o1 = offsetMin(zone, naive), ms = naive - o1 * 60000;
127
+ var o2 = offsetMin(zone, ms);
128
+ if (o2 !== o1) ms = naive - o2 * 60000;
129
+ return ms;
130
+ }
131
+ // returns the instant, or null when that wall-clock minute does not exist in that zone
132
+ function existsLocal(zone, y, mo, d, h, mi) {
133
+ var ms = localToUtc(zone, y, mo, d, h, mi), p = parts(zone, ms);
134
+ return (p.year === y && p.month === mo && p.day === d
135
+ && p.hour === h && p.minute === mi) ? ms : null;
136
+ }
137
+ var _tr = {};
138
+ // every offset change in the zone over the window, narrowed to the minute
139
+ function transitions(zone, fromMs, days) {
140
+ var key = zone + '|' + Math.floor(fromMs / DAY) + '|' + days;
141
+ if (_tr[key]) return _tr[key];
142
+ var out = [], step = 3600000, prev = offsetMin(zone, fromMs), end = fromMs + days * DAY, t, o;
143
+ for (t = fromMs + step; t <= end; t += step) {
144
+ o = offsetMin(zone, t);
145
+ if (o !== prev) {
146
+ var lo = t - step, hi = t, mid;
147
+ while (hi - lo > 60000) {
148
+ mid = lo + Math.floor((hi - lo) / 120000) * 60000;
149
+ if (mid <= lo) mid = lo + 60000;
150
+ if (offsetMin(zone, mid) === prev) lo = mid; else hi = mid;
151
+ }
152
+ out.push({ ms: hi, from: prev, to: o });
153
+ prev = o;
154
+ }
155
+ }
156
+ _tr[key] = out;
157
+ return out;
158
+ }
159
+
160
+ /* ---- field expansion --------------------------------------------------- */
161
+ function num(s, names) {
162
+ s = String(s).trim().toUpperCase();
163
+ if (names && Object.prototype.hasOwnProperty.call(names, s)) return names[s];
164
+ if (!/^\d+$/.test(s)) return null;
165
+ return parseInt(s, 10);
166
+ }
167
+ function expand(tok, lo, hi, names) {
168
+ tok = String(tok).trim();
169
+ var raw = tok, special = /[LW#]/i.test(tok);
170
+ if (special) return null;
171
+ if (tok === '?') tok = '*';
172
+ var chunks = tok.split(','), seen = {}, i, j, stepUsed = 0, wildcard = (tok === '*');
173
+ for (i = 0; i < chunks.length; i++) {
174
+ var p = chunks[i], step = 1, m = p.match(/^(.*)\/(\d+)$/);
175
+ if (m) { p = m[1]; step = parseInt(m[2], 10); if (!step) return null; stepUsed = step; }
176
+ var a, b;
177
+ if (p === '*' || p === '') { a = lo; b = hi; }
178
+ else {
179
+ var r = p.split('-');
180
+ a = num(r[0], names);
181
+ b = (r.length > 1) ? num(r[1], names) : (m ? hi : a);
182
+ if (a === null || b === null) return null;
183
+ }
184
+ if (a < lo || b > hi) return null;
185
+ if (a > b) {
186
+ for (j = a; j <= hi; j += step) seen[j] = 1;
187
+ for (j = lo; j <= b; j += step) seen[j] = 1;
188
+ } else {
189
+ for (j = a; j <= b; j += step) seen[j] = 1;
190
+ }
191
+ }
192
+ var vals = Object.keys(seen).map(Number).sort(function (x, y) { return x - y; });
193
+ if (!vals.length) return null;
194
+ return { vals: vals, raw: raw, restricted: !(raw === '*' || raw === '?'),
195
+ step: stepUsed, wildcard: wildcard, span: hi - lo + 1 };
196
+ }
197
+
198
+ /* ---- parse ------------------------------------------------------------- */
199
+ function parse(expr, dialect) {
200
+ var d = DIALECT[dialect] || DIALECT.crontab;
201
+ var e = String(expr).trim();
202
+ if (SHORTHAND[e.toLowerCase()]) { e = SHORTHAND[e.toLowerCase()]; d = DIALECT.crontab; }
203
+ var f = e.split(/\s+/);
204
+ if (d.n.indexOf(f.length) < 0) return { ok: false, why: 'fields', got: f.length, want: d.n };
205
+ var off = d.off;
206
+ if (off === -1) off = (f.length === 6) ? 1 : 0; // node-cron: seconds are optional
207
+ var mi = expand(f[off], 0, 59, null);
208
+ var hr = expand(f[off + 1], 0, 23, null);
209
+ var dom = expand(f[off + 2], 1, 31, null);
210
+ var mon = expand(f[off + 3], 1, 12, MON_NAMES);
211
+ var dowLo = d.sunday === 1 ? 1 : 0, dowHi = d.sunday === 1 ? 7 : 7;
212
+ var dowNames = {}, k;
213
+ for (k in DOW_NAMES) if (Object.prototype.hasOwnProperty.call(DOW_NAMES, k)) {
214
+ dowNames[k] = DOW_NAMES[k] + (d.sunday === 1 ? 1 : 0);
215
+ }
216
+ var dow = expand(f[off + 4], dowLo, dowHi, dowNames);
217
+ if (!mi || !hr || !dom || !mon || !dow) return { ok: false, why: 'field-value' };
218
+ // normalise day-of-week to 0=Sunday .. 6=Saturday
219
+ var norm = {}, i;
220
+ for (i = 0; i < dow.vals.length; i++) {
221
+ var v = dow.vals[i];
222
+ norm[d.sunday === 1 ? (v - 1) : (v === 7 ? 0 : v)] = 1;
223
+ }
224
+ dow.vals = Object.keys(norm).map(Number).sort(function (x, y) { return x - y; });
225
+ return { ok: true, mi: mi, hr: hr, dom: dom, mon: mon, dow: dow,
226
+ dialect: dialect || 'crontab', label: d.label, expr: e };
227
+ }
228
+
229
+ function dayMatches(s, y, mo, d) {
230
+ if (s.mon.vals.indexOf(mo) < 0) return false;
231
+ var w = new Date(Date.UTC(y, mo - 1, d)).getUTCDay();
232
+ var dOK = s.dom.vals.indexOf(d) >= 0, wOK = s.dow.vals.indexOf(w) >= 0;
233
+ if (s.dom.restricted && s.dow.restricted) return dOK || wOK; // POSIX: union, not intersection
234
+ if (s.dom.restricted) return dOK;
235
+ if (s.dow.restricted) return wOK;
236
+ return true;
237
+ }
238
+ function pad(n) { return (n < 10 ? '0' : '') + n; }
239
+ function stamp(y, mo, d, h, mi) {
240
+ return y + '-' + pad(mo) + '-' + pad(d) + ' ' + pad(h) + ':' + pad(mi);
241
+ }
242
+
243
+ function nextRuns(s, zone, fromMs, count) {
244
+ var p = parts(zone, fromMs), cur = Date.UTC(p.year, p.month - 1, p.day), out = [], k;
245
+ for (k = 0; k < 1500 && out.length < count; k++) {
246
+ var dt = new Date(cur + k * DAY);
247
+ var yy = dt.getUTCFullYear(), mm = dt.getUTCMonth() + 1, dd = dt.getUTCDate();
248
+ if (!dayMatches(s, yy, mm, dd)) continue;
249
+ for (var a = 0; a < s.hr.vals.length && out.length < count; a++) {
250
+ for (var b = 0; b < s.mi.vals.length && out.length < count; b++) {
251
+ var H = s.hr.vals[a], M = s.mi.vals[b];
252
+ var ms = existsLocal(zone, yy, mm, dd, H, M);
253
+ if (ms === null || ms <= fromMs) continue;
254
+ out.push({ ms: ms, txt: stamp(yy, mm, dd, H, M) });
255
+ }
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ function daysMatching(s, fromMs, days) {
261
+ var d0 = new Date(fromMs), cur = Date.UTC(d0.getUTCFullYear(), d0.getUTCMonth(), d0.getUTCDate());
262
+ var n = 0, k;
263
+ for (k = 0; k < days; k++) {
264
+ var dt = new Date(cur + k * DAY);
265
+ if (dayMatches(s, dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate())) n++;
266
+ }
267
+ return n;
268
+ }
269
+ function monthsWithoutADay(s) {
270
+ var LEN = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
271
+ var NM = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
272
+ 'August', 'September', 'October', 'November', 'December'];
273
+ if (!s.dom.restricted || s.dow.restricted) return [];
274
+ var out = [], m, i, hit;
275
+ for (m = 0; m < 12; m++) {
276
+ if (s.mon.vals.indexOf(m + 1) < 0) continue;
277
+ hit = false;
278
+ for (i = 0; i < s.dom.vals.length; i++) if (s.dom.vals[i] <= LEN[m]) { hit = true; break; }
279
+ if (!hit) out.push(NM[m]);
280
+ }
281
+ return out;
282
+ }
283
+ // wall-clock minutes that do not exist, or happen twice, in the window
284
+ function dstHits(s, zone, fromMs, days) {
285
+ var tr = transitions(zone, fromMs, days), res = [], i, q;
286
+ for (i = 0; i < tr.length; i++) {
287
+ var t = tr[i], delta = t.to - t.from, n = Math.abs(delta);
288
+ if (!n) continue;
289
+ var base = (delta > 0) ? (t.ms + t.from * 60000) : (t.ms + t.to * 60000);
290
+ for (q = 0; q < n; q++) {
291
+ var nz = new Date(base + q * 60000);
292
+ var yy = nz.getUTCFullYear(), mm = nz.getUTCMonth() + 1, dd = nz.getUTCDate();
293
+ var H = nz.getUTCHours(), M = nz.getUTCMinutes();
294
+ if (s.mi.vals.indexOf(M) >= 0 && s.hr.vals.indexOf(H) >= 0 && dayMatches(s, yy, mm, dd)) {
295
+ res.push({ kind: delta > 0 ? 'gap' : 'dup', at: stamp(yy, mm, dd, H, M) });
296
+ }
297
+ }
298
+ }
299
+ return res;
300
+ }
301
+
302
+ /* ---- pulling schedules out of a file ----------------------------------- */
303
+ var EXPR = "[0-9A-Za-z*?,/\\-]+(?:\\s+[0-9A-Za-z*?,/\\-]+){4,6}";
304
+ function docZone(text, fallback) {
305
+ var m = String(text).match(/^[ \t-]*(?:timeZone|time_zone)\s*:\s*["']?([A-Za-z]+\/[A-Za-z_+\-0-9\/]+)/m);
306
+ if (m && validZone(m[1])) return { zone: m[1], why: 'spec.timeZone in this file' };
307
+ m = String(text).match(/^\s*(?:CRON_TZ|TZ)\s*=\s*["']?([A-Za-z]+\/[A-Za-z_+\-0-9\/]+)/m);
308
+ if (m && validZone(m[1])) return { zone: m[1], why: 'CRON_TZ in this file' };
309
+ return { zone: fallback, why: 'the zone you picked' };
310
+ }
311
+ function findSchedules(text, fallbackZone) {
312
+ var lines = String(text).split(/\r?\n/), out = [], i, m;
313
+ var dz = docZone(text, fallbackZone);
314
+ for (i = 0; i < lines.length; i++) {
315
+ var L = lines[i], n = i + 1, hit = null;
316
+ if (/^\s*#/.test(L) && !/cron/i.test(L)) continue;
317
+ if ((m = L.match(new RegExp("-\\s*cron\\s*:\\s*['\"]\\s*(" + EXPR + ")\\s*['\"]")))) {
318
+ hit = { expr: m[1], dialect: 'crontab', zone: 'UTC', src: 'GitHub Actions', forced: true };
319
+ } else if ((m = L.match(new RegExp("\"schedule\"\\s*:\\s*\"\\s*(" + EXPR + ")\\s*\"")))) {
320
+ hit = { expr: m[1], dialect: 'crontab', zone: 'UTC', src: 'Vercel cron', forced: true };
321
+ } else if ((m = L.match(new RegExp("^\\s*schedule\\s*:\\s*['\"]?\\s*(" + EXPR + ")\\s*['\"]?\\s*$")))) {
322
+ hit = { expr: m[1], dialect: 'crontab', zone: dz.zone, src: 'Kubernetes CronJob', forced: false };
323
+ } else if ((m = L.match(new RegExp("^\\s*crons\\s*=\\s*\\[\\s*\"\\s*(" + EXPR + ")\\s*\"")))) {
324
+ hit = { expr: m[1], dialect: 'crontab', zone: 'UTC', src: 'Cloudflare Workers', forced: true };
325
+ } else if ((m = L.match(new RegExp("cron\\(\\s*(" + EXPR + ")\\s*\\)")))) {
326
+ hit = { expr: m[1], dialect: 'eventbridge', zone: 'UTC', src: 'EventBridge rule', forced: true };
327
+ } else if ((m = L.match(new RegExp("@Scheduled\\s*\\([^)]*cron\\s*=\\s*\"\\s*(" + EXPR + ")\\s*\"")))) {
328
+ hit = { expr: m[1], dialect: 'spring', zone: dz.zone, src: 'Spring @Scheduled', forced: false };
329
+ } else if ((m = L.match(new RegExp("cronSchedule\\s*\\(\\s*\"\\s*(" + EXPR + ")\\s*\"")))) {
330
+ hit = { expr: m[1], dialect: 'quartz', zone: dz.zone, src: 'Quartz', forced: false };
331
+ } else if ((m = L.match(new RegExp("cron\\.schedule\\s*\\(\\s*['\"]\\s*(" + EXPR + ")\\s*['\"]")))) {
332
+ hit = { expr: m[1], dialect: 'nodecron', zone: dz.zone, src: 'node-cron', forced: false };
333
+ } else if ((m = L.match(/^\s*(@(?:yearly|annually|monthly|weekly|daily|midnight|hourly))\s+\S/))) {
334
+ hit = { expr: m[1], dialect: 'crontab', zone: dz.zone, src: 'crontab', forced: false };
335
+ } else if ((m = L.match(new RegExp("^\\s*([0-9*/,\\-]+\\s+[0-9*/,\\-]+\\s+[0-9A-Za-z*/,\\-]+\\s+[0-9A-Za-z*/,\\-]+\\s+[0-9A-Za-z*/,\\-]+)\\s+\\S")))) {
336
+ hit = { expr: m[1], dialect: 'crontab', zone: dz.zone, src: 'crontab', forced: false };
337
+ }
338
+ if (hit) { hit.line = n; hit.zoneWhy = hit.forced ? 'this platform runs schedules in UTC only' : dz.why; out.push(hit); }
339
+ }
340
+ return out;
341
+ }
342
+
343
+ /* ---- the findings the regex table cannot produce ------------------------ */
344
+ function analyzeText(text, fallbackZone, nowMs) {
345
+ var zone0 = validZone(fallbackZone) ? fallbackZone : 'UTC';
346
+ var now = nowMs || Date.now();
347
+ var found = findSchedules(text, zone0), out = [], i;
348
+ for (i = 0; i < found.length; i++) {
349
+ var f = found[i], s = parse(f.expr, f.dialect);
350
+ if (!s.ok) {
351
+ // a wrong FIELD COUNT is already named by the rule table; a wrong VALUE is not
352
+ if (s.why === 'field-value') {
353
+ out.push({ line: f.line, sev: 'error', engine: true,
354
+ msg: f.src + ' \u00b7 ' + f.expr + ' \u2014 this cannot be read as '
355
+ + (DIALECT[f.dialect] || DIALECT.crontab).label + '. A field is out of range '
356
+ + '(minute 0-59, hour 0-23, day 1-31, month 1-12), or it uses L, W or # '
357
+ + 'which only Quartz understands. Nothing runs.' });
358
+ }
359
+ continue;
360
+ }
361
+ var runs = nextRuns(s, f.zone, now, 3);
362
+ var perYear = daysMatching(s, now, 365) * s.hr.vals.length * s.mi.vals.length;
363
+ if (!runs.length) {
364
+ out.push({ line: f.line, sev: 'error', engine: true,
365
+ msg: f.src + ' \u00b7 ' + f.expr + ' \u2014 this schedule never fires. '
366
+ + 'No date in the next four years satisfies it.' });
367
+ continue;
368
+ }
369
+ // ⛔runs may hold fewer than three: a leap-day schedule has one hit in four years.
370
+ var when = [];
371
+ for (var w = 0; w < runs.length; w++) when.push(runs[w].txt);
372
+ out.push({ line: f.line, sev: 'info', engine: true,
373
+ msg: f.src + ' \u00b7 ' + f.expr + ' \u2014 fires next at ' + when.join(', ')
374
+ + (runs.length < 3 ? ' (and not again inside the next four years)' : '')
375
+ + ' ' + f.zone + ' (' + f.zoneWhy + ') \u00b7 ' + perYear
376
+ + (perYear === 1 ? ' run' : ' runs') + ' in the next 365 days.' });
377
+ var skipped = monthsWithoutADay(s);
378
+ if (skipped.length) {
379
+ out.push({ line: f.line, sev: 'error', engine: true,
380
+ msg: 'Day-of-month ' + s.dom.raw + ' never occurs in ' + skipped.join(', ')
381
+ + '. This schedule is silently absent in ' + skipped.length
382
+ + ' month' + (skipped.length > 1 ? 's' : '') + ' of every year.' });
383
+ }
384
+ if (s.dom.restricted && s.dow.restricted) {
385
+ out.push({ line: f.line, sev: 'error', engine: true,
386
+ msg: 'Day-of-month and day-of-week are both restricted, so POSIX cron takes the UNION: '
387
+ + 'this fires on day ' + s.dom.raw + ' of the month OR on weekday ' + s.dow.raw
388
+ + ', which is ' + perYear + ' runs a year, not the one you meant. '
389
+ + 'Leave one of the two fields as *.' });
390
+ }
391
+ var fields = [['minute', s.mi], ['hour', s.hr], ['day-of-month', s.dom], ['month', s.mon]];
392
+ for (var q = 0; q < fields.length; q++) {
393
+ var fld = fields[q][1];
394
+ if (fld.step > 1 && fld.raw.indexOf('*/') === 0 && (fld.span % fld.step) !== 0) {
395
+ var last = fld.vals[fld.vals.length - 1];
396
+ out.push({ line: f.line, sev: 'warn', engine: true,
397
+ msg: 'Step ' + fld.raw + ' does not divide the ' + fields[q][0] + ' field. '
398
+ + 'It restarts at the top of every cycle, so the gap between the last run ('
399
+ + last + ') and the next is ' + (fld.span - last + fld.vals[0]) + ', not ' + fld.step + '.' });
400
+ }
401
+ }
402
+ var hits = dstHits(s, f.zone, now, 366);
403
+ for (var h = 0; h < hits.length && h < 2; h++) {
404
+ out.push({ line: f.line, sev: 'error', engine: true,
405
+ msg: hits[h].kind === 'gap'
406
+ ? ('On ' + hits[h].at + ' the clock in ' + f.zone + ' jumps forward and this wall-clock '
407
+ + 'time does not exist at all. Move the job outside the transition hour.')
408
+ : ('On ' + hits[h].at + ' the clock in ' + f.zone + ' goes back and this wall-clock '
409
+ + 'time happens twice. Whether your scheduler fires once or twice here is '
410
+ + 'implementation-defined - move the job outside the transition hour.') });
411
+ }
412
+ }
413
+ return out;
414
+ }
415
+
416
+ return { validZone: validZone, parse: parse, expand: expand, nextRuns: nextRuns, dstHits: dstHits,
417
+ findSchedules: findSchedules, analyzeText: analyzeText, transitions: transitions,
418
+ existsLocal: existsLocal, offsetMin: offsetMin, daysMatching: daysMatching,
419
+ monthsWithoutADay: monthsWithoutADay, dayMatches: dayMatches };
420
+ })();
421
+ var RULES=[{"pattern": "-\\s*cron:\\s*['\\\"]\\s*(?:\\S+\\s+){5}\\S+\\s*['\\\"]", "flags": "", "sev": "error", "message": "GitHub Actions takes a five-field POSIX cron. Six fields is not a schedule it can read, the workflow simply never runs on time, and the Actions tab shows nothing at all. Drop the leading seconds field.", "fix": "- cron: '0 3 * * *'"}, {"pattern": "@Scheduled\\s*\\([^)]*cron\\s*=\\s*\\\"(?:[^\\s\\\"]+\\s+){4}[^\\s\\\"]+\\\"", "flags": "", "sev": "error", "message": "Spring's @Scheduled cron takes six fields, seconds first. A five-field Unix expression is rejected at startup with 'Cron expression must consist of 6 fields'. Prepend a 0 for the seconds field.", "fix": "@Scheduled(cron = \"0 0 3 * * *\")"}, {"pattern": "cron\\(\\s*(?:[^\\s)]+\\s+){4}[^\\s)]+\\s*\\)", "flags": "", "sev": "error", "message": "Amazon EventBridge cron() takes six fields and the sixth is the YEAR, not seconds: cron(minutes hours day-of-month month day-of-week year). A five-field Unix expression is rejected when the rule is created.", "fix": "cron(0 3 * * ? *)"}, {"pattern": "cron\\(\\s*[^\\s)]+\\s+[^\\s)]+\\s+(?!\\?)[^\\s)]+\\s+[^\\s)]+\\s+(?!\\?)[^\\s)]+\\s+[^\\s)]+\\s*\\)", "flags": "", "sev": "error", "message": "EventBridge will not accept a value or a * in both day-of-month and day-of-week; one of the two must be a ?. Note also that EventBridge numbers the week 1-7 with 1 = Sunday, so a Unix 1 (Monday) becomes Sunday here.", "fix": "cron(0 3 * * ? *)"}, {"pattern": "CronScheduleBuilder\\.cronSchedule\\s*\\(|org\\.quartz\\.CronExpression|new\\s+CronTrigger\\b", "flags": "", "sev": "warn", "message": "Quartz numbers the week 1-7 with 1 = Sunday, one place off from Unix cron, and it requires a ? in either day-of-month or day-of-week. A Unix expression pasted here runs on the wrong weekday rather than failing.", "fix": ""}, {"pattern": "^\\s*schedule:\\s*['\\\"]?(?:[^\\s'\\\"]+\\s+){5}[^\\s'\\\"]+", "flags": "", "sev": "error", "message": "A Kubernetes CronJob schedule is five fields. The API server rejects six, so kubectl apply fails - or, in a Helm template rendered at deploy time, the release fails and the old CronJob keeps running the old schedule.", "fix": "schedule: \"0 3 * * *\""}, {"pattern": "^\\s*concurrencyPolicy:\\s*[\\\"']?Allow", "flags": "", "sev": "warn", "message": "Allow is also what you get when the field is absent. The moment one run takes longer than the interval, the next run starts anyway and the two overlap - two exports, two invoices, two emails to the same customer. Forbid or Replace is usually what was meant.", "fix": "concurrencyPolicy: Forbid"}, {"pattern": "^\\s*startingDeadlineSeconds:\\s*(?:[1-9]\\d{5,})\\b", "flags": "", "sev": "error", "message": "A startingDeadlineSeconds window this long lets the CronJob controller count more than 100 missed starts. At 100 it stops scheduling this CronJob permanently and logs one line - 'Cannot determine if job needs to be started: too many missed start time (> 100)' - with no event, no alert and no restart.", "fix": "startingDeadlineSeconds: 200"}, {"pattern": "^\\s*suspend:\\s*true", "flags": "", "sev": "warn", "message": "A suspended CronJob keeps accumulating missed start times. Resume it after more than 100 missed intervals and the controller refuses to schedule it again at all, so a job paused over a long freeze never comes back on its own.", "fix": ""}, {"pattern": "^\\s*restartPolicy:\\s*[\\\"']?Always", "flags": "", "sev": "error", "message": "A Job pod template may only use Never or OnFailure. Always is rejected by the API server, so this CronJob is never created and nothing anywhere reports a missing schedule.", "fix": "restartPolicy: OnFailure"}, {"pattern": "^\\s*(?:CRON_TZ|TZ)\\s*=\\s*[A-Za-z]", "flags": "", "sev": "warn", "message": "CRON_TZ is a Vixie/cronie extension. busybox crond, which is the cron in every Alpine image, ignores it and runs in the container's own zone. Set the zone inside the job's command instead if the image is not running cronie.", "fix": ""}, {"pattern": "^\\s*ENV\\s+TZ\\s*[=\\s]", "flags": "", "sev": "warn", "message": "Setting TZ in an image that has no tzdata package changes nothing: both glibc and musl fall back to UTC when /usr/share/zoneinfo is missing, so every schedule in the container silently runs on UTC. Add tzdata (apk add --no-cache tzdata, or apt-get install -y tzdata).", "fix": "RUN apk add --no-cache tzdata"}, {"pattern": "cron:.*#.*\\b(?:EST|EDT|PST|PDT|CST|CDT|CET|CEST|BST|IST|KST|JST|AEST|local time)\\b", "flags": "i", "sev": "error", "message": "GitHub Actions runs every scheduled workflow on UTC and offers no per-schedule zone. A comment naming a local zone is right for half the year: on 2026-10-25 in Europe and 2026-11-01 in the United States the local hour this really fires at moves by one, and nothing in the file changes.", "fix": ""}, {"pattern": "^\\s*crons\\s*=\\s*\\[", "flags": "", "sev": "warn", "message": "Cloudflare Workers cron triggers run on UTC only; there is no per-trigger time zone. A 09:00 local job needs its expression changed by hand on every DST date, or it drifts an hour twice a year.", "fix": ""}, {"pattern": "\\\"schedule\\\"\\s*:\\s*\\\"[^\\\"]*\\*/", "flags": "", "sev": "error", "message": "Vercel runs cron jobs on UTC only, and on the Hobby plan any expression that resolves to more often than once a day fails at deploy time rather than at runtime, so the deployment itself is rejected.", "fix": ""}, {"pattern": "crontab\\s*\\(", "flags": "", "sev": "info", "message": "Celery beat runs its schedules on UTC unless the timezone setting is set. Once it is set, the beat schedule moves with DST, so a job written for 02:30 local silently does not run, or runs twice, on the transition day.", "fix": ""}, {"pattern": "^\\s*schedule:\\s*$", "flags": "", "sev": "warn", "message": "A scheduled workflow in a public repository is disabled automatically after 60 days with no repository activity. GitHub sends one email to whoever last enabled it and shows nothing in the Actions tab; the workflow just stops.", "fix": ""}, {"pattern": "-\\s*cron:\\s*['\\\"]\\s*\\*/[1-4]\\s", "flags": "", "sev": "error", "message": "GitHub Actions will not run a schedule more often than every 5 minutes. A shorter interval does not error - it is simply not honoured.", "fix": "- cron: '*/5 * * * *'"}, {"pattern": "-\\s*cron:\\s*['\\\"]\\s*0\\s+", "flags": "", "sev": "info", "message": "Every scheduled workflow on GitHub that fires at minute 0 lands in the same queue. GitHub's own documentation warns that runs during periods of high load may be delayed, and delayed runs are dropped rather than queued. Move the minute off 0.", "fix": "- cron: '7 3 * * *'"}, {"pattern": "catchup\\s*=\\s*True", "flags": "", "sev": "error", "message": "With a start_date in the past this queues one DAG run for every interval between that date and now, the moment the DAG is unpaused - hundreds of runs at once, every one of them consuming worker time and downstream API quota.", "fix": "catchup=False"}, {"pattern": "start_date\\s*=\\s*(?:[A-Za-z_.]*\\.)?now\\s*\\(|days_ago\\s*\\(", "flags": "", "sev": "error", "message": "A start_date evaluated when the file is parsed moves every time the scheduler re-reads it, so the first interval never closes and the DAG never runs. Pin a fixed datetime instead.", "fix": "start_date=datetime(2026, 1, 1)"}, {"pattern": "^\\s*OnCalendar\\s*=", "flags": "", "sev": "info", "message": "A systemd timer fires within AccuracySec of the stated time and AccuracySec defaults to 1min, so this is not an exact-second schedule. Without Persistent=true a run missed while the machine was down never happens at all.", "fix": "AccuracySec=1s\nPersistent=true"}, {"pattern": "^\\s*@reboot\\b", "flags": "", "sev": "warn", "message": "@reboot fires on every start of the cron daemon, not once per machine boot. In a container that restarts - a rolling deploy, an OOM kill, a node drain - this runs again each time.", "fix": ""}, {"pattern": "^\\s*@(?:daily|midnight)\\b", "flags": "", "sev": "info", "message": "@daily is 0 0 * * * in the daemon's own zone. Midnight is the busiest minute on every shared runner and the exact instant the date rolls over, so a job that stamps 'yesterday' is one minute of clock skew away from stamping the wrong day.", "fix": "17 0 * * *"}, {"pattern": "cron\\.schedule\\s*\\(\\s*['\\\"](?:\\S+\\s+){5}\\S+['\\\"]", "flags": "", "sev": "info", "message": "node-cron reads a six-field expression as seconds first. The identical string copied into a crontab file or a Kubernetes CronJob means something completely different, because there the first field is minutes.", "fix": ""}, {"pattern": "^\\s*[\\d*/,\\-]+\\s+[\\d*/,\\-]+\\s+(?:\\?\\s|\\S+\\s+\\?\\s|\\S+\\s+\\S+\\s+\\?\\s)", "flags": "", "sev": "error", "message": "? is a Quartz and EventBridge character. Vixie cron, cronie, busybox crond and Kubernetes CronJob all reject it, and a crontab that fails to load leaves the previous crontab running.", "fix": ""}, {"pattern": "^\\s*[\\d*/,\\-]+\\s+[\\d*/,\\-]+\\s+\\S*[LW#]\\S*\\s", "flags": "", "sev": "error", "message": "L (last), W (nearest weekday) and # (nth weekday) are Quartz extensions. POSIX cron, Kubernetes CronJob and GitHub Actions do not understand them, so 'last day of month' has to be written as a guard inside the command.", "fix": ""}, {"pattern": "^\\s*(?:[\\d*/,\\-]+\\s+){5}.*[^\\\\]%", "flags": "", "sev": "error", "message": "In a crontab an unescaped % ends the command; everything after it is fed to the job on standard input. date +%Y-%m-%d therefore runs as date + and the filename comes out empty. Escape each one as \\%.", "fix": "date +\\%Y-\\%m-\\%d"}, {"pattern": "^\\s*(?:[\\d*/,\\-]+\\s+){5}(?:python[23]?|node|npm|npx|php|ruby|java|aws|kubectl|docker|psql|pg_dump)\\b", "flags": "", "sev": "error", "message": "cron runs with a minimal PATH of /usr/bin:/bin and none of your shell profile, so a bare python, node, aws or kubectl that works in your terminal is 'command not found' at 03:00 - and the error goes to local mail that nobody reads. Use the absolute path.", "fix": "/usr/local/bin/python3"}];
422
+ function scan(t){var L=String(t).split(/\r?\n/),h=[],i,r,re;
423
+ for(i=0;i<L.length;i++){for(var k=0;k<RULES.length;k++){r=RULES[k];
424
+ try{re=new RegExp(r.pattern,r.flags||'')}catch(e){continue}
425
+ if(re.test(L[i]))h.push({line:i+1,msg:r.message,sev:r.sev||'warn'})}}
426
+ var zs=document.getElementById('zone'),z=zs?zs.value:'UTC';
427
+ var comp=CRONX.analyzeText(t,z);
428
+ for(var c=0;c<comp.length;c++)h.push({line:comp[c].line,msg:comp[c].msg,sev:comp[c].sev});
429
+ h.sort(function(a,b){return a.line-b.line});
430
+ return h}
431
+ document.getElementById('zone').onchange=function(){document.getElementById('go').click()};
432
+ document.getElementById('go').onclick=function(){
433
+ var t=document.getElementById('src').value,o=document.getElementById('out');
434
+ if(!t.trim()){o.innerHTML='';return}
435
+ var h=scan(t);
436
+ if(!h.length){o.innerHTML='<div class="ok">No schedule in this file fires at the wrong time.</div>';return}
437
+ var s='<table><tr><th>Line</th><th>Finding</th></tr>';
438
+ for(var i=0;i<h.length;i++){s+='<tr><td class="n">'+h[i].line+'</td><td>'+
439
+ String(h[i].msg).replace(/&/g,'&amp;').replace(/</g,'&lt;')+'</td></tr>'}
440
+ o.innerHTML=s+'</table>'};
441
+ document.getElementById('go').click();
442
+ </script>
443
  </html>
444
+
445
+
446
+ <section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.cron-schedule-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/cron-schedule-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/cron-schedule-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/cron-schedule-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/cron-schedule-lint" rel="noopener" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.cron-schedule-lint
447
+ npx @readystack/cron-schedule-lint &lt;file&gt;
448
+ npx @readystack/cron-schedule-lint --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://buy.polar.sh/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>
449
+ <section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Cron Schedule Lint for crontab, Kubernetes CronJob, GitHub Actions, Spring and EventBridge every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"cron-schedule-lint",lang:"en",source:"rule_alert"})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script></section>
450
+
451
+ <section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Names every schedule line in the file you have open that fires at the wrong hour, twice, or never at all - and prints the next three times each one really fires, in the zone that actually ap</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">Healthchecks.io Business, the ordinary cron monitor, is $20 every month for 100 jobs - $240 a year, forever - and by design it can only tell you after a run was already missed.</p><a href="https://buy.polar.sh/polar_cl_t0Tavjz6GbjmBsKk04ImLmG6Y3VDIbuP0pht50WAFPX" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>
452
+ <section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Cron Schedule Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads the crontab, Kubernetes CronJob, GitHub Actions workflow, Spring annotation, wrangler.toml or systemd timer you have open and, for every schedule line, prints the next three instants it really fires at, how many times it fires in a year, and the exact dates when that wall-clock time does not exist or happens twice. Twenty-nine rules ship with it.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Backend, platform and SRE engineers who own the schedules nobody watches: the monthly invoice run, the nightly rollup, the export a finance team waits for. It is aimed at the person reviewing a schedule an assistant generated, who will be asked three weeks later why November&#x27;s invoices never went out.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why can I not just check the expression on a cron explainer site?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Because a syntax explainer holds no calendar and no time zone. It cannot tell you that 0 0 31 * * skips five months of every year, that 01:30 in America/New_York happens twice on 2026-11-01, or that 02:30 in Pacific/Auckland does not exist at all on 2026-09-27. Those answers need the time zone database, not a parser.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Checking the file you have open is free and complete: all twenty-nine rules run, every schedule gets its real run times and its transition dates, and nothing is hidden, watermarked or time-limited. The licence covers a different job - every schedule in the repository merged into one calendar, a CSV, JSON or HTML export, and machine output a CI step can fail on.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">How does $29 compare with a cron monitor?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The extension is 29 US dollars once, with a 7-day full refund. Healthchecks.io Business, the ordinary cron monitor, is $20 every month for 100 jobs, which is $240 a year, forever - and by design it can only alert you after a run was already missed. This one answers before the schedule ships.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Cron Schedule Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads the crontab, Kubernetes CronJob, GitHub Actions workflow, Spring annotation, wrangler.toml or systemd timer you have open and, for every schedule line, prints the next three instants it really fires at, how many times it fires in a year, and the exact dates when that wall-clock time does not exist or happens twice. Twenty-nine rules ship with it."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Backend, platform and SRE engineers who own the schedules nobody watches: the monthly invoice run, the nightly rollup, the export a finance team waits for. It is aimed at the person reviewing a schedule an assistant generated, who will be asked three weeks later why November's invoices never went out."}}, {"@type": "Question", "name": "Why can I not just check the expression on a cron explainer site?", "acceptedAnswer": {"@type": "Answer", "text": "Because a syntax explainer holds no calendar and no time zone. It cannot tell you that 0 0 31 * * skips five months of every year, that 01:30 in America/New_York happens twice on 2026-11-01, or that 02:30 in Pacific/Auckland does not exist at all on 2026-09-27. Those answers need the time zone database, not a parser."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Checking the file you have open is free and complete: all twenty-nine rules run, every schedule gets its real run times and its transition dates, and nothing is hidden, watermarked or time-limited. The licence covers a different job - every schedule in the repository merged into one calendar, a CSV, JSON or HTML export, and machine output a CI step can fail on."}}, {"@type": "Question", "name": "How does $29 compare with a cron monitor?", "acceptedAnswer": {"@type": "Answer", "text": "The extension is 29 US dollars once, with a 7-day full refund. Healthchecks.io Business, the ordinary cron monitor, is $20 every month for 100 jobs, which is $240 a year, forever - and by design it can only alert you after a run was already missed. This one answers before the schedule ships."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:0,slug:"cron-schedule-lint",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence, 7-day refund.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/workflow-expiry-check"><span class="yj-rel-t">Workflow Expiry - dated GitHub Actions breakage</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/project-dashboard-template-tableau"><span class="yj-rel-t">Project Dashboard Template for Tableau — PMO Schedule, Budget &amp; Risk (.twb, co</span><span class="yj-rel-p">$120</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/actions-deprecation-lint-2026"><span class="yj-rel-t">GitHub Actions Deprecation Lint: the 2026 runner and action EOL dates</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/webflow-restaurant-template"><span class="yj-rel-t">Webflow Restaurant Template — delivered as self-contained HTML you own: 26 pag</span><span class="yj-rel-p">$49</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/cloud-cost-landmine-lint"><span class="yj-rel-t">Cloud Cost Landmine Lint for Terraform, CloudFormation and Kubernetes</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/play-release-blocker-lint"><span class="yj-rel-t">Google Play Release Blocker Lint - target API 36, Billing 8, 16 KB</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section><!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div>
453
+ <script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||'')})}).catch(function(){});}catch(e){}})();</script>