fixes related to automation and celery schedules
This commit is contained in:
@@ -186,13 +186,76 @@ class UnifiedSiteSettingsViewSet(viewsets.ViewSet):
|
|||||||
# Update automation settings
|
# Update automation settings
|
||||||
if 'automation' in data:
|
if 'automation' in data:
|
||||||
auto = data['automation']
|
auto = data['automation']
|
||||||
|
schedule_changed = False
|
||||||
|
|
||||||
if 'enabled' in auto:
|
if 'enabled' in auto:
|
||||||
|
if automation_config.is_enabled != auto['enabled']:
|
||||||
|
schedule_changed = True
|
||||||
automation_config.is_enabled = auto['enabled']
|
automation_config.is_enabled = auto['enabled']
|
||||||
if 'frequency' in auto:
|
if 'frequency' in auto:
|
||||||
|
if automation_config.frequency != auto['frequency']:
|
||||||
|
schedule_changed = True
|
||||||
automation_config.frequency = auto['frequency']
|
automation_config.frequency = auto['frequency']
|
||||||
if 'time' in auto:
|
if 'time' in auto:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
automation_config.scheduled_time = datetime.strptime(auto['time'], '%H:%M').time()
|
new_time = datetime.strptime(auto['time'], '%H:%M').time()
|
||||||
|
if automation_config.scheduled_time != new_time:
|
||||||
|
schedule_changed = True
|
||||||
|
automation_config.scheduled_time = new_time
|
||||||
|
|
||||||
|
# Reset last_run_at and recalculate next_run_at if any schedule setting changed
|
||||||
|
if schedule_changed:
|
||||||
|
automation_config.last_run_at = None
|
||||||
|
|
||||||
|
# Recalculate next_run_at based on new schedule
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
scheduled_time = automation_config.scheduled_time
|
||||||
|
|
||||||
|
# Calculate next run at the scheduled time
|
||||||
|
next_run = now.replace(
|
||||||
|
hour=scheduled_time.hour,
|
||||||
|
minute=scheduled_time.minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
|
||||||
|
# If scheduled time has passed today, set to tomorrow (for daily)
|
||||||
|
# or appropriate next occurrence for weekly/monthly
|
||||||
|
if next_run <= now:
|
||||||
|
if automation_config.frequency == 'daily':
|
||||||
|
next_run = next_run + timedelta(days=1)
|
||||||
|
elif automation_config.frequency == 'weekly':
|
||||||
|
# Next Monday
|
||||||
|
days_until_monday = (7 - now.weekday()) % 7
|
||||||
|
if days_until_monday == 0:
|
||||||
|
days_until_monday = 7
|
||||||
|
next_run = now + timedelta(days=days_until_monday)
|
||||||
|
next_run = next_run.replace(
|
||||||
|
hour=scheduled_time.hour,
|
||||||
|
minute=scheduled_time.minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
elif automation_config.frequency == 'monthly':
|
||||||
|
# Next 1st of month
|
||||||
|
if now.month == 12:
|
||||||
|
next_run = now.replace(year=now.year + 1, month=1, day=1)
|
||||||
|
else:
|
||||||
|
next_run = now.replace(month=now.month + 1, day=1)
|
||||||
|
next_run = next_run.replace(
|
||||||
|
hour=scheduled_time.hour,
|
||||||
|
minute=scheduled_time.minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
next_run = next_run + timedelta(days=1)
|
||||||
|
|
||||||
|
automation_config.next_run_at = next_run
|
||||||
|
logger.info(f"[UnifiedSettings] Schedule changed for site {site_id}, reset last_run_at=None, next_run_at={next_run}")
|
||||||
|
|
||||||
# Update stage configuration
|
# Update stage configuration
|
||||||
if 'stages' in data:
|
if 'stages' in data:
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ class AutomationConfigResource(resources.ModelResource):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = AutomationConfig
|
model = AutomationConfig
|
||||||
fields = ('id', 'site__domain', 'is_enabled', 'frequency', 'scheduled_time',
|
fields = ('id', 'site__domain', 'is_enabled', 'frequency', 'scheduled_time',
|
||||||
'within_stage_delay', 'between_stage_delay', 'last_run_at', 'created_at')
|
'last_run_at', 'next_run_at', 'created_at')
|
||||||
export_order = fields
|
export_order = fields
|
||||||
|
|
||||||
|
|
||||||
@admin.register(AutomationConfig)
|
@admin.register(AutomationConfig)
|
||||||
class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
||||||
resource_class = AutomationConfigResource
|
resource_class = AutomationConfigResource
|
||||||
list_display = ('site', 'is_enabled', 'frequency', 'scheduled_time', 'within_stage_delay', 'between_stage_delay', 'last_run_at')
|
list_display = ('site', 'is_enabled', 'frequency', 'scheduled_time', 'next_scheduled_run', 'last_run_at')
|
||||||
list_filter = ('is_enabled', 'frequency')
|
list_filter = ('is_enabled', 'frequency')
|
||||||
search_fields = ('site__domain',)
|
search_fields = ('site__domain',)
|
||||||
actions = [
|
actions = [
|
||||||
@@ -34,6 +34,142 @@ class AutomationConfigAdmin(ExportMixin, AccountAdminMixin, Igny8ModelAdmin):
|
|||||||
'bulk_update_delays',
|
'bulk_update_delays',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def next_scheduled_run(self, obj):
|
||||||
|
"""
|
||||||
|
Calculate the next scheduled run based on:
|
||||||
|
- Celery Beat schedule (every 15 minutes at :00, :15, :30, :45)
|
||||||
|
- Frequency (daily, weekly, monthly)
|
||||||
|
- Scheduled time
|
||||||
|
- 23-hour block after last_run_at
|
||||||
|
|
||||||
|
Celery checks window at :00 for :00-:14, at :15 for :15-:29, etc.
|
||||||
|
So scheduled_time 12:12 will be picked up at the 12:00 check.
|
||||||
|
"""
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
if not obj.is_enabled:
|
||||||
|
return 'Disabled'
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
scheduled_hour = obj.scheduled_time.hour
|
||||||
|
scheduled_minute = obj.scheduled_time.minute
|
||||||
|
|
||||||
|
# Calculate the Celery window start time for this scheduled_time
|
||||||
|
# If scheduled at :12, Celery checks at :00 (window :00-:14)
|
||||||
|
# If scheduled at :35, Celery checks at :30 (window :30-:44)
|
||||||
|
window_start_minute = (scheduled_minute // 15) * 15
|
||||||
|
|
||||||
|
# Calculate next occurrence based on frequency
|
||||||
|
def get_next_celery_pickup():
|
||||||
|
# Start with today at the Celery window start time
|
||||||
|
candidate = now.replace(
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
|
||||||
|
if obj.frequency == 'daily':
|
||||||
|
# If time has passed today (Celery already checked this window), next is tomorrow
|
||||||
|
if candidate <= now:
|
||||||
|
candidate += timedelta(days=1)
|
||||||
|
elif obj.frequency == 'weekly':
|
||||||
|
# Run on Mondays
|
||||||
|
days_until_monday = (7 - now.weekday()) % 7
|
||||||
|
if days_until_monday == 0:
|
||||||
|
# Today is Monday - check if time passed
|
||||||
|
candidate = now.replace(
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
if candidate <= now:
|
||||||
|
days_until_monday = 7
|
||||||
|
candidate += timedelta(days=7)
|
||||||
|
else:
|
||||||
|
candidate = now + timedelta(days=days_until_monday)
|
||||||
|
candidate = candidate.replace(
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
elif obj.frequency == 'monthly':
|
||||||
|
# Run on 1st of month
|
||||||
|
candidate = now.replace(
|
||||||
|
day=1,
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
if candidate <= now:
|
||||||
|
# Next month
|
||||||
|
if now.month == 12:
|
||||||
|
candidate = candidate.replace(year=now.year + 1, month=1)
|
||||||
|
else:
|
||||||
|
candidate = candidate.replace(month=now.month + 1)
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
next_celery_pickup = get_next_celery_pickup()
|
||||||
|
|
||||||
|
# Check 23-hour block
|
||||||
|
if obj.last_run_at:
|
||||||
|
earliest_eligible = obj.last_run_at + timedelta(hours=23)
|
||||||
|
if next_celery_pickup < earliest_eligible:
|
||||||
|
# Blocked - need to skip to next cycle
|
||||||
|
if obj.frequency == 'daily':
|
||||||
|
# Move to next day's window
|
||||||
|
next_celery_pickup = earliest_eligible.replace(
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
if next_celery_pickup < earliest_eligible:
|
||||||
|
next_celery_pickup += timedelta(days=1)
|
||||||
|
elif obj.frequency == 'weekly':
|
||||||
|
# Find next Monday after earliest_eligible
|
||||||
|
days_until_monday = (7 - earliest_eligible.weekday()) % 7
|
||||||
|
if days_until_monday == 0:
|
||||||
|
test_candidate = earliest_eligible.replace(
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
if test_candidate <= earliest_eligible:
|
||||||
|
days_until_monday = 7
|
||||||
|
next_celery_pickup = earliest_eligible + timedelta(days=days_until_monday)
|
||||||
|
next_celery_pickup = next_celery_pickup.replace(
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
elif obj.frequency == 'monthly':
|
||||||
|
# Find next 1st of month after earliest_eligible
|
||||||
|
next_celery_pickup = earliest_eligible.replace(
|
||||||
|
day=1,
|
||||||
|
hour=scheduled_hour,
|
||||||
|
minute=window_start_minute,
|
||||||
|
second=0,
|
||||||
|
microsecond=0
|
||||||
|
)
|
||||||
|
if next_celery_pickup < earliest_eligible:
|
||||||
|
if earliest_eligible.month == 12:
|
||||||
|
next_celery_pickup = next_celery_pickup.replace(year=earliest_eligible.year + 1, month=1)
|
||||||
|
else:
|
||||||
|
next_celery_pickup = next_celery_pickup.replace(month=earliest_eligible.month + 1)
|
||||||
|
|
||||||
|
# Format nicely
|
||||||
|
return next_celery_pickup.strftime('%b %d, %Y, %-I:%M %p')
|
||||||
|
|
||||||
|
next_scheduled_run.short_description = 'Next Scheduled Run'
|
||||||
|
|
||||||
def bulk_enable(self, request, queryset):
|
def bulk_enable(self, request, queryset):
|
||||||
"""Enable selected automation configs"""
|
"""Enable selected automation configs"""
|
||||||
updated = queryset.update(is_enabled=True)
|
updated = queryset.update(is_enabled=True)
|
||||||
|
|||||||
@@ -16,45 +16,63 @@ logger = get_task_logger(__name__)
|
|||||||
@shared_task(name='automation.check_scheduled_automations')
|
@shared_task(name='automation.check_scheduled_automations')
|
||||||
def check_scheduled_automations():
|
def check_scheduled_automations():
|
||||||
"""
|
"""
|
||||||
Check for scheduled automation runs (runs every hour)
|
Check for scheduled automation runs (runs every 15 minutes)
|
||||||
|
Matches automations scheduled within the current 15-minute window.
|
||||||
"""
|
"""
|
||||||
logger.info("[AutomationTask] Checking scheduled automations")
|
logger.info("[AutomationTask] Checking scheduled automations")
|
||||||
|
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
current_time = now.time()
|
current_time = now.time()
|
||||||
|
|
||||||
|
# Calculate 15-minute window boundaries
|
||||||
|
# Window starts at current quarter hour (0, 15, 30, 45)
|
||||||
|
window_start_minute = (current_time.minute // 15) * 15
|
||||||
|
window_end_minute = window_start_minute + 14
|
||||||
|
|
||||||
|
logger.info(f"[AutomationTask] Current time: {current_time}, checking window {current_time.hour}:{window_start_minute:02d}-{current_time.hour}:{window_end_minute:02d}")
|
||||||
|
|
||||||
# Find configs that should run now
|
# Find configs that should run now
|
||||||
for config in AutomationConfig.objects.filter(is_enabled=True):
|
for config in AutomationConfig.objects.filter(is_enabled=True):
|
||||||
# Check if it's time to run
|
# Check if it's time to run
|
||||||
should_run = False
|
should_run = False
|
||||||
|
scheduled_hour = config.scheduled_time.hour
|
||||||
|
scheduled_minute = config.scheduled_time.minute
|
||||||
|
|
||||||
|
# Check if scheduled time falls within current 15-minute window
|
||||||
|
def is_in_window():
|
||||||
|
if current_time.hour != scheduled_hour:
|
||||||
|
return False
|
||||||
|
return window_start_minute <= scheduled_minute <= window_end_minute
|
||||||
|
|
||||||
if config.frequency == 'daily':
|
if config.frequency == 'daily':
|
||||||
# Run if current time matches scheduled_time
|
# Run if scheduled_time falls within current 15-minute window
|
||||||
if current_time.hour == config.scheduled_time.hour and current_time.minute < 60:
|
if is_in_window():
|
||||||
should_run = True
|
should_run = True
|
||||||
elif config.frequency == 'weekly':
|
elif config.frequency == 'weekly':
|
||||||
# Run on Mondays at scheduled_time
|
# Run on Mondays within scheduled window
|
||||||
if now.weekday() == 0 and current_time.hour == config.scheduled_time.hour and current_time.minute < 60:
|
if now.weekday() == 0 and is_in_window():
|
||||||
should_run = True
|
should_run = True
|
||||||
elif config.frequency == 'monthly':
|
elif config.frequency == 'monthly':
|
||||||
# Run on 1st of month at scheduled_time
|
# Run on 1st of month within scheduled window
|
||||||
if now.day == 1 and current_time.hour == config.scheduled_time.hour and current_time.minute < 60:
|
if now.day == 1 and is_in_window():
|
||||||
should_run = True
|
should_run = True
|
||||||
|
|
||||||
|
logger.debug(f"[AutomationTask] Site {config.site_id}: freq={config.frequency}, scheduled={config.scheduled_time}, should_run={should_run}")
|
||||||
|
|
||||||
if should_run:
|
if should_run:
|
||||||
# Check if already ran today
|
# Check if already ran within the last 23 hours (prevents duplicate runs)
|
||||||
if config.last_run_at:
|
if config.last_run_at:
|
||||||
time_since_last_run = now - config.last_run_at
|
time_since_last_run = now - config.last_run_at
|
||||||
if time_since_last_run < timedelta(hours=23):
|
if time_since_last_run < timedelta(hours=23):
|
||||||
logger.info(f"[AutomationTask] Skipping site {config.site.id} - already ran today")
|
logger.info(f"[AutomationTask] Skipping site {config.site_id} - already ran {time_since_last_run} ago")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if already running OR paused (don't start new if existing in progress)
|
# Check if already running OR paused (don't start new if existing in progress)
|
||||||
if AutomationRun.objects.filter(site=config.site, status__in=['running', 'paused']).exists():
|
if AutomationRun.objects.filter(site=config.site, status__in=['running', 'paused']).exists():
|
||||||
logger.info(f"[AutomationTask] Skipping site {config.site.id} - automation in progress (running/paused)")
|
logger.info(f"[AutomationTask] Skipping site {config.site_id} - automation in progress (running/paused)")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
logger.info(f"[AutomationTask] Starting scheduled automation for site {config.site.id}")
|
logger.info(f"[AutomationTask] Starting scheduled automation for site {config.site_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
service = AutomationService(config.account, config.site)
|
service = AutomationService(config.account, config.site)
|
||||||
|
|||||||
@@ -115,13 +115,30 @@ class AutomationViewSet(viewsets.ViewSet):
|
|||||||
site=site
|
site=site
|
||||||
)
|
)
|
||||||
|
|
||||||
# Update fields
|
# Update fields - track if schedule changed
|
||||||
|
schedule_changed = False
|
||||||
|
|
||||||
if 'is_enabled' in request.data:
|
if 'is_enabled' in request.data:
|
||||||
|
if config.is_enabled != request.data['is_enabled']:
|
||||||
|
schedule_changed = True
|
||||||
config.is_enabled = request.data['is_enabled']
|
config.is_enabled = request.data['is_enabled']
|
||||||
if 'frequency' in request.data:
|
if 'frequency' in request.data:
|
||||||
|
if config.frequency != request.data['frequency']:
|
||||||
|
schedule_changed = True
|
||||||
config.frequency = request.data['frequency']
|
config.frequency = request.data['frequency']
|
||||||
if 'scheduled_time' in request.data:
|
if 'scheduled_time' in request.data:
|
||||||
config.scheduled_time = request.data['scheduled_time']
|
new_time = request.data['scheduled_time']
|
||||||
|
if str(config.scheduled_time) != str(new_time):
|
||||||
|
schedule_changed = True
|
||||||
|
config.scheduled_time = new_time
|
||||||
|
|
||||||
|
# Reset last_run_at and recalculate next_run_at if any schedule setting changed
|
||||||
|
if schedule_changed:
|
||||||
|
config.last_run_at = None
|
||||||
|
# Recalculate next_run_at based on new schedule
|
||||||
|
from igny8_core.business.automation.tasks import _calculate_next_run
|
||||||
|
from django.utils import timezone
|
||||||
|
config.next_run_at = _calculate_next_run(config, timezone.now())
|
||||||
# Stage enabled toggles
|
# Stage enabled toggles
|
||||||
if 'stage_1_enabled' in request.data:
|
if 'stage_1_enabled' in request.data:
|
||||||
config.stage_1_enabled = request.data['stage_1_enabled']
|
config.stage_1_enabled = request.data['stage_1_enabled']
|
||||||
@@ -1921,3 +1938,35 @@ class AutomationViewSet(viewsets.ViewSet):
|
|||||||
},
|
},
|
||||||
'initial_snapshot': initial_snapshot
|
'initial_snapshot': initial_snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@extend_schema(tags=['Automation'])
|
||||||
|
@action(detail=False, methods=['get'], url_path='server_time')
|
||||||
|
def server_time(self, request):
|
||||||
|
"""
|
||||||
|
GET /api/v1/automation/server_time/
|
||||||
|
Get current server time (UTC) used for all automation scheduling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- server_time: Current UTC timestamp (ISO 8601 format)
|
||||||
|
- server_time_formatted: Human-readable UTC time
|
||||||
|
- timezone: Server timezone setting (always UTC)
|
||||||
|
- celery_timezone: Celery task timezone setting
|
||||||
|
- use_tz: Whether Django is timezone-aware
|
||||||
|
|
||||||
|
Note: All automation schedules (scheduled_time) are in UTC.
|
||||||
|
When user sets "02:00", the automation runs at 02:00 UTC.
|
||||||
|
"""
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
now = timezone.now()
|
||||||
|
|
||||||
|
return Response({
|
||||||
|
'server_time': now.isoformat(),
|
||||||
|
'server_time_formatted': now.strftime('%H:%M'),
|
||||||
|
'server_time_date': now.strftime('%Y-%m-%d'),
|
||||||
|
'server_time_time': now.strftime('%H:%M:%S'),
|
||||||
|
'timezone': settings.TIME_ZONE,
|
||||||
|
'celery_timezone': getattr(settings, 'CELERY_TIMEZONE', settings.TIME_ZONE),
|
||||||
|
'use_tz': settings.USE_TZ,
|
||||||
|
'note': 'All automation schedules are in UTC. When you set "02:00", the automation runs at 02:00 UTC.'
|
||||||
|
})
|
||||||
@@ -57,7 +57,7 @@ app.conf.beat_schedule = {
|
|||||||
# Automation Tasks
|
# Automation Tasks
|
||||||
'check-scheduled-automations': {
|
'check-scheduled-automations': {
|
||||||
'task': 'automation.check_scheduled_automations',
|
'task': 'automation.check_scheduled_automations',
|
||||||
'schedule': crontab(minute=0), # Every hour at :00
|
'schedule': crontab(minute='0,15,30,45'), # Every 15 minutes
|
||||||
},
|
},
|
||||||
# Publishing Scheduler Tasks
|
# Publishing Scheduler Tasks
|
||||||
'schedule-approved-content': {
|
'schedule-approved-content': {
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ import {
|
|||||||
CheckCircleIcon,
|
CheckCircleIcon,
|
||||||
ClockIcon,
|
ClockIcon,
|
||||||
PaperPlaneIcon,
|
PaperPlaneIcon,
|
||||||
ArrowRightIcon
|
ArrowRightIcon,
|
||||||
|
TimeIcon
|
||||||
} from '../../icons';
|
} from '../../icons';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -77,9 +78,31 @@ const AutomationPage: React.FC = () => {
|
|||||||
const [globalProgress, setGlobalProgress] = useState<GlobalProgress | null>(null);
|
const [globalProgress, setGlobalProgress] = useState<GlobalProgress | null>(null);
|
||||||
const [stageProgress, setStageProgress] = useState<StageProgress[]>([]);
|
const [stageProgress, setStageProgress] = useState<StageProgress[]>([]);
|
||||||
const [initialSnapshot, setInitialSnapshot] = useState<InitialSnapshot | null>(null);
|
const [initialSnapshot, setInitialSnapshot] = useState<InitialSnapshot | null>(null);
|
||||||
|
|
||||||
|
// Server time state - shows the actual time used for all operations
|
||||||
|
const [serverTime, setServerTime] = useState<string | null>(null);
|
||||||
|
const [serverTimezone, setServerTimezone] = useState<string>('UTC');
|
||||||
|
|
||||||
// Track site ID to avoid duplicate calls when activeSite object reference changes
|
// Track site ID to avoid duplicate calls when activeSite object reference changes
|
||||||
const siteId = activeSite?.id;
|
const siteId = activeSite?.id;
|
||||||
|
|
||||||
|
// Fetch and update server time every second
|
||||||
|
useEffect(() => {
|
||||||
|
const loadServerTime = async () => {
|
||||||
|
try {
|
||||||
|
const data = await automationService.getServerTime();
|
||||||
|
setServerTime(data.server_time_formatted);
|
||||||
|
setServerTimezone(data.timezone);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load server time:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadServerTime();
|
||||||
|
const interval = setInterval(loadServerTime, 1000); // Update every second
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate time remaining until next scheduled run
|
* Calculate time remaining until next scheduled run
|
||||||
@@ -588,12 +611,9 @@ const AutomationPage: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="h-4 w-px bg-white/25"></div>
|
<div className="h-4 w-px bg-white/25"></div>
|
||||||
<div className="text-sm text-white/90">
|
<div className="text-sm text-white inline-flex items-center gap-1">
|
||||||
<span className="font-medium">Est:</span>{' '}
|
<TimeIcon className="size-3.5" />
|
||||||
<span className="font-semibold text-white">{estimate?.estimated_credits || 0} content pieces</span>
|
<span className="font-semibold tabular-nums">{serverTime ? serverTime.substring(0, 5) : '--:--'}</span>
|
||||||
{estimate && !estimate.sufficient && (
|
|
||||||
<span className="ml-1 text-white/90 font-semibold">(Limit reached)</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -161,11 +161,8 @@ export default function AIAutomationSettings({ siteId }: AIAutomationSettingsPro
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('[AIAutomationSettings] Saving payload:', JSON.stringify(payload, null, 2));
|
|
||||||
|
|
||||||
// Save unified settings
|
// Save unified settings
|
||||||
const updated = await updateUnifiedSiteSettings(siteId, payload);
|
const updated = await updateUnifiedSiteSettings(siteId, payload);
|
||||||
console.log('[AIAutomationSettings] Received updated settings:', JSON.stringify(updated.stages, null, 2));
|
|
||||||
setSettings(updated);
|
setSettings(updated);
|
||||||
|
|
||||||
// Save image settings
|
// Save image settings
|
||||||
@@ -816,6 +813,38 @@ export default function AIAutomationSettings({ siteId }: AIAutomationSettingsPro
|
|||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Scheduler Info Card - Full Width */}
|
||||||
|
<Card className="p-4 bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-start gap-4">
|
||||||
|
<div className="p-2 bg-gray-200 dark:bg-gray-700 rounded-lg flex-shrink-0">
|
||||||
|
<ClockIcon className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 mb-3">
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">System</span>
|
||||||
|
<p className="text-sm font-semibold text-gray-900 dark:text-white">Background Task Queue</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Check Frequency</span>
|
||||||
|
<p className="text-sm font-semibold text-gray-900 dark:text-white">Every 15 minutes</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Check Times</span>
|
||||||
|
<p className="text-sm font-semibold text-gray-900 dark:text-white">:00, :15, :30, :45</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Timezone</span>
|
||||||
|
<p className="text-sm font-semibold text-gray-900 dark:text-white">UTC</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
The scheduler checks for due automations every 15 minutes. Your scheduled time will trigger within its 15-minute window (e.g., 14:35 triggers at the 14:30 check). Automations only run once per day — if already run, the next run is tomorrow. All times are UTC.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,4 +419,20 @@ export const automationService = {
|
|||||||
}> => {
|
}> => {
|
||||||
return fetchAPI(buildUrl('/production_stats/', { site_id: siteId }));
|
return fetchAPI(buildUrl('/production_stats/', { site_id: siteId }));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get server time (UTC) used for all automation scheduling
|
||||||
|
*/
|
||||||
|
getServerTime: async (): Promise<{
|
||||||
|
server_time: string;
|
||||||
|
server_time_formatted: string;
|
||||||
|
server_time_date: string;
|
||||||
|
server_time_time: string;
|
||||||
|
timezone: string;
|
||||||
|
celery_timezone: string;
|
||||||
|
use_tz: boolean;
|
||||||
|
note: string;
|
||||||
|
}> => {
|
||||||
|
return fetchAPI(buildUrl('/server_time/'));
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user