Upload 76 files
Browse files- Dockerfile +50 -6
- apex-scheduler.service +17 -0
- app/__init__.py +7 -7
- app/__pycache__/__init__.cpython-314.pyc +0 -0
- app/__pycache__/task_runner.cpython-314.pyc +0 -0
- app/models/__init__.py +135 -0
- app/models/__pycache__/__init__.cpython-314.pyc +0 -0
- app/routes/__pycache__/admin_tasks.cpython-314.pyc +0 -0
- app/routes/admin_tasks.py +123 -0
- app/task_runner.py +287 -0
- app/templates/admin/base.html +8 -0
- app/templates/admin/task_logs.html +129 -0
- app/templates/admin/tasks.html +193 -0
- docker-compose.yml +23 -0
- instance/dev_metals_investment.db +0 -0
- scripts/__pycache__/daily_gains.cpython-314.pyc +0 -0
Dockerfile
CHANGED
|
@@ -1,22 +1,66 @@
|
|
| 1 |
-
#
|
|
|
|
|
|
|
| 2 |
FROM python:3.11-bookworm
|
| 3 |
|
| 4 |
-
# Mettre à jour et installer
|
| 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 |
-
#
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 38 |
-
|
| 39 |
}
|
| 40 |
except Exception:
|
| 41 |
-
# Return defaults if database is not yet initialized
|
| 42 |
return {
|
| 43 |
-
|
| 44 |
-
|
| 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
|
|
|