Docfile commited on
Commit
d324dde
·
verified ·
1 Parent(s): 73d203f

Upload 76 files

Browse files
Dockerfile CHANGED
@@ -1,22 +1,66 @@
1
- # Utiliser une image basée sur Debian 12 "Bookworm", où wkhtmltopdf est disponible
 
 
2
  FROM python:3.11-bookworm
3
 
4
- # Mettre à jour et installer wkhtmltopdf et ses dépendances
5
  RUN apt-get update && apt-get install -y \
6
  wkhtmltopdf \
7
  xvfb \
 
8
  && rm -rf /var/lib/apt/lists/*
9
 
 
10
  RUN useradd -m -u 1000 user
11
- USER user
12
- ENV PATH="/home/user/.local/bin:$PATH"
13
 
 
 
 
 
 
14
  WORKDIR /app
15
 
 
16
  COPY --chown=user ./requirements.txt requirements.txt
17
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
18
 
 
19
  COPY --chown=user . /app
20
 
21
- # Commande de démarrage Flask
22
- CMD ["flask", "run", "--host=0.0.0.0", "--port=7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Dockerfile pour Apex Ores avec Scheduler DB-Based
2
+ # Ce Dockerfile lance à la fois l'application Flask ET le scheduler en arrière-plan
3
+
4
  FROM python:3.11-bookworm
5
 
6
+ # Mettre à jour et installer les dépendances système
7
  RUN apt-get update && apt-get install -y \
8
  wkhtmltopdf \
9
  xvfb \
10
+ supervisor \
11
  && rm -rf /var/lib/apt/lists/*
12
 
13
+ # Créer l'utilisateur
14
  RUN useradd -m -u 1000 user
 
 
15
 
16
+ # Créer les répertoires pour supervisor et les logs
17
+ RUN mkdir -p /var/log/supervisor /var/log/apex \
18
+ && chown -R user:user /var/log/supervisor /var/log/apex
19
+
20
+ # Définir le répertoire de travail
21
  WORKDIR /app
22
 
23
+ # Copier et installer les dépendances Python
24
  COPY --chown=user ./requirements.txt requirements.txt
25
  RUN pip install --no-cache-dir --upgrade -r requirements.txt
26
 
27
+ # Copier tout le code
28
  COPY --chown=user . /app
29
 
30
+ # Créer la configuration supervisord
31
+ RUN echo '[supervisord]\n\
32
+ nodaemon=true\n\
33
+ user=root\n\
34
+ logfile=/var/log/supervisor/supervisord.log\n\
35
+ pidfile=/var/run/supervisord.pid\n\
36
+ \n\
37
+ [program:flask]\n\
38
+ command=flask run --host=0.0.0.0 --port=7860\n\
39
+ directory=/app\n\
40
+ user=user\n\
41
+ autostart=true\n\
42
+ autorestart=true\n\
43
+ stdout_logfile=/var/log/apex/flask.log\n\
44
+ stderr_logfile=/var/log/apex/flask.log\n\
45
+ environment=PYTHONPATH=/app\n\
46
+ \n\
47
+ [program:scheduler]\n\
48
+ command=python3 -m app.task_runner --daemon\n\
49
+ directory=/app\n\
50
+ user=user\n\
51
+ autostart=true\n\
52
+ autorestart=true\n\
53
+ stdout_logfile=/var/log/apex/scheduler.log\n\
54
+ stderr_logfile=/var/log/apex/scheduler.log\n\
55
+ environment=PYTHONPATH=/app' > /etc/supervisor/conf.d/apex.conf
56
+
57
+ # Variables d'environnement
58
+ ENV PYTHONPATH=/app
59
+ ENV FLASK_APP=run.py
60
+ ENV PATH="/home/user/.local/bin:$PATH"
61
+
62
+ # Exposer le port Flask
63
+ EXPOSE 7860
64
+
65
+ # Commande de démarrage : lance supervisord qui gère les deux processus
66
+ CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/apex.conf"]
apex-scheduler.service ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [Unit]
2
+ Description=Apex Ores DB Task Scheduler
3
+ After=network.target
4
+
5
+ [Service]
6
+ Type=simple
7
+ User=gojo
8
+ WorkingDirectory=/home/gojo/Bureau/jhjv
9
+ Environment=PYTHONPATH=/home/gojo/Bureau/jhjv
10
+ ExecStart=/usr/bin/python3 -m app.task_runner --daemon
11
+ Restart=always
12
+ RestartSec=10
13
+ StandardOutput=append:/var/log/apex-scheduler.log
14
+ StandardError=append:/var/log/apex-scheduler.log
15
+
16
+ [Install]
17
+ WantedBy=multi-user.target
app/__init__.py CHANGED
@@ -20,28 +20,28 @@ def create_app(config_name="default"):
20
  login_manager.login_view = "auth.login"
21
  login_manager.login_message = "Veuillez vous connecter pour accéder à cette page."
22
 
23
- from app.routes import auth, main, admin_routes, payments
24
 
25
  app.register_blueprint(auth.bp)
26
  app.register_blueprint(main.bp)
27
  app.register_blueprint(admin_routes.bp)
28
  app.register_blueprint(payments.bp)
 
29
 
30
  # Global context processor for app settings
31
  @app.context_processor
32
  def inject_app_settings():
33
- """Inject app name and logo into all templates"""
34
  from app.models import AppSettings
 
35
  try:
36
  return {
37
- 'app_name': AppSettings.get_app_name(),
38
- 'app_logo': AppSettings.get_app_logo(),
39
  }
40
  except Exception:
41
- # Return defaults if database is not yet initialized
42
  return {
43
- 'app_name': 'Apex Ores',
44
- 'app_logo': None,
45
  }
46
 
47
  return app
 
20
  login_manager.login_view = "auth.login"
21
  login_manager.login_message = "Veuillez vous connecter pour accéder à cette page."
22
 
23
+ from app.routes import auth, main, admin_routes, payments, admin_tasks
24
 
25
  app.register_blueprint(auth.bp)
26
  app.register_blueprint(main.bp)
27
  app.register_blueprint(admin_routes.bp)
28
  app.register_blueprint(payments.bp)
29
+ app.register_blueprint(admin_tasks.bp)
30
 
31
  # Global context processor for app settings
32
  @app.context_processor
33
  def inject_app_settings():
 
34
  from app.models import AppSettings
35
+
36
  try:
37
  return {
38
+ "app_name": AppSettings.get_app_name(),
39
+ "app_logo": AppSettings.get_app_logo(),
40
  }
41
  except Exception:
 
42
  return {
43
+ "app_name": "Apex Ores",
44
+ "app_logo": None,
45
  }
46
 
47
  return app
app/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/app/__pycache__/__init__.cpython-314.pyc and b/app/__pycache__/__init__.cpython-314.pyc differ
 
app/__pycache__/task_runner.cpython-314.pyc ADDED
Binary file (14.8 kB). View file
 
app/models/__init__.py CHANGED
@@ -471,3 +471,138 @@ class Notification(db.Model):
471
 
472
  is_read = db.Column(db.Boolean, default=False)
473
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
471
 
472
  is_read = db.Column(db.Boolean, default=False)
473
  created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
474
+
475
+
476
+ class ScheduledTask(db.Model):
477
+ """Model for database-based scheduled tasks - replaces cron jobs"""
478
+
479
+ __tablename__ = "scheduled_tasks"
480
+
481
+ id = db.Column(db.Integer, primary_key=True)
482
+ name = db.Column(db.String(100), unique=True, nullable=False)
483
+ description = db.Column(db.String(255))
484
+
485
+ # Configuration
486
+ schedule_type = db.Column(db.String(20), default="daily") # daily, hourly, interval
487
+ schedule_time = db.Column(db.Time, nullable=True) # Heure exacte (ex: 00:05:00)
488
+ interval_minutes = db.Column(db.Integer, nullable=True) # Ou intervalle
489
+
490
+ # État
491
+ is_active = db.Column(db.Boolean, default=True)
492
+ last_run_at = db.Column(db.DateTime, nullable=True)
493
+ next_run_at = db.Column(db.DateTime, nullable=True)
494
+ last_status = db.Column(
495
+ db.String(20), default="pending"
496
+ ) # pending, running, success, failed
497
+ last_error = db.Column(db.Text, nullable=True)
498
+ run_count = db.Column(db.Integer, default=0)
499
+ fail_count = db.Column(db.Integer, default=0)
500
+
501
+ # Métadonnées
502
+ created_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
503
+ updated_at = db.Column(
504
+ db.DateTime,
505
+ default=lambda: datetime.now(timezone.utc),
506
+ onupdate=lambda: datetime.now(timezone.utc),
507
+ )
508
+
509
+ def __repr__(self):
510
+ return f"<ScheduledTask {self.name}>"
511
+
512
+ def should_run(self):
513
+ """Vérifie si la tâche doit s'exécuter maintenant"""
514
+ from datetime import timezone
515
+
516
+ if not self.is_active:
517
+ return False
518
+
519
+ if not self.next_run_at:
520
+ self.calculate_next_run()
521
+ return True
522
+
523
+ now = datetime.now(timezone.utc)
524
+ next_run = self.next_run_at
525
+
526
+ if next_run.tzinfo is None:
527
+ next_run = next_run.replace(tzinfo=timezone.utc)
528
+
529
+ return now >= next_run
530
+
531
+ def calculate_next_run(self):
532
+ """Calcule la prochaine exécution"""
533
+ from datetime import date, time, timedelta
534
+
535
+ now = datetime.now(timezone.utc)
536
+
537
+ if self.schedule_type == "daily" and self.schedule_time:
538
+ # Prochaine exécution à l'heure définie
539
+ next_run = datetime.combine(date.today(), self.schedule_time)
540
+ next_run = next_run.replace(tzinfo=timezone.utc)
541
+ if next_run <= now:
542
+ next_run += timedelta(days=1)
543
+ self.next_run_at = next_run
544
+
545
+ elif self.schedule_type == "hourly":
546
+ # Toutes les heures
547
+ self.next_run_at = now.replace(
548
+ minute=0, second=0, microsecond=0
549
+ ) + timedelta(hours=1)
550
+
551
+ elif self.schedule_type == "interval" and self.interval_minutes:
552
+ # Intervalle personnalisé
553
+ if self.last_run_at:
554
+ self.next_run_at = self.last_run_at + timedelta(
555
+ minutes=self.interval_minutes
556
+ )
557
+ else:
558
+ self.next_run_at = now
559
+
560
+ def mark_running(self):
561
+ """Marque la tâche comme en cours"""
562
+ self.last_status = "running"
563
+ db.session.commit()
564
+
565
+ def mark_success(self):
566
+ """Marque la tâche comme réussie"""
567
+ self.last_run_at = datetime.now(timezone.utc)
568
+ self.run_count += 1
569
+ self.last_status = "success"
570
+ self.last_error = None
571
+ self.calculate_next_run()
572
+ db.session.commit()
573
+
574
+ def mark_failed(self, error_message):
575
+ """Marque la tâche comme échouée"""
576
+ self.last_run_at = datetime.now(timezone.utc)
577
+ self.fail_count += 1
578
+ self.last_status = "failed"
579
+ self.last_error = str(error_message)[:500] # Limiter la taille
580
+ self.calculate_next_run()
581
+ db.session.commit()
582
+
583
+
584
+ class TaskExecutionLog(db.Model):
585
+ """Log d'exécution des tâches planifiées"""
586
+
587
+ __tablename__ = "task_execution_logs"
588
+
589
+ id = db.Column(db.Integer, primary_key=True)
590
+ task_id = db.Column(db.Integer, db.ForeignKey("scheduled_tasks.id"), nullable=False)
591
+ task_name = db.Column(db.String(100), nullable=False)
592
+
593
+ started_at = db.Column(db.DateTime, default=lambda: datetime.now(timezone.utc))
594
+ completed_at = db.Column(db.DateTime, nullable=True)
595
+ status = db.Column(db.String(20), default="running") # running, success, failed
596
+ error_message = db.Column(db.Text, nullable=True)
597
+ output_log = db.Column(db.Text, nullable=True)
598
+
599
+ # Pour debug
600
+ server_hostname = db.Column(db.String(100), nullable=True)
601
+ process_id = db.Column(db.Integer, nullable=True)
602
+
603
+ task = db.relationship(
604
+ "ScheduledTask", backref=db.backref("execution_logs", lazy="dynamic")
605
+ )
606
+
607
+ def __repr__(self):
608
+ return f"<TaskExecutionLog {self.task_name} {self.status}>"
app/models/__pycache__/__init__.cpython-314.pyc CHANGED
Binary files a/app/models/__pycache__/__init__.cpython-314.pyc and b/app/models/__pycache__/__init__.cpython-314.pyc differ
 
app/routes/__pycache__/admin_tasks.cpython-314.pyc ADDED
Binary file (6.45 kB). View file
 
app/routes/admin_tasks.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import wraps
2
+
3
+ from flask import (
4
+ Blueprint,
5
+ flash,
6
+ jsonify,
7
+ redirect,
8
+ render_template,
9
+ request,
10
+ url_for,
11
+ )
12
+ from flask_login import current_user, login_required
13
+
14
+ from app import db
15
+ from app.models import ScheduledTask, TaskExecutionLog
16
+
17
+ bp = Blueprint("admin_tasks", __name__, url_prefix="/admin/tasks")
18
+
19
+
20
+ def admin_required(f):
21
+ """Vérifie que l'utilisateur est administrateur"""
22
+
23
+ @wraps(f)
24
+ def decorated_function(*args, **kwargs):
25
+ if not current_user.is_authenticated or not getattr(
26
+ current_user, "is_admin", False
27
+ ):
28
+ flash("Accès réservé aux administrateurs.", "error")
29
+ return redirect(url_for("main.index"))
30
+ return f(*args, **kwargs)
31
+
32
+ return decorated_function
33
+
34
+
35
+ @bp.route("/")
36
+ @login_required
37
+ @admin_required
38
+ def list_tasks():
39
+ """Liste toutes les tâches planifiées"""
40
+ tasks = ScheduledTask.query.order_by(ScheduledTask.name).all()
41
+
42
+ stats = {
43
+ "total": len(tasks),
44
+ "active": sum(1 for t in tasks if t.is_active),
45
+ "failed": sum(1 for t in tasks if t.last_status == "failed"),
46
+ "pending": sum(1 for t in tasks if t.should_run()),
47
+ }
48
+
49
+ return render_template("admin/tasks.html", tasks=tasks, stats=stats)
50
+
51
+
52
+ @bp.route("/logs")
53
+ @login_required
54
+ @admin_required
55
+ def view_logs():
56
+ """Voir l'historique des exécutions"""
57
+ page = request.args.get("page", 1, type=int)
58
+ per_page = 50
59
+
60
+ logs = TaskExecutionLog.query.order_by(TaskExecutionLog.started_at.desc()).paginate(
61
+ page=page, per_page=per_page, error_out=False
62
+ )
63
+
64
+ return render_template("admin/task_logs.html", logs=logs)
65
+
66
+
67
+ @bp.route("/run/<int:task_id>", methods=["POST"])
68
+ @login_required
69
+ @admin_required
70
+ def run_task_now(task_id):
71
+ """Exécute une tâche manuellement"""
72
+ task = ScheduledTask.query.get_or_404(task_id)
73
+
74
+ try:
75
+ from app.task_runner import TaskRunner
76
+
77
+ runner = TaskRunner()
78
+ runner.execute_task(task)
79
+ flash(f"Tâche '{task.name}' exécutée avec succès!", "success")
80
+ except Exception as e:
81
+ flash(f"Erreur lors de l'exécution: {str(e)}", "error")
82
+
83
+ return redirect(url_for("admin_tasks.list_tasks"))
84
+
85
+
86
+ @bp.route("/toggle/<int:task_id>", methods=["POST"])
87
+ @login_required
88
+ @admin_required
89
+ def toggle_task(task_id):
90
+ """Active ou désactive une tâche"""
91
+ task = ScheduledTask.query.get_or_404(task_id)
92
+ task.is_active = not task.is_active
93
+ db.session.commit()
94
+
95
+ status = "activée" if task.is_active else "désactivée"
96
+ flash(f"Tâche '{task.name}' {status}.", "success")
97
+
98
+ return redirect(url_for("admin_tasks.list_tasks"))
99
+
100
+
101
+ @bp.route("/api/status")
102
+ @login_required
103
+ @admin_required
104
+ def api_status():
105
+ """API JSON pour le statut des tâches (utilisé par le dashboard)"""
106
+ tasks = ScheduledTask.query.all()
107
+
108
+ return jsonify(
109
+ {
110
+ "tasks": [
111
+ {
112
+ "id": t.id,
113
+ "name": t.name,
114
+ "status": t.last_status,
115
+ "is_active": t.is_active,
116
+ "last_run": t.last_run_at.isoformat() if t.last_run_at else None,
117
+ "next_run": t.next_run_at.isoformat() if t.next_run_at else None,
118
+ "should_run": t.should_run(),
119
+ }
120
+ for t in tasks
121
+ ]
122
+ }
123
+ )
app/task_runner.py ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Database-based Task Scheduler for Apex Ores
3
+
4
+ Remplace les cron jobs traditionnels par un système robuste basé sur la DB:
5
+ - Rattrapage automatique des tâches manquées après redémarrage
6
+ - Historique complet des exécutions
7
+ - Interface admin pour monitoring et contrôle
8
+ - Tolérance aux pannes serveur
9
+
10
+ Usage:
11
+ # Mode daemon (boucle infinie)
12
+ python -m app.task_runner --daemon
13
+
14
+ # Mode one-shot (pour cron ou test)
15
+ python -m app.task_runner
16
+
17
+ # Via Flask CLI
18
+ flask run-scheduler
19
+ """
20
+
21
+ import sys
22
+ import os
23
+ import socket
24
+ import traceback
25
+ from datetime import datetime, timedelta, timezone, date, time
26
+ from io import StringIO
27
+ from contextlib import redirect_stdout
28
+
29
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
30
+
31
+ from app import create_app, db
32
+ from app.models import ScheduledTask, TaskExecutionLog
33
+
34
+
35
+ class TaskRunner:
36
+ def __init__(self):
37
+ self.app = create_app()
38
+ self.hostname = socket.gethostname()
39
+ self.pid = os.getpid()
40
+
41
+ def run_pending_tasks(self):
42
+ """Exécute toutes les tâches qui doivent tourner"""
43
+ with self.app.app_context():
44
+ now = datetime.now(timezone.utc)
45
+
46
+ pending_tasks = ScheduledTask.query.filter(
47
+ ScheduledTask.is_active == True,
48
+ db.or_(
49
+ ScheduledTask.next_run_at == None, ScheduledTask.next_run_at <= now
50
+ ),
51
+ ).all()
52
+
53
+ print(f"[{now}] {len(pending_tasks)} tâche(s) à exécuter")
54
+
55
+ for task in pending_tasks:
56
+ self.execute_task(task)
57
+
58
+ def execute_task(self, task):
59
+ """Exécute une tâche spécifique avec logging complet"""
60
+ print(f"\n[{datetime.now(timezone.utc)}] Exécution de: {task.name}")
61
+
62
+ with self.app.app_context():
63
+ execution_log = TaskExecutionLog(
64
+ task_id=task.id,
65
+ task_name=task.name,
66
+ server_hostname=self.hostname,
67
+ process_id=self.pid,
68
+ )
69
+ db.session.add(execution_log)
70
+ task.mark_running()
71
+ execution_log.status = "running"
72
+ db.session.commit()
73
+
74
+ try:
75
+ output = self.run_task_function(task.name)
76
+
77
+ execution_log.status = "success"
78
+ execution_log.completed_at = datetime.now(timezone.utc)
79
+ execution_log.output_log = output
80
+ task.mark_success()
81
+
82
+ print(f" ✓ {task.name} terminé avec succès")
83
+
84
+ except Exception as e:
85
+ error_msg = f"{str(e)}\n{traceback.format_exc()}"
86
+
87
+ execution_log.status = "failed"
88
+ execution_log.completed_at = datetime.now(timezone.utc)
89
+ execution_log.error_message = error_msg[:2000]
90
+ task.mark_failed(str(e))
91
+
92
+ print(f" ✗ {task.name} a échoué: {str(e)}")
93
+
94
+ db.session.commit()
95
+
96
+ def run_task_function(self, task_name):
97
+ """Exécute la fonction associée au nom de la tâche"""
98
+ f = StringIO()
99
+
100
+ with redirect_stdout(f):
101
+ if task_name == "daily_gains":
102
+ self._run_daily_gains()
103
+ elif task_name == "hourly_cleanup":
104
+ self._run_hourly_cleanup()
105
+ elif task_name == "weekly_report":
106
+ self._run_weekly_report()
107
+ else:
108
+ raise ValueError(f"Tâche inconnue: {task_name}")
109
+
110
+ return f.getvalue()
111
+
112
+ def _run_daily_gains(self):
113
+ """Calcule les gains quotidiens et traite les commissions"""
114
+ from scripts.daily_gains import (
115
+ calculate_daily_gains,
116
+ auto_process_withdrawals,
117
+ cleanup_old_notifications,
118
+ print_platform_summary,
119
+ )
120
+
121
+ calculate_daily_gains()
122
+ auto_process_withdrawals()
123
+ cleanup_old_notifications()
124
+ print_platform_summary()
125
+
126
+ def _run_hourly_cleanup(self):
127
+ """Nettoyage périodique"""
128
+ print("Nettoyage horaire...")
129
+ from scripts.daily_gains import cleanup_old_notifications
130
+
131
+ cleanup_old_notifications()
132
+ print("Nettoyage terminé")
133
+
134
+ def _run_weekly_report(self):
135
+ """Rapport hebdomadaire"""
136
+ print("Génération du rapport hebdomadaire...")
137
+ print("TODO: Implémenter le rapport hebdomadaire")
138
+
139
+ def check_missed_tasks(self):
140
+ """Vérifie et exécute les tâches manquées depuis le dernier démarrage"""
141
+ with self.app.app_context():
142
+ now = datetime.now(timezone.utc)
143
+
144
+ missed_tasks = ScheduledTask.query.filter(
145
+ ScheduledTask.is_active == True,
146
+ db.or_(
147
+ ScheduledTask.last_run_at.is_(None),
148
+ ScheduledTask.last_run_at < now - timedelta(hours=25),
149
+ ),
150
+ ).all()
151
+
152
+ if missed_tasks:
153
+ print(f"\n⚠️ {len(missed_tasks)} tâche(s) manquée(s) détectée(s)!")
154
+ for task in missed_tasks:
155
+ last_run = (
156
+ task.last_run_at.strftime("%Y-%m-%d %H:%M")
157
+ if task.last_run_at
158
+ else "JAMAIS"
159
+ )
160
+ print(f" - {task.name} (dernier run: {last_run})")
161
+ self.execute_task(task)
162
+ else:
163
+ print(f"\n✓ Aucune tâche manquée")
164
+
165
+ def initialize_default_tasks(self):
166
+ """Crée les tâches par défaut si elles n'existent pas"""
167
+ with self.app.app_context():
168
+ default_tasks = [
169
+ {
170
+ "name": "daily_gains",
171
+ "description": "Calcule les gains quotidiens, traite les retraits et commissions",
172
+ "schedule_type": "daily",
173
+ "schedule_time": time(0, 5, 0),
174
+ "is_active": True,
175
+ },
176
+ {
177
+ "name": "hourly_cleanup",
178
+ "description": "Nettoyage des notifications et logs anciens",
179
+ "schedule_type": "hourly",
180
+ "is_active": True,
181
+ },
182
+ ]
183
+
184
+ for task_data in default_tasks:
185
+ existing = ScheduledTask.query.filter_by(name=task_data["name"]).first()
186
+ if not existing:
187
+ task = ScheduledTask(**task_data)
188
+ task.calculate_next_run()
189
+ db.session.add(task)
190
+ print(f"✓ Tâche créée: {task_data['name']}")
191
+
192
+ db.session.commit()
193
+
194
+ def print_status(self):
195
+ """Affiche le statut de toutes les tâches"""
196
+ with self.app.app_context():
197
+ tasks = ScheduledTask.query.all()
198
+
199
+ print("\n" + "=" * 70)
200
+ print("STATUT DES TÂCHES PLANIFIÉES")
201
+ print("=" * 70)
202
+
203
+ for task in tasks:
204
+ status_icon = (
205
+ "✓"
206
+ if task.last_status == "success"
207
+ else "✗"
208
+ if task.last_status == "failed"
209
+ else "○"
210
+ )
211
+ next_run = (
212
+ task.next_run_at.strftime("%d/%m %H:%M")
213
+ if task.next_run_at
214
+ else "Non calculé"
215
+ )
216
+ last_run = (
217
+ task.last_run_at.strftime("%d/%m %H:%M")
218
+ if task.last_run_at
219
+ else "Jamais"
220
+ )
221
+
222
+ print(
223
+ f"{status_icon} {task.name:20} | Prochain: {next_run:12} | Dernier: {last_run:12} | Runs: {task.run_count}"
224
+ )
225
+
226
+ print("=" * 70)
227
+
228
+
229
+ def run_scheduler_daemon():
230
+ """Mode daemon - boucle infinie qui vérifie chaque minute"""
231
+ runner = TaskRunner()
232
+
233
+ print("=" * 70)
234
+ print("SCHEDULER DB-BASED - MODE DAEMON")
235
+ print(f"Démarré à: {datetime.now(timezone.utc)}")
236
+ print(f"Hostname: {runner.hostname} | PID: {runner.pid}")
237
+ print("=" * 70)
238
+
239
+ runner.initialize_default_tasks()
240
+ runner.check_missed_tasks()
241
+ runner.print_status()
242
+
243
+ print("\nEn attente des tâches... (Ctrl+C pour arrêter)\n")
244
+
245
+ try:
246
+ while True:
247
+ runner.run_pending_tasks()
248
+ import time
249
+
250
+ time.sleep(60)
251
+ except KeyboardInterrupt:
252
+ print("\n\nArrêt du scheduler.")
253
+ print(f"Terminé à: {datetime.now(timezone.utc)}")
254
+
255
+
256
+ def run_scheduler_once():
257
+ """Mode one-shot - une seule exécution puis sortie"""
258
+ runner = TaskRunner()
259
+
260
+ print("=" * 70)
261
+ print("SCHEDULER DB-BASED - MODE ONE-SHOT")
262
+ print(f"Exécution à: {datetime.now(timezone.utc)}")
263
+ print("=" * 70)
264
+
265
+ runner.initialize_default_tasks()
266
+ runner.check_missed_tasks()
267
+ runner.run_pending_tasks()
268
+ runner.print_status()
269
+
270
+ print("\nExécution terminée.")
271
+
272
+
273
+ def check_and_run_missed_on_startup():
274
+ """Vérifie les tâches manquées au démarrage de l'app (non bloquant)"""
275
+ try:
276
+ runner = TaskRunner()
277
+ runner.initialize_default_tasks()
278
+ runner.check_missed_tasks()
279
+ except Exception as e:
280
+ print(f"[Scheduler] Erreur lors de la vérification des tâches: {e}")
281
+
282
+
283
+ if __name__ == "__main__":
284
+ if len(sys.argv) > 1 and sys.argv[1] == "--daemon":
285
+ run_scheduler_daemon()
286
+ else:
287
+ run_scheduler_once()
app/templates/admin/base.html CHANGED
@@ -134,6 +134,14 @@
134
  <i class="fas fa-sliders-h w-5 mr-3"></i>
135
  Paramètres Généraux
136
  </a>
 
 
 
 
 
 
 
 
137
  </div>
138
  </nav>
139
 
 
134
  <i class="fas fa-sliders-h w-5 mr-3"></i>
135
  Paramètres Généraux
136
  </a>
137
+
138
+ <a
139
+ href="{{ url_for('admin_tasks.list_tasks') }}"
140
+ class="sidebar-link flex items-center px-4 py-3 rounded-lg border-l-4 border-transparent text-gray-300 hover:text-yellow-400 {% if 'tasks' in request.endpoint %}active{% endif %}"
141
+ >
142
+ <i class="fas fa-clock w-5 mr-3"></i>
143
+ Tâches Planifiées
144
+ </a>
145
  </div>
146
  </nav>
147
 
app/templates/admin/task_logs.html ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin/base.html" %}
2
+
3
+ {% block title %}Logs des Tâches{% endblock %}
4
+ {% block page_title %}Logs des Tâches{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="space-y-6">
8
+ <!-- Header avec navigation -->
9
+ <div class="flex justify-between items-center">
10
+ <h3 class="text-lg font-semibold text-white">Historique des Exécutions</h3>
11
+ <a href="{{ url_for('admin_tasks.list_tasks') }}" class="px-4 py-2 bg-gray-700 hover:bg-gray-600 text-white rounded-lg text-sm">
12
+ <i class="fas fa-arrow-left mr-2"></i>Retour aux Tâches
13
+ </a>
14
+ </div>
15
+
16
+ <!-- Logs -->
17
+ <div class="bg-gray-800 rounded-lg border border-gray-700">
18
+ <div class="overflow-x-auto">
19
+ <table class="w-full text-left">
20
+ <thead class="bg-gray-700/50">
21
+ <tr>
22
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Date</th>
23
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Tâche</th>
24
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Statut</th>
25
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Durée</th>
26
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Serveur</th>
27
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Détails</th>
28
+ </tr>
29
+ </thead>
30
+ <tbody class="divide-y divide-gray-700">
31
+ {% for log in logs.items %}
32
+ <tr class="hover:bg-gray-700/30">
33
+ <td class="px-6 py-4 text-sm text-gray-300">
34
+ {{ log.started_at.strftime('%d/%m/%Y %H:%M:%S') }}
35
+ </td>
36
+ <td class="px-6 py-4 text-sm font-medium text-white">
37
+ {{ log.task_name }}
38
+ </td>
39
+ <td class="px-6 py-4">
40
+ {% if log.status == 'success' %}
41
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400">
42
+ <i class="fas fa-check mr-1"></i> Succès
43
+ </span>
44
+ {% elif log.status == 'failed' %}
45
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-500/20 text-red-400">
46
+ <i class="fas fa-times mr-1"></i> Échec
47
+ </span>
48
+ {% else %}
49
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-500/20 text-blue-400">
50
+ <i class="fas fa-spinner fa-spin mr-1"></i> En cours
51
+ </span>
52
+ {% endif %}
53
+ </td>
54
+ <td class="px-6 py-4 text-sm text-gray-300">
55
+ {% if log.completed_at %}
56
+ {% set duration = (log.completed_at - log.started_at).total_seconds() // 60 %}
57
+ {% if duration > 0 %}
58
+ {{ duration|int }} min
59
+ {% else %}
60
+ {{ ((log.completed_at - log.started_at).total_seconds())|int }} s
61
+ {% endif %}
62
+ {% else %}
63
+ -
64
+ {% endif %}
65
+ </td>
66
+ <td class="px-6 py-4 text-sm text-gray-400">
67
+ {{ log.server_hostname or 'N/A' }}
68
+ <br><span class="text-xs">PID: {{ log.process_id or 'N/A' }}</span>
69
+ </td>
70
+ <td class="px-6 py-4 text-sm text-gray-300">
71
+ {% if log.error_message %}
72
+ <div class="max-w-xs">
73
+ <p class="text-red-400 text-xs truncate">{{ log.error_message[:100] }}</p>
74
+ </div>
75
+ {% elif log.output_log %}
76
+ <div class="max-w-xs">
77
+ <p class="text-xs text-gray-400 truncate">{{ log.output_log[:100] }}</p>
78
+ </div>
79
+ {% else %}
80
+ <span class="text-gray-500">-</span>
81
+ {% endif %}
82
+ </td>
83
+ </tr>
84
+ {% if log.error_message or log.output_log %}
85
+ <tr class="bg-gray-700/20">
86
+ <td colspan="6" class="px-6 py-3">
87
+ {% if log.error_message %}
88
+ <div class="mb-2">
89
+ <p class="text-xs font-semibold text-red-400 mb-1">Erreur:</p>
90
+ <pre class="text-xs text-red-300 bg-red-900/20 p-2 rounded overflow-x-auto">{{ log.error_message }}</pre>
91
+ </div>
92
+ {% endif %}
93
+ {% if log.output_log %}
94
+ <div>
95
+ <p class="text-xs font-semibold text-gray-400 mb-1">Sortie:</p>
96
+ <pre class="text-xs text-gray-300 bg-gray-700/50 p-2 rounded overflow-x-auto max-h-40 overflow-y-auto">{{ log.output_log }}</pre>
97
+ </div>
98
+ {% endif %}
99
+ </td>
100
+ </tr>
101
+ {% endif %}
102
+ {% endfor %}
103
+ </tbody>
104
+ </table>
105
+ </div>
106
+
107
+ <!-- Pagination -->
108
+ {% if logs.pages > 1 %}
109
+ <div class="px-6 py-4 border-t border-gray-700 flex justify-between items-center">
110
+ <p class="text-sm text-gray-400">
111
+ Page {{ logs.page }} sur {{ logs.pages }}
112
+ </p>
113
+ <div class="flex space-x-2">
114
+ {% if logs.has_prev %}
115
+ <a href="{{ url_for('admin_tasks.view_logs', page=logs.prev_num) }}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded text-sm">
116
+ <i class="fas fa-chevron-left"></i> Précédent
117
+ </a>
118
+ {% endif %}
119
+ {% if logs.has_next %}
120
+ <a href="{{ url_for('admin_tasks.view_logs', page=logs.next_num) }}" class="px-3 py-1 bg-gray-700 hover:bg-gray-600 text-white rounded text-sm">
121
+ Suivant <i class="fas fa-chevron-right"></i>
122
+ </a>
123
+ {% endif %}
124
+ </div>
125
+ </div>
126
+ {% endif %}
127
+ </div>
128
+ </div>
129
+ {% endblock %}
app/templates/admin/tasks.html ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {% extends "admin/base.html" %}
2
+
3
+ {% block title %}Tâches Planifiées{% endblock %}
4
+ {% block page_title %}Tâches Planifiées{% endblock %}
5
+
6
+ {% block content %}
7
+ <div class="space-y-6">
8
+ <!-- Statistiques -->
9
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4">
10
+ <div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
11
+ <div class="flex items-center">
12
+ <div class="p-3 rounded-full bg-blue-500/20 text-blue-400">
13
+ <i class="fas fa-tasks text-xl"></i>
14
+ </div>
15
+ <div class="ml-4">
16
+ <p class="text-sm text-gray-400">Total Tâches</p>
17
+ <p class="text-2xl font-bold text-white">{{ stats.total }}</p>
18
+ </div>
19
+ </div>
20
+ </div>
21
+
22
+ <div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
23
+ <div class="flex items-center">
24
+ <div class="p-3 rounded-full bg-green-500/20 text-green-400">
25
+ <i class="fas fa-play text-xl"></i>
26
+ </div>
27
+ <div class="ml-4">
28
+ <p class="text-sm text-gray-400">Actives</p>
29
+ <p class="text-2xl font-bold text-white">{{ stats.active }}</p>
30
+ </div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
35
+ <div class="flex items-center">
36
+ <div class="p-3 rounded-full bg-yellow-500/20 text-yellow-400">
37
+ <i class="fas fa-clock text-xl"></i>
38
+ </div>
39
+ <div class="ml-4">
40
+ <p class="text-sm text-gray-400">En Attente</p>
41
+ <p class="text-2xl font-bold text-white">{{ stats.pending }}</p>
42
+ </div>
43
+ </div>
44
+ </div>
45
+
46
+ <div class="bg-gray-800 rounded-lg p-4 border border-gray-700">
47
+ <div class="flex items-center">
48
+ <div class="p-3 rounded-full bg-red-500/20 text-red-400">
49
+ <i class="fas fa-exclamation-triangle text-xl"></i>
50
+ </div>
51
+ <div class="ml-4">
52
+ <p class="text-sm text-gray-400">Échecs</p>
53
+ <p class="text-2xl font-bold text-white">{{ stats.failed }}</p>
54
+ </div>
55
+ </div>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Table des tâches -->
60
+ <div class="bg-gray-800 rounded-lg border border-gray-700">
61
+ <div class="p-6 border-b border-gray-700 flex justify-between items-center">
62
+ <h3 class="text-lg font-semibold text-white">Liste des Tâches</h3>
63
+ <a href="{{ url_for('admin_tasks.view_logs') }}" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm">
64
+ <i class="fas fa-history mr-2"></i>Voir les Logs
65
+ </a>
66
+ </div>
67
+ <div class="overflow-x-auto">
68
+ <table class="w-full text-left">
69
+ <thead class="bg-gray-700/50">
70
+ <tr>
71
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Nom</th>
72
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Planning</th>
73
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Statut</th>
74
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Dernier Run</th>
75
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Prochain Run</th>
76
+ <th class="px-6 py-3 text-sm font-medium text-gray-300">Actions</th>
77
+ </tr>
78
+ </thead>
79
+ <tbody class="divide-y divide-gray-700">
80
+ {% for task in tasks %}
81
+ <tr class="hover:bg-gray-700/30 {% if task.should_run() %}bg-yellow-500/10{% endif %}">
82
+ <td class="px-6 py-4">
83
+ <div class="flex items-center">
84
+ <div>
85
+ <p class="text-sm font-medium text-white">{{ task.name }}</p>
86
+ <p class="text-xs text-gray-400">{{ task.description }}</p>
87
+ {% if task.should_run() %}
88
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-500/20 text-yellow-400 mt-1">
89
+ À exécuter!
90
+ </span>
91
+ {% endif %}
92
+ </div>
93
+ </div>
94
+ </td>
95
+ <td class="px-6 py-4 text-sm text-gray-300">
96
+ {% if task.schedule_type == 'daily' %}
97
+ Quotidien à {{ task.schedule_time.strftime('%H:%M') }}
98
+ {% elif task.schedule_type == 'hourly' %}
99
+ Toutes les heures
100
+ {% else %}
101
+ Tous les {{ task.interval_minutes }} min
102
+ {% endif %}
103
+ </td>
104
+ <td class="px-6 py-4">
105
+ {% if task.last_status == 'success' %}
106
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-500/20 text-green-400">
107
+ <i class="fas fa-check mr-1"></i> Succès
108
+ </span>
109
+ {% elif task.last_status == 'failed' %}
110
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-500/20 text-red-400">
111
+ <i class="fas fa-times mr-1"></i> Échec
112
+ </span>
113
+ {% elif task.last_status == 'running' %}
114
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-500/20 text-blue-400">
115
+ <i class="fas fa-spinner fa-spin mr-1"></i> En cours
116
+ </span>
117
+ {% else %}
118
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-500/20 text-gray-400">
119
+ En attente
120
+ </span>
121
+ {% endif %}
122
+ </td>
123
+ <td class="px-6 py-4 text-sm text-gray-300">
124
+ {% if task.last_run_at %}
125
+ {{ task.last_run_at.strftime('%d/%m/%Y %H:%M') }}
126
+ <br><span class="text-xs text-gray-500">{{ task.run_count }} runs</span>
127
+ {% else %}
128
+ <span class="text-gray-500">Jamais</span>
129
+ {% endif %}
130
+ </td>
131
+ <td class="px-6 py-4 text-sm text-gray-300">
132
+ {% if task.next_run_at %}
133
+ {{ task.next_run_at.strftime('%d/%m/%Y %H:%M') }}
134
+ {% else %}
135
+ <span class="text-gray-500">Non calculé</span>
136
+ {% endif %}
137
+ </td>
138
+ <td class="px-6 py-4">
139
+ <div class="flex space-x-2">
140
+ <form method="POST" action="{{ url_for('admin_tasks.run_task_now', task_id=task.id) }}" class="inline">
141
+ <button type="submit" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white rounded text-sm" title="Exécuter maintenant">
142
+ <i class="fas fa-play"></i>
143
+ </button>
144
+ </form>
145
+ <form method="POST" action="{{ url_for('admin_tasks.toggle_task', task_id=task.id) }}" class="inline">
146
+ <button type="submit" class="px-3 py-1 {% if task.is_active %}bg-gray-600 hover:bg-gray-700{% else %}bg-green-600 hover:bg-green-700{% endif %} text-white rounded text-sm" title="{{ 'Désactiver' if task.is_active else 'Activer' }}">
147
+ <i class="fas {% if task.is_active %}fa-pause{% else %}fa-play{% endif %}"></i>
148
+ </button>
149
+ </form>
150
+ </div>
151
+ </td>
152
+ </tr>
153
+ {% if task.last_error %}
154
+ <tr class="bg-red-500/5">
155
+ <td colspan="6" class="px-6 py-2">
156
+ <p class="text-xs text-red-400">
157
+ <i class="fas fa-exclamation-circle mr-1"></i>
158
+ <strong>Erreur:</strong> {{ task.last_error[:200] }}{% if task.last_error|length > 200 %}...{% endif %}
159
+ </p>
160
+ </td>
161
+ </tr>
162
+ {% endif %}
163
+ {% endfor %}
164
+ </tbody>
165
+ </table>
166
+ </div>
167
+ </div>
168
+
169
+ <!-- Info -->
170
+ <div class="bg-blue-900/30 border border-blue-700 rounded-lg p-4">
171
+ <div class="flex items-start">
172
+ <i class="fas fa-info-circle text-blue-400 mt-0.5 mr-3"></i>
173
+ <div>
174
+ <h4 class="text-sm font-medium text-blue-300">À propos du Scheduler</h4>
175
+ <p class="text-sm text-blue-200 mt-1">
176
+ Ce système remplace les cron jobs traditionnels. Les tâches sont stockées en base de données
177
+ et peuvent être exécutées manuellement ou automatiquement. Si le serveur redémarre,
178
+ les tâches manquées seront automatiquement rattrapées.
179
+ </p>
180
+ </div>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ {% endblock %}
185
+
186
+ {% block scripts %}
187
+ <script>
188
+ // Auto-refresh toutes les 30 secondes pour voir les mises à jour en temps réel
189
+ setInterval(function() {
190
+ window.location.reload();
191
+ }, 30000);
192
+ </script>
193
+ {% endblock %}
docker-compose.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ apex-ores:
5
+ build: .
6
+ container_name: apex-ores
7
+ ports:
8
+ - "7860:7860"
9
+ volumes:
10
+ # Persister la base de données SQLite
11
+ - ./instance:/app/instance
12
+ # Persister les logs
13
+ - ./logs:/var/log/apex
14
+ environment:
15
+ - FLASK_ENV=production
16
+ - PYTHONPATH=/app
17
+ restart: unless-stopped
18
+ healthcheck:
19
+ test: ["CMD", "curl", "-f", "http://localhost:7860/"]
20
+ interval: 30s
21
+ timeout: 10s
22
+ retries: 3
23
+ start_period: 40s
instance/dev_metals_investment.db CHANGED
Binary files a/instance/dev_metals_investment.db and b/instance/dev_metals_investment.db differ
 
scripts/__pycache__/daily_gains.cpython-314.pyc ADDED
Binary file (17.2 kB). View file