asdasd
This commit is contained in:
@@ -1390,64 +1390,423 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
<a class="nav-link" href="/modules">Zurück</a>
|
||||
</div>
|
||||
<?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>
|
||||
(() => {
|
||||
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 = [
|
||||
['0', 'Sonntag'],
|
||||
['1', 'Montag'],
|
||||
['2', 'Dienstag'],
|
||||
['3', 'Mittwoch'],
|
||||
['4', 'Donnerstag'],
|
||||
['5', 'Freitag'],
|
||||
['6', 'Samstag'],
|
||||
];
|
||||
const weekdayMap = {
|
||||
'0': 'Sonntag',
|
||||
'1': 'Montag',
|
||||
'2': 'Dienstag',
|
||||
'3': 'Mittwoch',
|
||||
'4': 'Donnerstag',
|
||||
'5': 'Freitag',
|
||||
'6': 'Samstag',
|
||||
};
|
||||
|
||||
const buildExpression = (kind, values) => {
|
||||
const [hourRaw, minuteRaw] = String(values.time || '18:00').split(':');
|
||||
const minute = Math.max(0, Math.min(59, Number(minuteRaw || 0)));
|
||||
const hour = Math.max(0, Math.min(23, Number(hourRaw || 0)));
|
||||
const everyDays = Math.max(1, Number(values.intervalDays || 2));
|
||||
const weekday = String(values.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)));
|
||||
const modalState = {
|
||||
entry: null,
|
||||
tab: 'daily',
|
||||
hour: '18',
|
||||
minute: '00',
|
||||
weekday: '1',
|
||||
};
|
||||
|
||||
switch (kind) {
|
||||
case 'every_x_days':
|
||||
return `${minute} ${hour} */${everyDays} * *`;
|
||||
const modalFields = {
|
||||
enabled: modal.querySelector('[data-modal-enabled]'),
|
||||
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':
|
||||
return `${minute} ${hour} * * ${weekday}`;
|
||||
case 'monthly_day':
|
||||
return `${minute} ${hour} * * ${modalState.weekday || '1'}`;
|
||||
case 'monthly':
|
||||
return `${minute} ${hour} ${monthDay} * *`;
|
||||
case 'every_x_hours':
|
||||
return `${minute} */${everyHours} * * *`;
|
||||
case 'custom':
|
||||
return String(modalFields.expression.value || '').trim();
|
||||
case 'daily':
|
||||
default:
|
||||
return `${minute} ${hour} * * *`;
|
||||
}
|
||||
};
|
||||
|
||||
const weekdayMarkup = () => weekdayOptions.map(([value, label]) => (
|
||||
`<option value="${value}">${label}</option>`
|
||||
)).join('');
|
||||
const buildSummary = (tab) => {
|
||||
const enabled = modalFields.enabled.value === '1' ? 'Aktiv' : 'Inaktiv';
|
||||
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 jobName = job.dataset.jobName || 'job';
|
||||
const entry = document.createElement('div');
|
||||
entry.className = 'scheduler-entry';
|
||||
entry.dataset.schedulerEntry = '';
|
||||
entry.innerHTML = `
|
||||
<label><input type="checkbox" value="1" data-enabled> Aktiv</label>
|
||||
<input type="text" value="" data-cron-expression>
|
||||
<small class="muted">Cron-Syntax: Minute Stunde Tag Monat Wochentag</small>
|
||||
<input type="text" value="" data-cron-timezone>
|
||||
<select data-cron-builder-mode>
|
||||
<option value="builder">Builder</option>
|
||||
<option value="manual">Cron-Syntax</option>
|
||||
</select>
|
||||
<div data-cron-builder-fields>
|
||||
<input type="hidden" value="" data-cron-expression>
|
||||
<input type="hidden" value="" data-cron-timezone>
|
||||
<input type="hidden" value="builder" data-cron-builder-mode>
|
||||
<div data-cron-builder-fields hidden>
|
||||
<select data-cron-builder-kind>
|
||||
<option value="daily">Taeglich</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="every_x_hours">Alle x Stunden</option>
|
||||
</select>
|
||||
<input type="time" value="18:00" data-cron-builder-time>
|
||||
<input type="number" min="1" value="2" data-cron-builder-interval-days>
|
||||
<select data-cron-builder-weekday>${weekdayMarkup()}</select>
|
||||
<input type="number" min="1" max="31" value="1" data-cron-builder-month-day>
|
||||
<input type="number" min="1" max="23" value="6" data-cron-builder-interval-hours>
|
||||
<input type="hidden" value="18:00" data-cron-builder-time>
|
||||
<input type="hidden" value="2" data-cron-builder-interval-days>
|
||||
<select data-cron-builder-weekday>
|
||||
<option value="0">Sonntag</option>
|
||||
<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>
|
||||
<small class="muted">Letzter Start: -</small>
|
||||
<small class="muted">Letzter Erfolg: -</small>
|
||||
<small class="muted">Naechster Lauf UTC: -</small>
|
||||
<small class="muted">Naechster Lauf lokal: -</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);
|
||||
entry.querySelector('[data-cron-expression]').value = values.cron_expression || '0 18 * * *';
|
||||
entry.querySelector('[data-cron-timezone]').value = values.timezone || 'UTC';
|
||||
entry.querySelector('[data-cron-builder-mode]').value = values.builderMode || 'builder';
|
||||
entry.querySelector('[data-cron-builder-kind]').value = values.builderKind || 'daily';
|
||||
entry.querySelector('[data-cron-builder-time]').value = values.time || '18:00';
|
||||
entry.querySelector('[data-cron-builder-interval-days]').value = values.intervalDays || '2';
|
||||
entry.querySelector('[data-cron-builder-weekday]').value = values.weekday || '1';
|
||||
entry.querySelector('[data-cron-builder-month-day]').value = values.monthDay || '1';
|
||||
entry.querySelector('[data-cron-builder-interval-hours]').value = values.intervalHours || '6';
|
||||
|
||||
getEntryField(entry, '[data-enabled]').checked = Boolean(values.enabled);
|
||||
getEntryField(entry, '[data-cron-expression]').value = values.cron_expression || '0 18 * * *';
|
||||
getEntryField(entry, '[data-cron-timezone]').value = values.timezone || 'UTC';
|
||||
getEntryField(entry, '[data-cron-builder-mode]').value = values.builderMode || 'builder';
|
||||
getEntryField(entry, '[data-cron-builder-kind]').value = values.builderKind || 'daily';
|
||||
getEntryField(entry, '[data-cron-builder-time]').value = values.time || '18:00';
|
||||
getEntryField(entry, '[data-cron-builder-interval-days]').value = values.intervalDays || '2';
|
||||
getEntryField(entry, '[data-cron-builder-weekday]').value = values.weekday || '1';
|
||||
getEntryField(entry, '[data-cron-builder-month-day]').value = values.monthDay || '1';
|
||||
getEntryField(entry, '[data-cron-builder-interval-hours]').value = values.intervalHours || '6';
|
||||
return entry;
|
||||
};
|
||||
|
||||
@@ -1505,80 +1869,72 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
});
|
||||
};
|
||||
|
||||
const bindEntry = (entry) => {
|
||||
const expression = entry.querySelector('[data-cron-expression]');
|
||||
const mode = entry.querySelector('[data-cron-builder-mode]');
|
||||
const fields = entry.querySelector('[data-cron-builder-fields]');
|
||||
const kind = entry.querySelector('[data-cron-builder-kind]');
|
||||
const time = entry.querySelector('[data-cron-builder-time]');
|
||||
const intervalDays = entry.querySelector('[data-cron-builder-interval-days]');
|
||||
const weekday = entry.querySelector('[data-cron-builder-weekday]');
|
||||
const monthDay = entry.querySelector('[data-cron-builder-month-day]');
|
||||
const intervalHours = entry.querySelector('[data-cron-builder-interval-hours]');
|
||||
if (!expression || !mode || !fields || !kind || !time || !intervalDays || !weekday || !monthDay || !intervalHours) {
|
||||
return;
|
||||
const bindEntry = (job, entry) => {
|
||||
hideEditorNodes(entry);
|
||||
updateEntrySummary(entry);
|
||||
|
||||
let actions = entry.querySelector('[data-entry-actions]');
|
||||
if (!actions) {
|
||||
actions = document.createElement('div');
|
||||
actions.className = 'scheduler-entry__actions';
|
||||
actions.dataset.entryActions = '';
|
||||
actions.innerHTML = `
|
||||
<button class="nav-link" type="button" data-entry-edit>Bearbeiten</button>
|
||||
${job.dataset.jobMode === 'multi' ? '<button class="nav-link" type="button" data-remove-scheduler-entry>Entfernen</button>' : ''}
|
||||
`;
|
||||
entry.appendChild(actions);
|
||||
}
|
||||
|
||||
const sync = () => {
|
||||
const builderMode = mode.value === 'manual' ? 'manual' : 'builder';
|
||||
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;
|
||||
actions.querySelector('[data-entry-edit]')?.addEventListener('click', () => openModal(entry));
|
||||
actions.querySelector('[data-remove-scheduler-entry]')?.addEventListener('click', () => {
|
||||
entry.remove();
|
||||
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) => {
|
||||
job.querySelectorAll('[data-scheduler-entry]').forEach((entry) => bindEntry(entry));
|
||||
job.querySelectorAll('[data-scheduler-entry]').forEach((entry) => bindEntry(job, entry));
|
||||
reindexJob(job);
|
||||
|
||||
const addButton = job.querySelector('[data-add-scheduler-entry]');
|
||||
if (!addButton) return;
|
||||
|
||||
addButton.addEventListener('click', () => {
|
||||
addButton?.addEventListener('click', () => {
|
||||
const container = job.querySelector('[data-scheduler-entries]');
|
||||
if (!container) return;
|
||||
|
||||
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);
|
||||
const entry = createEntry(job, { timezone: container.querySelector('[data-cron-timezone]')?.value || 'UTC' });
|
||||
container.appendChild(entry);
|
||||
bindEntry(entry);
|
||||
bindEntry(job, entry);
|
||||
reindexJob(job);
|
||||
openModal(entry);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -1591,16 +1947,141 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
|
||||
.scheduler-entry {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--surface) 88%, white);
|
||||
}
|
||||
|
||||
[data-cron-builder-fields] {
|
||||
.scheduler-entry__summary {
|
||||
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;
|
||||
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>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user