|
@@ -206,6 +206,9 @@ export class SchedulesPage {
|
|
|
// Find next scheduled run - for enabled, use active; for disabled, find most recent queued
|
|
// Find next scheduled run - for enabled, use active; for disabled, find most recent queued
|
|
|
const nextRun = hasEnabledSchedule ? activeSchedule : siteSchedules.find(s => s.status === 'queued');
|
|
const nextRun = hasEnabledSchedule ? activeSchedule : siteSchedules.find(s => s.status === 'queued');
|
|
|
|
|
|
|
|
|
|
+ const currentCron = site.scrape_cron || '';
|
|
|
|
|
+ const cronLabel = currentCron ? `Cron: <code class="text-xs">${currentCron}</code>` : 'Cron: <span class="text-xs italic">sitemap default</span>';
|
|
|
|
|
+
|
|
|
return `
|
|
return `
|
|
|
<div class="p-4 border border-gray-200 dark:border-gray-600 rounded-lg hover:shadow-md transition-shadow">
|
|
<div class="p-4 border border-gray-200 dark:border-gray-600 rounded-lg hover:shadow-md transition-shadow">
|
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center justify-between">
|
|
@@ -284,6 +287,28 @@ export class SchedulesPage {
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
|
|
+ <!-- Cron Editor -->
|
|
|
|
|
+ <div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
|
|
|
|
+ <div class="flex items-center justify-between mb-2">
|
|
|
|
|
+ <h5 class="text-sm font-medium text-gray-900 dark:text-white">Crawl frequency</h5>
|
|
|
|
|
+ <span class="text-xs text-gray-500 dark:text-gray-400">${cronLabel}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="flex items-center gap-2 flex-wrap">
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ class="cron-input flex-1 min-w-[200px] px-2 py-1 text-xs font-mono border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
|
|
+ placeholder="e.g. 0 3 * * * (daily at 03:00) — leave blank for sitemap default"
|
|
|
|
|
+ value="${currentCron}"
|
|
|
|
|
+ data-site-id="${site.id}"
|
|
|
|
|
+ />
|
|
|
|
|
+ <button type="button" class="btn btn-secondary btn-xs cron-preset" data-cron="0 * * * *" data-site-id="${site.id}" title="Every hour">Hourly</button>
|
|
|
|
|
+ <button type="button" class="btn btn-secondary btn-xs cron-preset" data-cron="0 3 * * *" data-site-id="${site.id}" title="Every day at 03:00">Daily</button>
|
|
|
|
|
+ <button type="button" class="btn btn-secondary btn-xs cron-preset" data-cron="0 3 * * 0" data-site-id="${site.id}" title="Every Sunday at 03:00">Weekly</button>
|
|
|
|
|
+ <button type="button" class="btn btn-secondary btn-xs cron-preset" data-cron="0 3 1 * *" data-site-id="${site.id}" title="1st of month at 03:00">Monthly</button>
|
|
|
|
|
+ <button type="button" class="btn btn-primary btn-xs cron-save" data-site-id="${site.id}">Save</button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
<!-- Schedule Details -->
|
|
<!-- Schedule Details -->
|
|
|
${siteSchedules.length > 0 ? `
|
|
${siteSchedules.length > 0 ? `
|
|
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
|
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-600">
|
|
@@ -354,6 +379,52 @@ export class SchedulesPage {
|
|
|
private bindToggleEvents(): void {
|
|
private bindToggleEvents(): void {
|
|
|
if (!this.element) return;
|
|
if (!this.element) return;
|
|
|
|
|
|
|
|
|
|
+ // Preset buttons fill the input for that site
|
|
|
|
|
+ this.element.querySelectorAll('.cron-preset').forEach(btn => {
|
|
|
|
|
+ btn.addEventListener('click', (e) => {
|
|
|
|
|
+ const target = e.currentTarget as HTMLButtonElement;
|
|
|
|
|
+ const siteId = target.getAttribute('data-site-id');
|
|
|
|
|
+ const cron = target.getAttribute('data-cron') || '';
|
|
|
|
|
+ const input = this.element?.querySelector(`input.cron-input[data-site-id="${siteId}"]`) as HTMLInputElement | null;
|
|
|
|
|
+ if (input) input.value = cron;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Save button persists the cron expression
|
|
|
|
|
+ this.element.querySelectorAll('.cron-save').forEach(btn => {
|
|
|
|
|
+ btn.addEventListener('click', async (e) => {
|
|
|
|
|
+ const target = e.currentTarget as HTMLButtonElement;
|
|
|
|
|
+ const siteId = target.getAttribute('data-site-id');
|
|
|
|
|
+ if (!siteId) return;
|
|
|
|
|
+ const input = this.element?.querySelector(`input.cron-input[data-site-id="${siteId}"]`) as HTMLInputElement | null;
|
|
|
|
|
+ const raw = input?.value.trim() ?? '';
|
|
|
|
|
+ const cron = raw === '' ? null : raw;
|
|
|
|
|
+
|
|
|
|
|
+ target.disabled = true;
|
|
|
|
|
+ try {
|
|
|
|
|
+ const response = await this.apiService.updateSiteSchedule(siteId, { cron });
|
|
|
|
|
+ if (response.success) {
|
|
|
|
|
+ const next = response.data?.next_run_at;
|
|
|
|
|
+ this.toastService.success(
|
|
|
|
|
+ 'Schedule Updated',
|
|
|
|
|
+ cron
|
|
|
|
|
+ ? `Cron set to "${cron}"${next ? ` — next run ${new Date(next).toLocaleString()}` : ''}`
|
|
|
|
|
+ : 'Cron cleared — using sitemap default'
|
|
|
|
|
+ );
|
|
|
|
|
+ await this.loadScheduleDetails();
|
|
|
|
|
+ this.renderStats();
|
|
|
|
|
+ this.renderSchedulesList();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.toastService.error('Update Failed', response.error || 'Failed to update cron');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ this.toastService.error('Error', 'Failed to update cron');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ target.disabled = false;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
this.element.querySelectorAll('.schedule-toggle').forEach(toggle => {
|
|
this.element.querySelectorAll('.schedule-toggle').forEach(toggle => {
|
|
|
toggle.addEventListener('change', async (e) => {
|
|
toggle.addEventListener('change', async (e) => {
|
|
|
const checkbox = e.target as HTMLInputElement;
|
|
const checkbox = e.target as HTMLInputElement;
|
|
@@ -367,7 +438,7 @@ export class SchedulesPage {
|
|
|
checkbox.disabled = true;
|
|
checkbox.disabled = true;
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const response = await this.apiService.updateSiteSchedule(siteId, enabled);
|
|
|
|
|
|
|
+ const response = await this.apiService.updateSiteSchedule(siteId, { enabled });
|
|
|
|
|
|
|
|
if (response.success) {
|
|
if (response.success) {
|
|
|
this.toastService.success(
|
|
this.toastService.success(
|