mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-03 16:41:27 +01:00
fixed worker
This commit is contained in:
1 parent
d1b8e08531
commit
33e4b0376e
5 files changed
+98
-32
No files matched your search
@@ -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
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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) => (
|
||||||
|
|||||||
Reference in new issue
Block a user