asdasd
All checks were successful
Deploy / deploy-production (push) Has been skipped
Deploy / deploy-staging (push) Successful in 6s

This commit is contained in:
2026-04-30 03:12:30 +02:00
parent 8879a4ae5c
commit e1b2f7e613

View File

@@ -1390,64 +1390,423 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
<a class="nav-link" href="/modules">Zurück</a> <a class="nav-link" href="/modules">Zurück</a>
</div> </div>
<?php endif; ?> <?php endif; ?>
<div class="scheduler-modal" data-scheduler-modal hidden>
<div class="scheduler-modal__backdrop" data-scheduler-close></div>
<div class="scheduler-modal__dialog" role="dialog" aria-modal="true" aria-labelledby="scheduler-modal-title">
<div class="scheduler-modal__head">
<div>
<span class="pill">Scheduler</span>
<h3 id="scheduler-modal-title">Cron bearbeiten</h3>
</div>
<button class="nav-link" type="button" data-scheduler-close>Schliessen</button>
</div>
<div class="scheduler-modal__body">
<label class="setup-field muted">
<span>Aktiv</span>
<select data-modal-enabled>
<option value="1">Ja</option>
<option value="0">Nein</option>
</select>
</label>
<label class="setup-field muted">
<span>Zeitzone</span>
<input type="text" value="UTC" data-modal-timezone>
</label>
<div class="scheduler-builder">
<div class="scheduler-builder__tabs">
<button class="scheduler-chip" type="button" data-builder-tab="hourly">Hourly</button>
<button class="scheduler-chip" type="button" data-builder-tab="daily">Daily</button>
<button class="scheduler-chip" type="button" data-builder-tab="weekly">Weekly</button>
<button class="scheduler-chip" type="button" data-builder-tab="monthly">Monthly</button>
<button class="scheduler-chip" type="button" data-builder-tab="custom">Custom</button>
</div>
<div class="scheduler-builder__panel" data-builder-panel="hourly" hidden>
<label class="setup-field muted">
<span>Alle x Stunden</span>
<input type="number" min="1" max="23" value="6" data-modal-interval-hours>
</label>
<div>
<span class="scheduler-builder__label">Minuten</span>
<div class="scheduler-builder__chips" data-minute-chips></div>
</div>
</div>
<div class="scheduler-builder__panel" data-builder-panel="daily" hidden>
<div>
<span class="scheduler-builder__label">Stunden</span>
<div class="scheduler-builder__chips" data-hour-chips></div>
</div>
<div>
<span class="scheduler-builder__label">Minuten</span>
<div class="scheduler-builder__chips" data-minute-chips></div>
</div>
</div>
<div class="scheduler-builder__panel" data-builder-panel="weekly" hidden>
<div>
<span class="scheduler-builder__label">Wochentage</span>
<div class="scheduler-builder__chips" data-weekday-chips></div>
</div>
<div>
<span class="scheduler-builder__label">Stunden</span>
<div class="scheduler-builder__chips" data-hour-chips></div>
</div>
<div>
<span class="scheduler-builder__label">Minuten</span>
<div class="scheduler-builder__chips" data-minute-chips></div>
</div>
</div>
<div class="scheduler-builder__panel" data-builder-panel="monthly" hidden>
<label class="setup-field muted">
<span>Tag im Monat</span>
<input type="number" min="1" max="31" value="1" data-modal-month-day>
</label>
<div>
<span class="scheduler-builder__label">Stunden</span>
<div class="scheduler-builder__chips" data-hour-chips></div>
</div>
<div>
<span class="scheduler-builder__label">Minuten</span>
<div class="scheduler-builder__chips" data-minute-chips></div>
</div>
</div>
<div class="scheduler-builder__panel" data-builder-panel="custom" hidden>
<label class="setup-field muted">
<span>Cron-Syntax</span>
<input type="text" value="" data-modal-expression>
</label>
</div>
</div>
<div class="scheduler-preview">
<span>Vorschau</span>
<strong data-modal-summary>-</strong>
<code data-modal-code>-</code>
</div>
</div>
<div class="scheduler-modal__actions">
<button class="cta-button" type="button" data-scheduler-save>Uebernehmen</button>
<button class="nav-link" type="button" data-scheduler-close>Abbrechen</button>
</div>
</div>
</div>
<script> <script>
(() => { (() => {
const jobs = document.querySelectorAll('[data-scheduler-job]'); const jobs = document.querySelectorAll('[data-scheduler-job]');
if (!jobs.length) return; const modal = document.querySelector('[data-scheduler-modal]');
if (!jobs.length || !modal) return;
const weekdayOptions = [ const weekdayMap = {
['0', 'Sonntag'], '0': 'Sonntag',
['1', 'Montag'], '1': 'Montag',
['2', 'Dienstag'], '2': 'Dienstag',
['3', 'Mittwoch'], '3': 'Mittwoch',
['4', 'Donnerstag'], '4': 'Donnerstag',
['5', 'Freitag'], '5': 'Freitag',
['6', 'Samstag'], '6': 'Samstag',
]; };
const buildExpression = (kind, values) => { const modalState = {
const [hourRaw, minuteRaw] = String(values.time || '18:00').split(':'); entry: null,
const minute = Math.max(0, Math.min(59, Number(minuteRaw || 0))); tab: 'daily',
const hour = Math.max(0, Math.min(23, Number(hourRaw || 0))); hour: '18',
const everyDays = Math.max(1, Number(values.intervalDays || 2)); minute: '00',
const weekday = String(values.weekday || '1'); weekday: '1',
const monthDay = Math.max(1, Math.min(31, Number(values.monthDay || 1))); };
const everyHours = Math.max(1, Math.min(23, Number(values.intervalHours || 6)));
switch (kind) { const modalFields = {
case 'every_x_days': enabled: modal.querySelector('[data-modal-enabled]'),
return `${minute} ${hour} */${everyDays} * *`; timezone: modal.querySelector('[data-modal-timezone]'),
intervalHours: modal.querySelector('[data-modal-interval-hours]'),
monthDay: modal.querySelector('[data-modal-month-day]'),
expression: modal.querySelector('[data-modal-expression]'),
summary: modal.querySelector('[data-modal-summary]'),
code: modal.querySelector('[data-modal-code]'),
tabs: Array.from(modal.querySelectorAll('[data-builder-tab]')),
panels: Array.from(modal.querySelectorAll('[data-builder-panel]')),
};
const minuteContainers = Array.from(modal.querySelectorAll('[data-minute-chips]'));
const hourContainers = Array.from(modal.querySelectorAll('[data-hour-chips]'));
const weekdayContainers = Array.from(modal.querySelectorAll('[data-weekday-chips]'));
const chipButton = (label, value, group) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'scheduler-chip';
button.textContent = label;
button.dataset.value = value;
button.dataset.group = group;
return button;
};
const populateChips = () => {
minuteContainers.forEach((container) => {
if (container.children.length) return;
['00', '05', '10', '15', '20', '25', '30', '35', '40', '45', '50', '55'].forEach((minute) => {
container.appendChild(chipButton(minute, minute, 'minute'));
});
});
hourContainers.forEach((container) => {
if (container.children.length) return;
Array.from({ length: 24 }, (_, hour) => String(hour).padStart(2, '0')).forEach((hour) => {
container.appendChild(chipButton(hour, hour, 'hour'));
});
});
weekdayContainers.forEach((container) => {
if (container.children.length) return;
Object.entries(weekdayMap).forEach(([value, label]) => {
container.appendChild(chipButton(label.slice(0, 3), value, 'weekday'));
});
});
};
const formatTime = (hour, minute) => `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
const buildExpression = (tab) => {
const minute = Math.max(0, Math.min(59, Number(modalState.minute || '0')));
const hour = Math.max(0, Math.min(23, Number(modalState.hour || '0')));
const intervalHours = Math.max(1, Math.min(23, Number(modalFields.intervalHours.value || 6)));
const monthDay = Math.max(1, Math.min(31, Number(modalFields.monthDay.value || 1)));
switch (tab) {
case 'hourly':
return `${minute} */${intervalHours} * * *`;
case 'weekly': case 'weekly':
return `${minute} ${hour} * * ${weekday}`; return `${minute} ${hour} * * ${modalState.weekday || '1'}`;
case 'monthly_day': case 'monthly':
return `${minute} ${hour} ${monthDay} * *`; return `${minute} ${hour} ${monthDay} * *`;
case 'every_x_hours': case 'custom':
return `${minute} */${everyHours} * * *`; return String(modalFields.expression.value || '').trim();
case 'daily': case 'daily':
default: default:
return `${minute} ${hour} * * *`; return `${minute} ${hour} * * *`;
} }
}; };
const weekdayMarkup = () => weekdayOptions.map(([value, label]) => ( const buildSummary = (tab) => {
`<option value="${value}">${label}</option>` const enabled = modalFields.enabled.value === '1' ? 'Aktiv' : 'Inaktiv';
)).join(''); const timezone = modalFields.timezone.value.trim() || 'UTC';
const time = formatTime(modalState.hour, modalState.minute);
switch (tab) {
case 'hourly':
return `${enabled}, alle ${modalFields.intervalHours.value || '1'} Stunden um Minute ${modalState.minute}, ${timezone}`;
case 'weekly':
return `${enabled}, woechentlich ${weekdayMap[modalState.weekday || '1']} um ${time}, ${timezone}`;
case 'monthly':
return `${enabled}, monatlich am ${modalFields.monthDay.value || '1'}. um ${time}, ${timezone}`;
case 'custom':
return `${enabled}, benutzerdefinierte Cron-Syntax, ${timezone}`;
case 'daily':
default:
return `${enabled}, taeglich um ${time}, ${timezone}`;
}
};
const setActiveTab = (tab) => {
modalState.tab = tab;
modalFields.tabs.forEach((button) => {
button.classList.toggle('is-active', button.dataset.builderTab === tab);
});
modalFields.panels.forEach((panel) => {
panel.hidden = panel.dataset.builderPanel !== tab;
});
refreshPreview();
};
const refreshChipState = () => {
modal.querySelectorAll('.scheduler-chip[data-group="minute"]').forEach((button) => {
button.classList.toggle('is-active', button.dataset.value === modalState.minute);
});
modal.querySelectorAll('.scheduler-chip[data-group="hour"]').forEach((button) => {
button.classList.toggle('is-active', button.dataset.value === modalState.hour);
});
modal.querySelectorAll('.scheduler-chip[data-group="weekday"]').forEach((button) => {
button.classList.toggle('is-active', button.dataset.value === modalState.weekday);
});
};
const refreshPreview = () => {
if (modalState.tab !== 'custom') {
modalFields.expression.value = buildExpression(modalState.tab);
}
modalFields.summary.textContent = buildSummary(modalState.tab);
modalFields.code.textContent = buildExpression(modalState.tab) || '-';
refreshChipState();
};
const getEntryField = (entry, selector) => entry.querySelector(selector);
const updateEntrySummary = (entry) => {
const enabled = getEntryField(entry, '[data-enabled]')?.checked ?? false;
const expression = getEntryField(entry, '[data-cron-expression]')?.value || '';
const timezone = (getEntryField(entry, '[data-cron-timezone]')?.value || 'UTC').trim() || 'UTC';
const builderMode = getEntryField(entry, '[data-cron-builder-mode]')?.value || 'builder';
const builderKind = getEntryField(entry, '[data-cron-builder-kind]')?.value || 'daily';
const time = getEntryField(entry, '[data-cron-builder-time]')?.value || '18:00';
const weekday = getEntryField(entry, '[data-cron-builder-weekday]')?.value || '1';
const monthDay = getEntryField(entry, '[data-cron-builder-month-day]')?.value || '1';
const intervalHours = getEntryField(entry, '[data-cron-builder-interval-hours]')?.value || '6';
let summary = enabled ? 'Aktiv' : 'Inaktiv';
if (builderMode === 'manual') {
summary += `, Custom, ${timezone}`;
} else if (builderKind === 'every_x_hours') {
summary += `, alle ${intervalHours} Stunden, ${timezone}`;
} else if (builderKind === 'weekly') {
summary += `, ${weekdayMap[weekday] || weekday} ${time}, ${timezone}`;
} else if (builderKind === 'monthly_day') {
summary += `, monatlich am ${monthDay}. ${time}, ${timezone}`;
} else {
summary += `, taeglich ${time}, ${timezone}`;
}
let summaryNode = entry.querySelector('[data-entry-summary]');
if (!summaryNode) {
summaryNode = document.createElement('div');
summaryNode.className = 'scheduler-entry__summary';
summaryNode.dataset.entrySummary = '';
entry.prepend(summaryNode);
}
let statusNode = entry.querySelector('[data-entry-status]');
if (!statusNode) {
statusNode = document.createElement('div');
statusNode.className = 'scheduler-entry__status';
statusNode.dataset.entryStatus = '';
entry.appendChild(statusNode);
}
const lastStart = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Letzter Start'));
const lastSuccess = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Letzter Erfolg'));
const nextLocal = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Naechster Lauf lokal'));
const status = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Status:'));
const parseError = Array.from(entry.querySelectorAll('small')).find((node) => node.textContent.includes('Cron-Fehler'));
summaryNode.innerHTML = `
<strong>${summary}</strong>
<span>${expression || '-'}</span>
`;
statusNode.innerHTML = `
<small class="muted">${lastStart ? lastStart.textContent : 'Letzter Start: -'}</small>
<small class="muted">${lastSuccess ? lastSuccess.textContent : 'Letzter Erfolg: -'}</small>
<small class="muted">${nextLocal ? nextLocal.textContent : 'Naechster Lauf lokal: -'}</small>
<small class="muted">${status ? status.textContent : 'Status: -'}</small>
${parseError ? `<small class="muted scheduler-entry__error">${parseError.textContent}</small>` : ''}
`;
};
const hideEditorNodes = (entry) => {
[
'label',
'[data-cron-expression]',
'[data-cron-timezone]',
'[data-cron-builder-mode]',
'[data-cron-builder-fields]',
'[data-remove-scheduler-entry]',
].forEach((selector) => {
const node = selector === 'label' ? entry.querySelector(':scope > label') : entry.querySelector(selector);
if (node) {
node.classList.add('scheduler-entry__editor');
node.hidden = true;
}
});
Array.from(entry.querySelectorAll('small')).forEach((node) => {
node.classList.add('scheduler-entry__editor');
node.hidden = true;
});
};
const openModal = (entry) => {
modalState.entry = entry;
const mode = getEntryField(entry, '[data-cron-builder-mode]')?.value || 'builder';
const kind = getEntryField(entry, '[data-cron-builder-kind]')?.value || 'daily';
const time = getEntryField(entry, '[data-cron-builder-time]')?.value || '18:00';
const [hour = '18', minute = '00'] = time.split(':');
modalFields.enabled.value = getEntryField(entry, '[data-enabled]')?.checked ? '1' : '0';
modalFields.timezone.value = getEntryField(entry, '[data-cron-timezone]')?.value || 'UTC';
modalFields.intervalHours.value = getEntryField(entry, '[data-cron-builder-interval-hours]')?.value || '6';
modalFields.monthDay.value = getEntryField(entry, '[data-cron-builder-month-day]')?.value || '1';
modalFields.expression.value = getEntryField(entry, '[data-cron-expression]')?.value || '';
modalState.hour = String(hour).padStart(2, '0');
modalState.minute = String(minute).padStart(2, '0');
modalState.weekday = getEntryField(entry, '[data-cron-builder-weekday]')?.value || '1';
setActiveTab(mode === 'manual'
? 'custom'
: (kind === 'every_x_hours' ? 'hourly' : (kind === 'weekly' ? 'weekly' : (kind === 'monthly_day' ? 'monthly' : 'daily'))));
modal.hidden = false;
document.body.classList.add('scheduler-modal-open');
};
const closeModal = () => {
modal.hidden = true;
modalState.entry = null;
document.body.classList.remove('scheduler-modal-open');
};
const saveModal = () => {
const entry = modalState.entry;
if (!entry) return;
const modeNode = getEntryField(entry, '[data-cron-builder-mode]');
const kindNode = getEntryField(entry, '[data-cron-builder-kind]');
const timeNode = getEntryField(entry, '[data-cron-builder-time]');
const weekdayNode = getEntryField(entry, '[data-cron-builder-weekday]');
const monthDayNode = getEntryField(entry, '[data-cron-builder-month-day]');
const intervalHoursNode = getEntryField(entry, '[data-cron-builder-interval-hours]');
const intervalDaysNode = getEntryField(entry, '[data-cron-builder-interval-days]');
const enabledNode = getEntryField(entry, '[data-enabled]');
const expressionNode = getEntryField(entry, '[data-cron-expression]');
const timezoneNode = getEntryField(entry, '[data-cron-timezone]');
if (!modeNode || !kindNode || !timeNode || !weekdayNode || !monthDayNode || !intervalHoursNode || !intervalDaysNode || !enabledNode || !expressionNode || !timezoneNode) {
return;
}
enabledNode.checked = modalFields.enabled.value === '1';
timezoneNode.value = modalFields.timezone.value.trim() || 'UTC';
timeNode.value = formatTime(modalState.hour, modalState.minute);
weekdayNode.value = modalState.weekday;
monthDayNode.value = String(Math.max(1, Math.min(31, Number(modalFields.monthDay.value || 1))));
intervalHoursNode.value = String(Math.max(1, Math.min(23, Number(modalFields.intervalHours.value || 6))));
intervalDaysNode.value = intervalDaysNode.value || '2';
if (modalState.tab === 'custom') {
modeNode.value = 'manual';
expressionNode.value = modalFields.expression.value.trim();
} else {
modeNode.value = 'builder';
kindNode.value = modalState.tab === 'hourly'
? 'every_x_hours'
: (modalState.tab === 'weekly' ? 'weekly' : (modalState.tab === 'monthly' ? 'monthly_day' : 'daily'));
expressionNode.value = buildExpression(modalState.tab);
}
updateEntrySummary(entry);
closeModal();
};
const createEntry = (job, values = {}) => { const createEntry = (job, values = {}) => {
const jobName = job.dataset.jobName || 'job';
const entry = document.createElement('div'); const entry = document.createElement('div');
entry.className = 'scheduler-entry'; entry.className = 'scheduler-entry';
entry.dataset.schedulerEntry = ''; entry.dataset.schedulerEntry = '';
entry.innerHTML = ` entry.innerHTML = `
<label><input type="checkbox" value="1" data-enabled> Aktiv</label> <label><input type="checkbox" value="1" data-enabled> Aktiv</label>
<input type="text" value="" data-cron-expression> <input type="hidden" value="" data-cron-expression>
<small class="muted">Cron-Syntax: Minute Stunde Tag Monat Wochentag</small> <input type="hidden" value="" data-cron-timezone>
<input type="text" value="" data-cron-timezone> <input type="hidden" value="builder" data-cron-builder-mode>
<select data-cron-builder-mode> <div data-cron-builder-fields hidden>
<option value="builder">Builder</option>
<option value="manual">Cron-Syntax</option>
</select>
<div data-cron-builder-fields>
<select data-cron-builder-kind> <select data-cron-builder-kind>
<option value="daily">Taeglich</option> <option value="daily">Taeglich</option>
<option value="every_x_days">Alle x Tage</option> <option value="every_x_days">Alle x Tage</option>
@@ -1455,31 +1814,36 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
<option value="monthly_day">X-Tag im Monat</option> <option value="monthly_day">X-Tag im Monat</option>
<option value="every_x_hours">Alle x Stunden</option> <option value="every_x_hours">Alle x Stunden</option>
</select> </select>
<input type="time" value="18:00" data-cron-builder-time> <input type="hidden" value="18:00" data-cron-builder-time>
<input type="number" min="1" value="2" data-cron-builder-interval-days> <input type="hidden" value="2" data-cron-builder-interval-days>
<select data-cron-builder-weekday>${weekdayMarkup()}</select> <select data-cron-builder-weekday>
<input type="number" min="1" max="31" value="1" data-cron-builder-month-day> <option value="0">Sonntag</option>
<input type="number" min="1" max="23" value="6" data-cron-builder-interval-hours> <option value="1">Montag</option>
<option value="2">Dienstag</option>
<option value="3">Mittwoch</option>
<option value="4">Donnerstag</option>
<option value="5">Freitag</option>
<option value="6">Samstag</option>
</select>
<input type="hidden" value="1" data-cron-builder-month-day>
<input type="hidden" value="6" data-cron-builder-interval-hours>
</div> </div>
<small class="muted">Letzter Start: -</small> <small class="muted">Letzter Start: -</small>
<small class="muted">Letzter Erfolg: -</small> <small class="muted">Letzter Erfolg: -</small>
<small class="muted">Naechster Lauf UTC: -</small>
<small class="muted">Naechster Lauf lokal: -</small> <small class="muted">Naechster Lauf lokal: -</small>
<small class="muted">Status: -</small> <small class="muted">Status: -</small>
${job.dataset.jobMode === 'multi' ? '<button class="nav-link" type="button" data-remove-scheduler-entry>Eintrag entfernen</button>' : ''}
`; `;
entry.querySelector('[data-enabled]').checked = Boolean(values.enabled); getEntryField(entry, '[data-enabled]').checked = Boolean(values.enabled);
entry.querySelector('[data-cron-expression]').value = values.cron_expression || '0 18 * * *'; getEntryField(entry, '[data-cron-expression]').value = values.cron_expression || '0 18 * * *';
entry.querySelector('[data-cron-timezone]').value = values.timezone || 'UTC'; getEntryField(entry, '[data-cron-timezone]').value = values.timezone || 'UTC';
entry.querySelector('[data-cron-builder-mode]').value = values.builderMode || 'builder'; getEntryField(entry, '[data-cron-builder-mode]').value = values.builderMode || 'builder';
entry.querySelector('[data-cron-builder-kind]').value = values.builderKind || 'daily'; getEntryField(entry, '[data-cron-builder-kind]').value = values.builderKind || 'daily';
entry.querySelector('[data-cron-builder-time]').value = values.time || '18:00'; getEntryField(entry, '[data-cron-builder-time]').value = values.time || '18:00';
entry.querySelector('[data-cron-builder-interval-days]').value = values.intervalDays || '2'; getEntryField(entry, '[data-cron-builder-interval-days]').value = values.intervalDays || '2';
entry.querySelector('[data-cron-builder-weekday]').value = values.weekday || '1'; getEntryField(entry, '[data-cron-builder-weekday]').value = values.weekday || '1';
entry.querySelector('[data-cron-builder-month-day]').value = values.monthDay || '1'; getEntryField(entry, '[data-cron-builder-month-day]').value = values.monthDay || '1';
entry.querySelector('[data-cron-builder-interval-hours]').value = values.intervalHours || '6'; getEntryField(entry, '[data-cron-builder-interval-hours]').value = values.intervalHours || '6';
return entry; return entry;
}; };
@@ -1505,80 +1869,72 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
}); });
}; };
const bindEntry = (entry) => { const bindEntry = (job, entry) => {
const expression = entry.querySelector('[data-cron-expression]'); hideEditorNodes(entry);
const mode = entry.querySelector('[data-cron-builder-mode]'); updateEntrySummary(entry);
const fields = entry.querySelector('[data-cron-builder-fields]');
const kind = entry.querySelector('[data-cron-builder-kind]'); let actions = entry.querySelector('[data-entry-actions]');
const time = entry.querySelector('[data-cron-builder-time]'); if (!actions) {
const intervalDays = entry.querySelector('[data-cron-builder-interval-days]'); actions = document.createElement('div');
const weekday = entry.querySelector('[data-cron-builder-weekday]'); actions.className = 'scheduler-entry__actions';
const monthDay = entry.querySelector('[data-cron-builder-month-day]'); actions.dataset.entryActions = '';
const intervalHours = entry.querySelector('[data-cron-builder-interval-hours]'); actions.innerHTML = `
if (!expression || !mode || !fields || !kind || !time || !intervalDays || !weekday || !monthDay || !intervalHours) { <button class="nav-link" type="button" data-entry-edit>Bearbeiten</button>
return; ${job.dataset.jobMode === 'multi' ? '<button class="nav-link" type="button" data-remove-scheduler-entry>Entfernen</button>' : ''}
`;
entry.appendChild(actions);
} }
const sync = () => { actions.querySelector('[data-entry-edit]')?.addEventListener('click', () => openModal(entry));
const builderMode = mode.value === 'manual' ? 'manual' : 'builder'; actions.querySelector('[data-remove-scheduler-entry]')?.addEventListener('click', () => {
fields.hidden = builderMode === 'manual';
expression.readOnly = builderMode !== 'manual';
if (builderMode === 'builder') {
expression.value = buildExpression(kind.value, {
time: time.value,
intervalDays: intervalDays.value,
weekday: weekday.value,
monthDay: monthDay.value,
intervalHours: intervalHours.value,
});
}
};
[mode, kind, time, intervalDays, weekday, monthDay, intervalHours].forEach((node) => {
node.addEventListener('change', sync);
node.addEventListener('input', sync);
});
const removeButton = entry.querySelector('[data-remove-scheduler-entry]');
if (removeButton) {
removeButton.addEventListener('click', () => {
const job = entry.closest('[data-scheduler-job]');
if (!job) return;
entry.remove(); entry.remove();
reindexJob(job); reindexJob(job);
}); });
}
sync();
}; };
populateChips();
modalFields.tabs.forEach((button) => {
button.addEventListener('click', () => setActiveTab(button.dataset.builderTab || 'daily'));
});
modal.querySelectorAll('.scheduler-chip[data-group]').forEach((button) => {
button.addEventListener('click', () => {
const group = button.dataset.group;
if (group === 'hour') {
modalState.hour = button.dataset.value || '18';
} else if (group === 'minute') {
modalState.minute = button.dataset.value || '00';
} else if (group === 'weekday') {
modalState.weekday = button.dataset.value || '1';
}
refreshPreview();
});
});
[modalFields.enabled, modalFields.timezone, modalFields.intervalHours, modalFields.monthDay, modalFields.expression].forEach((node) => {
node?.addEventListener('input', refreshPreview);
node?.addEventListener('change', refreshPreview);
});
modal.querySelectorAll('[data-scheduler-close]').forEach((button) => {
button.addEventListener('click', closeModal);
});
modal.querySelector('[data-scheduler-save]')?.addEventListener('click', saveModal);
jobs.forEach((job) => { jobs.forEach((job) => {
job.querySelectorAll('[data-scheduler-entry]').forEach((entry) => bindEntry(entry)); job.querySelectorAll('[data-scheduler-entry]').forEach((entry) => bindEntry(job, entry));
reindexJob(job); reindexJob(job);
const addButton = job.querySelector('[data-add-scheduler-entry]'); const addButton = job.querySelector('[data-add-scheduler-entry]');
if (!addButton) return; addButton?.addEventListener('click', () => {
addButton.addEventListener('click', () => {
const container = job.querySelector('[data-scheduler-entries]'); const container = job.querySelector('[data-scheduler-entries]');
if (!container) return; if (!container) return;
const entry = createEntry(job, { timezone: container.querySelector('[data-cron-timezone]')?.value || 'UTC' });
const source = container.querySelector('[data-scheduler-entry]');
const values = source ? {
timezone: source.querySelector('[data-cron-timezone]')?.value || 'UTC',
builderMode: source.querySelector('[data-cron-builder-mode]')?.value || 'builder',
builderKind: source.querySelector('[data-cron-builder-kind]')?.value || 'daily',
time: source.querySelector('[data-cron-builder-time]')?.value || '18:00',
intervalDays: source.querySelector('[data-cron-builder-interval-days]')?.value || '2',
weekday: source.querySelector('[data-cron-builder-weekday]')?.value || '1',
monthDay: source.querySelector('[data-cron-builder-month-day]')?.value || '1',
intervalHours: source.querySelector('[data-cron-builder-interval-hours]')?.value || '6',
} : {};
const entry = createEntry(job, values);
container.appendChild(entry); container.appendChild(entry);
bindEntry(entry); bindEntry(job, entry);
reindexJob(job); reindexJob(job);
openModal(entry);
}); });
}); });
})(); })();
@@ -1591,16 +1947,141 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
.scheduler-entry { .scheduler-entry {
display: grid; display: grid;
gap: 10px; gap: 12px;
padding: 14px; padding: 14px;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: 12px; border-radius: 12px;
background: color-mix(in srgb, var(--surface) 88%, white); background: color-mix(in srgb, var(--surface) 88%, white);
} }
[data-cron-builder-fields] { .scheduler-entry__summary {
display: grid; display: grid;
gap: 4px;
}
.scheduler-entry__summary strong {
color: var(--text);
}
.scheduler-entry__summary span {
color: var(--muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 0.9rem;
}
.scheduler-entry__status {
display: grid;
gap: 4px;
}
.scheduler-entry__error {
color: #b42318;
}
.scheduler-entry__actions {
display: flex;
gap: 10px; gap: 10px;
flex-wrap: wrap;
}
.scheduler-modal[hidden] {
display: none;
}
.scheduler-modal {
position: fixed;
inset: 0;
z-index: 90;
}
.scheduler-modal__backdrop {
position: absolute;
inset: 0;
background: rgba(10, 18, 28, 0.45);
}
.scheduler-modal__dialog {
position: relative;
width: min(920px, calc(100vw - 32px));
margin: 40px auto;
max-height: calc(100vh - 80px);
overflow: auto;
padding: 22px;
border-radius: 18px;
background: var(--surface);
border: 1px solid var(--line);
box-shadow: 0 30px 70px rgba(1, 22, 32, 0.24);
}
.scheduler-modal__head,
.scheduler-modal__actions {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
}
.scheduler-modal__body {
display: grid;
gap: 18px;
margin: 18px 0;
}
.scheduler-builder {
display: grid;
gap: 16px;
}
.scheduler-builder__tabs,
.scheduler-builder__chips {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.scheduler-builder__label {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 0.92rem;
}
.scheduler-chip {
appearance: none;
border: 1px solid var(--line);
background: #fff;
color: var(--text);
border-radius: 10px;
padding: 8px 12px;
cursor: pointer;
font: inherit;
}
.scheduler-chip.is-active {
background: #17172b;
color: #fff;
border-color: #17172b;
}
.scheduler-preview {
display: grid;
gap: 6px;
padding: 14px;
border-radius: 12px;
background: color-mix(in srgb, var(--surface-strong) 86%, white);
border: 1px solid var(--line);
}
.scheduler-preview code {
display: inline-block;
width: fit-content;
padding: 4px 8px;
border-radius: 8px;
background: #eef2ff;
}
.scheduler-modal-open {
overflow: hidden;
} }
</style> </style>
</form> </form>