mirror of
https://github.com/narrrl/holy-flavor-rater.git
synced 2026-11-04 00:47:02 +01:00
fixed worker entrypoint and implemented live logs with manual job scheduling
This commit is contained in:
1 parent
ef93ea41ca
commit
31144e4704
5 files changed
+88
-27
No files matched your search
@@ -11,6 +11,7 @@ class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--output', type=str, help='Specific output path for the backup file')
|
||||
parser.add_argument('--retention', type=int, default=7, help='Number of days to keep old backups')
|
||||
parser.add_argument('--full', action='store_true', help='Create a full archive including media')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
db_conf = settings.DATABASES['default']
|
||||
@@ -22,29 +23,53 @@ class Command(BaseCommand):
|
||||
backup_dir = os.path.join(settings.BASE_DIR, 'backups')
|
||||
os.makedirs(backup_dir, exist_ok=True)
|
||||
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
|
||||
# 1. Database Backup
|
||||
if options['output']:
|
||||
backup_path = options['output']
|
||||
db_backup_path = options['output']
|
||||
else:
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
backup_path = os.path.join(backup_dir, f'db_backup_{timestamp}.sqlite3')
|
||||
db_backup_path = os.path.join(backup_dir, f'db_backup_{timestamp}.sqlite3')
|
||||
|
||||
self.stdout.write(f"Starting backup of {db_path}...")
|
||||
self.stdout.write(f"Cloning database {db_path}...")
|
||||
|
||||
try:
|
||||
# Use SQLite's online backup API for a consistent copy
|
||||
src = sqlite3.connect(db_path)
|
||||
dst = sqlite3.connect(backup_path)
|
||||
dst = sqlite3.connect(db_backup_path)
|
||||
with dst:
|
||||
src.backup(dst)
|
||||
dst.close()
|
||||
src.close()
|
||||
self.stdout.write(self.style.SUCCESS(f"Successfully backed up database to {backup_path}"))
|
||||
self.stdout.write(self.style.SUCCESS(f"DB backup created: {db_backup_path}"))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Database backup failed: {e}"))
|
||||
self.stdout.write(self.style.ERROR(f"DB backup failed: {e}"))
|
||||
return
|
||||
|
||||
# 2. Cleanup Old Backups
|
||||
# 2. Full Archive (Optional)
|
||||
if options['full']:
|
||||
import tarfile
|
||||
archive_name = f"full_backup_{timestamp}.tar.gz"
|
||||
archive_path = os.path.join(backup_dir, archive_name)
|
||||
self.stdout.write(f"Creating full archive at {archive_path}...")
|
||||
|
||||
try:
|
||||
with tarfile.open(archive_path, "w:gz") as tar:
|
||||
# Add database
|
||||
tar.add(db_backup_path, arcname="db.sqlite3")
|
||||
# Add media
|
||||
media_dir = str(settings.MEDIA_ROOT)
|
||||
if os.path.exists(media_dir):
|
||||
tar.add(media_dir, arcname="media")
|
||||
|
||||
# Remove the raw sqlite backup after bundling it into the archive
|
||||
if not options['output']:
|
||||
os.remove(db_backup_path)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS(f"Full archive created successfully."))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Archive creation failed: {e}"))
|
||||
|
||||
# 3. Cleanup Old Backups
|
||||
retention_days = options['retention']
|
||||
cutoff = datetime.now() - timedelta(days=retention_days)
|
||||
|
||||
|
||||
@@ -39,10 +39,18 @@ class Command(BaseCommand):
|
||||
self.stdout.write(f"Executing Job: {job.get_name_display()}...")
|
||||
job.status = 'running'
|
||||
job.last_run = timezone.now()
|
||||
job.last_output = "" # Reset output for new run
|
||||
job.save()
|
||||
|
||||
# Capture management command output
|
||||
out = StringIO()
|
||||
class LiveStream(StringIO):
|
||||
def write(self, s):
|
||||
super().write(s)
|
||||
# Periodically update the DB with current output
|
||||
job.last_output = self.getvalue()
|
||||
job.save(update_fields=['last_output'])
|
||||
|
||||
out = LiveStream()
|
||||
error = ""
|
||||
|
||||
try:
|
||||
@@ -51,7 +59,7 @@ class Command(BaseCommand):
|
||||
elif job.name == 'cleanup_duplicates':
|
||||
call_command('cleanup_duplicates', stdout=out)
|
||||
elif job.name == 'backup_db':
|
||||
call_command('backup_db', stdout=out)
|
||||
call_command('backup_db', '--full', stdout=out)
|
||||
elif job.name == 'seed_legacy':
|
||||
call_command('seed_legacy_flavors', stdout=out)
|
||||
|
||||
|
||||
+11
-5
@@ -912,13 +912,19 @@ class AdminViewSet(viewsets.ViewSet):
|
||||
try:
|
||||
job = Job.objects.get(pk=pk)
|
||||
interval = request.data.get('interval_hours')
|
||||
next_run = request.data.get('next_run')
|
||||
|
||||
if interval is not None:
|
||||
job.interval_hours = int(interval)
|
||||
# If we just enabled it, set next_run
|
||||
if job.interval_hours > 0 and not job.next_run:
|
||||
from django.utils import timezone
|
||||
job.next_run = timezone.now()
|
||||
job.save()
|
||||
|
||||
if next_run is not None:
|
||||
from django.utils.dateparse import parse_datetime
|
||||
job.next_run = parse_datetime(next_run)
|
||||
elif interval is not None and job.interval_hours > 0 and not job.next_run:
|
||||
from django.utils import timezone
|
||||
job.next_run = timezone.now()
|
||||
|
||||
job.save()
|
||||
return Response(JobSerializer(job).data)
|
||||
except Job.DoesNotExist:
|
||||
return Response(status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
@@ -37,6 +37,11 @@ python manage.py seed_legacy_flavors
|
||||
echo "Syncing banner configurations..."
|
||||
python manage.py seed_banners
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
echo "Executing custom command: $@"
|
||||
exec "$@"
|
||||
fi
|
||||
|
||||
# Start the server using Gunicorn
|
||||
echo "Starting Gunicorn..."
|
||||
exec gunicorn holy_backend.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 120
|
||||
@@ -100,8 +100,8 @@ const AdminDashboard: React.FC = () => {
|
||||
const [selectedBanner, setSelectedBanner] = useState<Banner | null>(null);
|
||||
const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
const fetchData = async (silent = false) => {
|
||||
if (!silent) setLoading(true);
|
||||
try {
|
||||
const [statsRes, usersRes, bannersRes, jobsRes, configRes] = await Promise.all([
|
||||
api.get('admin-custom/stats/'),
|
||||
@@ -123,9 +123,18 @@ const AdminDashboard: React.FC = () => {
|
||||
console.error('Data fetch error:', err);
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
if (!silent) setLoading(false);
|
||||
};
|
||||
|
||||
// Poll for job updates if any are running or pending
|
||||
useEffect(() => {
|
||||
const anyRunning = jobs.some(j => j.status === 'running' || j.status === 'pending');
|
||||
if (anyRunning && currentTab === 3) {
|
||||
const interval = setInterval(() => fetchData(true), 3000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [jobs, currentTab]);
|
||||
|
||||
const handleUpdateConfig = async (key: string, value: any) => {
|
||||
try {
|
||||
const res = await api.patch('admin-custom/config/', { [key]: value });
|
||||
@@ -138,17 +147,18 @@ const AdminDashboard: React.FC = () => {
|
||||
const handleTriggerJob = async (id: number) => {
|
||||
try {
|
||||
await api.post(`admin-custom/${id}/trigger_job/`);
|
||||
alert('Job queued successfully');
|
||||
fetchData();
|
||||
fetchData(true);
|
||||
} catch (err) {
|
||||
alert('Failed to trigger job');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateJobSchedule = async (id: number, interval: number) => {
|
||||
const handleUpdateJobSchedule = async (id: number, interval: number, nextRun?: string) => {
|
||||
try {
|
||||
await api.patch(`admin-custom/${id}/update_job_schedule/`, { interval_hours: interval });
|
||||
fetchData();
|
||||
const data: any = { interval_hours: interval };
|
||||
if (nextRun) data.next_run = nextRun;
|
||||
await api.patch(`admin-custom/${id}/update_job_schedule/`, data);
|
||||
fetchData(true);
|
||||
} catch (err) {
|
||||
alert('Failed to update schedule');
|
||||
}
|
||||
@@ -417,7 +427,7 @@ const AdminDashboard: React.FC = () => {
|
||||
<Button
|
||||
variant="outlined"
|
||||
sx={{ mt: 2 }}
|
||||
onClick={fetchData}
|
||||
onClick={() => fetchData()}
|
||||
>
|
||||
Retry Loading
|
||||
</Button>
|
||||
@@ -485,9 +495,10 @@ const AdminDashboard: React.FC = () => {
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 2 }}>
|
||||
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>{job.name_display}</Typography>
|
||||
<Chip
|
||||
label={job.status.toUpperCase()}
|
||||
label={job.status === 'pending' ? 'QUEUED' : job.status.toUpperCase()}
|
||||
size="small"
|
||||
color={job.status === 'completed' ? 'success' : job.status === 'running' ? 'info' : job.status === 'failed' ? 'error' : 'default'}
|
||||
variant={job.status === 'running' ? 'filled' : 'outlined'}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
@@ -498,7 +509,13 @@ const AdminDashboard: React.FC = () => {
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">Next Run</Typography>
|
||||
<Typography variant="body2">{job.next_run ? formatDate(job.next_run) : (job.interval_hours > 0 ? 'Pending' : 'Disabled')}</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography variant="body2">{job.next_run ? formatDate(job.next_run) : (job.interval_hours > 0 ? 'Queued...' : 'Disabled')}</Typography>
|
||||
<Button size="small" onClick={() => {
|
||||
const val = prompt('Set next run (YYYY-MM-DD HH:MM):', job.next_run ? job.next_run.substring(0, 16).replace('T', ' ') : '');
|
||||
if (val !== null) handleUpdateJobSchedule(job.id, job.interval_hours, val);
|
||||
}}>Edit</Button>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">Interval (Hours)</Typography>
|
||||
|
||||
Reference in new issue
Block a user