fixed worker

This commit is contained in:
nvrl committed 2026-04-24 17:01:49 +02:00
1 parent d1b8e08531
commit 33e4b0376e
5 files changed
+98 -32

No files matched your search

+59 -3
View File
@@ -1,3 +1,5 @@
import json
from django.conf import settings from django.conf import settings
from django.core.mail import send_mail from django.core.mail import send_mail
from rest_framework import permissions, status, viewsets from rest_framework import permissions, status, viewsets
@@ -13,6 +15,46 @@ from api.serializers import (
SystemConfigSerializer, SystemConfigSerializer,
) )
JOB_TASK_MAP = {
"sync_flavors": "api.sync_flavors",
"cleanup_duplicates": "api.cleanup_duplicates",
"backup_db": "api.backup_db",
"seed_legacy": "api.seed_legacy",
}
def _sync_periodic_task(job: Job) -> None:
"""Create/update a django_celery_beat PeriodicTask for this Job.
Called when the admin changes interval_hours. interval_hours == 0 disables
the schedule (PeriodicTask.enabled=False).
"""
from django_celery_beat.models import IntervalSchedule, PeriodicTask
task_name = JOB_TASK_MAP.get(job.name)
if not task_name:
return
periodic_name = f"job:{job.name}"
if job.interval_hours <= 0:
PeriodicTask.objects.filter(name=periodic_name).update(enabled=False)
return
schedule, _ = IntervalSchedule.objects.get_or_create(
every=job.interval_hours,
period=IntervalSchedule.HOURS,
)
PeriodicTask.objects.update_or_create(
name=periodic_name,
defaults={
"interval": schedule,
"task": task_name,
"enabled": True,
"kwargs": json.dumps({}),
},
)
class AdminViewSet(viewsets.ViewSet): class AdminViewSet(viewsets.ViewSet):
permission_classes = [permissions.IsAdminUser] permission_classes = [permissions.IsAdminUser]
@@ -63,14 +105,27 @@ class AdminViewSet(viewsets.ViewSet):
@action(detail=True, methods=["post"]) @action(detail=True, methods=["post"])
def trigger_job(self, request: Request, pk=None) -> Response: def trigger_job(self, request: Request, pk=None) -> Response:
from celery import current_app
try: try:
job = Job.objects.get(pk=pk) job = Job.objects.get(pk=pk)
job.status = "pending"
job.save()
return Response({"status": "Job queued"})
except Job.DoesNotExist: except Job.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
task_name = JOB_TASK_MAP.get(job.name)
if not task_name:
return Response(
{"error": f"No celery task registered for {job.name}"},
status=status.HTTP_400_BAD_REQUEST,
)
job.status = "pending"
job.error_message = ""
job.save(update_fields=["status", "error_message"])
current_app.send_task(task_name)
return Response({"status": "Job queued", "task": task_name})
@action(detail=True, methods=["patch"]) @action(detail=True, methods=["patch"])
def update_job_schedule(self, request: Request, pk=None) -> Response: def update_job_schedule(self, request: Request, pk=None) -> Response:
try: try:
@@ -99,6 +154,7 @@ class AdminViewSet(viewsets.ViewSet):
job.next_run = timezone.now() job.next_run = timezone.now()
job.save() job.save()
_sync_periodic_task(job)
return Response(JobSerializer(job).data) return Response(JobSerializer(job).data)
except Job.DoesNotExist: except Job.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND) return Response(status=status.HTTP_404_NOT_FOUND)
+29 -23
View File
@@ -1,47 +1,53 @@
#!/bin/sh #!/bin/sh
# Exit on error
set -e set -e
# Apply database migrations # Worker/beat share the backend image but must not re-run migrate/seed on every
# restart. Set SKIP_INIT=true on those services. Only the primary backend
# container runs the init block below.
if [ "${SKIP_INIT:-false}" = "true" ]; then
echo "SKIP_INIT=true — skipping migrations/seed."
if [ "$#" -gt 0 ]; then
echo "Executing: $@"
exec "$@"
fi
echo "No command given with SKIP_INIT=true; exiting."
exit 0
fi
echo "Applying database migrations..." echo "Applying database migrations..."
python manage.py makemigrations python manage.py migrate --noinput
python manage.py migrate
# Create cache table for ratelimiting
echo "Creating cache table..." echo "Creating cache table..."
python manage.py createcachetable python manage.py createcachetable
# Collect static files
echo "Collecting static files..." echo "Collecting static files..."
python manage.py collectstatic --no-input python manage.py collectstatic --no-input
# Ensure media and cache directories exist
mkdir -p media/flavors mkdir -p media/flavors
mkdir -p django_cache mkdir -p django_cache
chmod -R 777 django_cache chmod -R 777 django_cache
# Sync flavors from the API # One-shot seed jobs. These should run on image first boot, not on every
echo "Syncing flavors..." # restart. A marker file on the bind-mounted volume gates them.
python manage.py sync_flavors SEED_MARKER="/app/.seeded"
if [ ! -f "$SEED_MARKER" ]; then
# Merge any duplicates that might have been created echo "First boot — running initial seed commands..."
echo "Cleaning up duplicates..." python manage.py sync_flavors || echo "sync_flavors failed (non-fatal on first boot)"
python manage.py cleanup_duplicates python manage.py cleanup_duplicates || echo "cleanup_duplicates failed (non-fatal)"
python manage.py seed_legacy_flavors || echo "seed_legacy_flavors failed (non-fatal)"
# Seed legacy flavors if needed python manage.py seed_banners || echo "seed_banners failed (non-fatal)"
echo "Seeding legacy flavors..." touch "$SEED_MARKER"
python manage.py seed_legacy_flavors else
echo "Seed marker present — skipping seed commands. Trigger them from /admin if needed."
# Auto-sync banner models and settings from JSON files # Banners are cheap + config-driven; always re-sync them.
echo "Syncing banner configurations..." python manage.py seed_banners || echo "seed_banners failed (non-fatal)"
python manage.py seed_banners fi
if [ "$#" -gt 0 ]; then if [ "$#" -gt 0 ]; then
echo "Executing custom command: $@" echo "Executing custom command: $@"
exec "$@" exec "$@"
fi fi
# Start the server using Gunicorn
echo "Starting Gunicorn..." echo "Starting Gunicorn..."
exec gunicorn holy_backend.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 120 exec gunicorn holy_backend.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 120
+2
View File
@@ -61,6 +61,7 @@ services:
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-holy_backend.settings.prod} - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-holy_backend.settings.prod}
- CELERY_BROKER_URL=${CELERY_BROKER_URL:-redis://redis:6379/0} - CELERY_BROKER_URL=${CELERY_BROKER_URL:-redis://redis:6379/0}
- CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND:-redis://redis:6379/0} - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND:-redis://redis:6379/0}
- SKIP_INIT=true
volumes: volumes:
- ./backend:/app - ./backend:/app
- ./legacy:/app/legacy - ./legacy:/app/legacy
@@ -86,6 +87,7 @@ services:
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-holy_backend.settings.prod} - DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-holy_backend.settings.prod}
- CELERY_BROKER_URL=${CELERY_BROKER_URL:-redis://redis:6379/0} - CELERY_BROKER_URL=${CELERY_BROKER_URL:-redis://redis:6379/0}
- CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND:-redis://redis:6379/0} - CELERY_RESULT_BACKEND=${CELERY_RESULT_BACKEND:-redis://redis:6379/0}
- SKIP_INIT=true
volumes: volumes:
- ./backend:/app - ./backend:/app
- ./legacy:/app/legacy - ./legacy:/app/legacy
+1 -1
View File
@@ -57,7 +57,7 @@ export const FlavorCard = ({
<Box <Box
sx={{ sx={{
width: '100%', width: '100%',
aspectRatio: compact ? '16/10' : '1/1', aspectRatio: '1 / 1',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
+7 -5
View File
@@ -684,13 +684,15 @@ const MainPage: React.FC<MainPageProps> = ({ adminMode }) => {
sx={{ sx={{
display: 'grid', display: 'grid',
gridTemplateColumns: { gridTemplateColumns: {
xs: 'repeat(2, 1fr)', xs: 'repeat(2, minmax(0, 1fr))',
sm: 'repeat(3, 1fr)', sm: 'repeat(3, minmax(0, 1fr))',
md: 'repeat(4, 1fr)', md: 'repeat(4, minmax(0, 1fr))',
lg: 'repeat(5, 1fr)', lg: 'repeat(5, minmax(0, 1fr))',
xl: 'repeat(6, 1fr)', xl: 'repeat(6, minmax(0, 1fr))',
}, },
gap: 2, gap: 2,
width: '100%',
minWidth: 0,
}} }}
> >
{newestFlavors.map((flavor) => ( {newestFlavors.map((flavor) => (