toolkit/src/routes/cron/+page.svelte
2026-03-15 16:34:31 -03:00

370 lines
8.5 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<svelte:head>
<title>Crontab Generator</title>
</svelte:head>
<script lang="js">
import { copy } from '$lib/clipboard.js';
/**
* @typedef {{ label: string, value: string }[]} Presets
*/
/** @type {{ label: string, name: string, placeholder: string, presets: Presets }[]} */
const fields = [
{
label: 'Minute',
name: 'minute',
placeholder: '0-59',
presets: [
{ label: '*', value: '*' },
{ label: '*/5', value: '*/5' },
{ label: '*/10', value: '*/10' },
{ label: '*/15', value: '*/15' },
{ label: '*/30', value: '*/30' },
{ label: '0', value: '0' },
],
},
{
label: 'Hour',
name: 'hour',
placeholder: '0-23',
presets: [
{ label: '*', value: '*' },
{ label: '*/2', value: '*/2' },
{ label: '*/4', value: '*/4' },
{ label: '*/6', value: '*/6' },
{ label: '*/12', value: '*/12' },
{ label: '0', value: '0' },
{ label: '6', value: '6' },
{ label: '12', value: '12' },
{ label: '18', value: '18' },
],
},
{
label: 'Day (month)',
name: 'dom',
placeholder: '1-31',
presets: [
{ label: '*', value: '*' },
{ label: '1', value: '1' },
{ label: '15', value: '15' },
{ label: 'L', value: 'L' },
],
},
{
label: 'Month',
name: 'month',
placeholder: '1-12',
presets: [
{ label: '*', value: '*' },
{ label: '1', value: '1' },
{ label: '3', value: '3' },
{ label: '6', value: '6' },
{ label: '9', value: '9' },
{ label: '12', value: '12' },
{ label: '1-6', value: '1-6' },
{ label: '7-12', value: '7-12' },
],
},
{
label: 'Day (week)',
name: 'dow',
placeholder: '0-7',
presets: [
{ label: '*', value: '*' },
{ label: 'MonFri', value: '1-5' },
{ label: 'SatSun', value: '6,0' },
{ label: 'Mon', value: '1' },
{ label: 'Wed', value: '3' },
{ label: 'Fri', value: '5' },
{ label: 'Sat', value: '6' },
{ label: 'Sun', value: '0' },
],
},
];
/** @type {Record<string, string>} */
let values = $state({
minute: '*',
hour: '*',
dom: '*',
month: '*',
dow: '*',
});
const expression = $derived(
`${values.minute} ${values.hour} ${values.dom} ${values.month} ${values.dow}`
);
/** @param {string} field @param {string} val */
const set = (field, val) => { values[field] = val; };
const DOW_NAMES = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MON_NAMES = ['', 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
/** @param {string} v @returns {string} */
function describeMinute(v) {
if (v === '*') return 'every minute';
if (v === '0') return 'at minute 0';
if (v.startsWith('*/')) return `every ${v.slice(2)} minutes`;
return `at minute ${v}`;
}
/** @param {string} v @returns {string} */
function describeHour(v) {
if (v === '*') return 'every hour';
if (v.startsWith('*/')) return `every ${v.slice(2)} hours`;
if (/^\d+$/.test(v)) return `at ${v.padStart(2, '0')}:xx`;
return `at hour ${v}`;
}
/** @param {string} v @returns {string} */
function describeDom(v) {
if (v === '*') return '';
if (v === 'L') return 'on the last day of the month';
if (/^\d+$/.test(v)) return `on day ${v} of the month`;
return `on day-of-month ${v}`;
}
/** @param {string} v @returns {string} */
function describeMonth(v) {
if (v === '*') return '';
if (/^\d+$/.test(v)) return `in ${MON_NAMES[+v] ?? v}`;
if (v.includes('-')) {
const [a, b] = v.split('-');
return `from ${MON_NAMES[+a] ?? a} to ${MON_NAMES[+b] ?? b}`;
}
return `in month ${v}`;
}
/** @param {string} v @returns {string} */
function describeDow(v) {
if (v === '*') return '';
if (v === '1-5') return 'on weekdays (MonFri)';
if (v === '6,0' || v === '0,6') return 'on weekends';
if (/^\d+$/.test(v)) return `on ${DOW_NAMES[+v] ?? v}`;
if (v.includes('-')) {
const [a, b] = v.split('-');
return `from ${DOW_NAMES[+a] ?? a} to ${DOW_NAMES[+b] ?? b}`;
}
return `on day-of-week ${v}`;
}
const description = $derived((() => {
const min = describeMinute(values.minute);
const hr = describeHour(values.hour);
const dom = describeDom(values.dom);
const mon = describeMonth(values.month);
const dow = describeDow(values.dow);
let parts = [];
if (values.minute === '*' && values.hour === '*') {
parts.push('every minute');
} else if (values.minute === '0' && values.hour !== '*') {
parts.push(`at ${values.hour.padStart(2,'0')}:00`);
} else if (values.hour === '*') {
parts.push(min);
} else {
parts.push(`${min}, ${hr}`);
}
if (dom) parts.push(dom);
if (mon) parts.push(mon);
if (dow) parts.push(dow);
return parts.join(', ');
})());
/** Common presets */
const quickPresets = [
{ label: 'Every minute', expr: '* * * * *' },
{ label: 'Every 5 minutes', expr: '*/5 * * * *' },
{ label: 'Every 15 minutes', expr: '*/15 * * * *' },
{ label: 'Every 30 minutes', expr: '*/30 * * * *' },
{ label: 'Every hour', expr: '0 * * * *' },
{ label: 'Every 6 hours', expr: '0 */6 * * *' },
{ label: 'Every day at midnight', expr: '0 0 * * *' },
{ label: 'Every day at noon', expr: '0 12 * * *' },
{ label: 'Every Mon at 09:00',expr: '0 9 * * 1' },
{ label: 'Weekdays at 08:00', expr: '0 8 * * 1-5' },
{ label: 'Every Sunday at 03:00', expr: '0 3 * * 0' },
{ label: '1st of each month', expr: '0 0 1 * *' },
{ label: 'Every quarter', expr: '0 0 1 */3 *' },
{ label: 'Once a year (Jan 1)',expr: '0 0 1 1 *' },
];
/** @param {string} expr */
function applyPreset(expr) {
const parts = expr.split(' ');
values.minute = parts[0];
values.hour = parts[1];
values.dom = parts[2];
values.month = parts[3];
values.dow = parts[4];
}
/** @param {string} expr */
function parseExpression(expr) {
const parts = expr.trim().split(/\s+/);
if (parts.length === 5) applyPreset(parts.join(' '));
}
let manualInput = $state('');
$effect(() => {
manualInput = expression;
});
</script>
<h1>Cron</h1>
<h2>Quick presets</h2>
<div class="presets">
{#each quickPresets as p}
<button onclick={() => applyPreset(p.expr)}>{p.label}</button>
{/each}
</div>
<h2>Builder</h2>
<table class="builder">
<thead>
<tr>
{#each fields as f}
<th>{f.label}</th>
{/each}
</tr>
</thead>
<tbody>
<tr>
{#each fields as f}
<td>
<input
type="text"
value={values[f.name]}
oninput={(e) => set(f.name, /** @type {HTMLInputElement} */ (e.target).value)}
size="6"
spellcheck="false"
/>
</td>
{/each}
</tr>
<tr>
{#each fields as f}
<td class="chips">
{#each f.presets as p}
<button
class:selected={values[f.name] === p.value}
onclick={() => set(f.name, p.value)}
>{p.label}</button>
{/each}
</td>
{/each}
</tr>
</tbody>
</table>
<h2>Expression</h2>
<p>
<code class="expr">{expression}</code>
<button onclick={() => copy(expression)}>copy</button>
</p>
<p class="desc">{description}</p>
<h2>Paste &amp; parse</h2>
<p>
<input
type="text"
bind:value={manualInput}
size="30"
placeholder="* * * * *"
spellcheck="false"
oninput={() => parseExpression(manualInput)}
/>
<button onclick={() => parseExpression(manualInput)}>parse</button>
</p>
<h2>Reference</h2>
<table class="ref">
<thead>
<tr><th>Field</th><th>Range</th><th>Special</th></tr>
</thead>
<tbody>
<tr><td>Minute</td><td>059</td><td rowspan="5"><code>*</code> any &nbsp; <code>,</code> list &nbsp; <code>-</code> range &nbsp; <code>/</code> step</td></tr>
<tr><td>Hour</td><td>023</td></tr>
<tr><td>Day (month)</td><td>131</td></tr>
<tr><td>Month</td><td>112</td></tr>
<tr><td>Day (week)</td><td>07 (0,7=Sun)</td></tr>
</tbody>
</table>
<style>
.presets {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-bottom: 8px;
}
.builder {
border-collapse: collapse;
margin-bottom: 8px;
}
.builder th,
.builder td {
border: 1px solid var(--border);
padding: 4px 8px;
vertical-align: top;
}
.builder th {
background: var(--bg-raised);
font-size: 0.85em;
}
.builder input {
width: 60px;
font-family: monospace;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 2px;
min-width: 80px;
}
.chips button.selected {
background: var(--active-bg);
color: var(--active-fg);
border-color: var(--active-bg);
}
.expr {
font-size: 1.3em;
margin-right: 8px;
letter-spacing: 0.05em;
}
.desc {
color: var(--fg-dim);
font-style: italic;
margin-top: 0;
}
.ref {
border-collapse: collapse;
font-size: 0.9em;
}
.ref th,
.ref td {
border: 1px solid var(--border);
padding: 3px 8px;
}
.ref th {
background: var(--bg-raised);
}
</style>