Remove JSON-based ad-hoc history storage and migrate to SQLAlchemy database backend with improved duration parsing and comprehensive CRUD operations

This commit is contained in:
Bruno Charest 2025-12-05 09:34:40 -05:00
parent ad3a8a5639
commit 123ca2cc08
14 changed files with 1237 additions and 1147 deletions

View File

@ -0,0 +1,52 @@
"""Add missing columns to schedules table for full scheduler support
Revision ID: 0002_add_schedule_columns
Revises: 0001_initial
Create Date: 2025-12-05
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = "0002_add_schedule_columns"
down_revision = "0001_initial"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Ajouter les colonnes manquantes à la table schedules
op.add_column("schedules", sa.Column("description", sa.Text(), nullable=True))
op.add_column("schedules", sa.Column("target_type", sa.String(), nullable=True, server_default="group"))
op.add_column("schedules", sa.Column("extra_vars", sa.JSON(), nullable=True))
op.add_column("schedules", sa.Column("timezone", sa.String(), nullable=True, server_default="America/Montreal"))
op.add_column("schedules", sa.Column("start_at", sa.DateTime(timezone=True), nullable=True))
op.add_column("schedules", sa.Column("end_at", sa.DateTime(timezone=True), nullable=True))
op.add_column("schedules", sa.Column("last_status", sa.String(), nullable=True, server_default="never"))
op.add_column("schedules", sa.Column("retry_on_failure", sa.Integer(), nullable=True, server_default="0"))
op.add_column("schedules", sa.Column("timeout", sa.Integer(), nullable=True, server_default="3600"))
op.add_column("schedules", sa.Column("run_count", sa.Integer(), nullable=True, server_default="0"))
op.add_column("schedules", sa.Column("success_count", sa.Integer(), nullable=True, server_default="0"))
op.add_column("schedules", sa.Column("failure_count", sa.Integer(), nullable=True, server_default="0"))
# Ajouter hosts_impacted à schedule_runs
op.add_column("schedule_runs", sa.Column("hosts_impacted", sa.Integer(), nullable=True, server_default="0"))
def downgrade() -> None:
op.drop_column("schedule_runs", "hosts_impacted")
op.drop_column("schedules", "failure_count")
op.drop_column("schedules", "success_count")
op.drop_column("schedules", "run_count")
op.drop_column("schedules", "timeout")
op.drop_column("schedules", "retry_on_failure")
op.drop_column("schedules", "last_status")
op.drop_column("schedules", "end_at")
op.drop_column("schedules", "start_at")
op.drop_column("schedules", "timezone")
op.drop_column("schedules", "extra_vars")
op.drop_column("schedules", "target_type")
op.drop_column("schedules", "description")

View File

@ -1,3 +0,0 @@
{
"hosts": {}
}

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,13 @@ class HostRepository:
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def get_by_name(self, name: str, include_deleted: bool = False) -> Optional[Host]:
stmt = select(Host).where(Host.name == name)
if not include_deleted:
stmt = stmt.where(Host.deleted_at.is_(None))
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create(self, *, id: str, name: str, ip_address: str, ansible_group: Optional[str] = None,
status: str = "unknown", reachable: bool = False, last_seen: Optional[datetime] = None) -> Host:
host = Host(

View File

@ -245,6 +245,10 @@ class DashboardManager {
// Tâche terminée - mettre à jour et rafraîchir les logs
this.handleTaskCompleted(data.data);
break;
case 'task_cancelled':
// Tâche annulée - mettre à jour l'UI
this.handleTaskCancelled(data.data);
break;
case 'task_progress':
// Mise à jour de progression - mettre à jour l'UI dynamiquement
this.handleTaskProgress(data.data);
@ -476,9 +480,13 @@ class DashboardManager {
</div>
<div class="flex flex-col space-y-1 ml-2">
<button class="p-1.5 bg-gray-700 rounded hover:bg-gray-600 transition-colors"
onclick="dashboard.viewTaskDetails(${task.id})" title="Voir les détails">
onclick="dashboard.viewTaskDetails('${task.id}')" title="Voir les détails">
<i class="fas fa-eye text-gray-300 text-xs"></i>
</button>
<button class="p-1.5 bg-red-700 rounded hover:bg-red-600 transition-colors"
onclick="dashboard.cancelTask('${task.id}')" title="Annuler la tâche">
<i class="fas fa-times text-white text-xs"></i>
</button>
</div>
</div>
</div>
@ -566,6 +574,22 @@ class DashboardManager {
);
}
handleTaskCancelled(taskData) {
console.log('Tâche annulée:', taskData);
// Retirer la tâche de la liste des tâches en cours
this.tasks = this.tasks.filter(t => String(t.id) !== String(taskData.id));
// Mettre à jour l'UI
this.updateRunningTasksUI(this.tasks.filter(t => t.status === 'running' || t.status === 'pending'));
// Rafraîchir les logs de tâches
this.refreshTaskLogs();
// Notification
this.showNotification('Tâche annulée', 'warning');
}
async loadLogs() {
try {
const logsData = await this.apiCall('/api/logs');
@ -4116,6 +4140,45 @@ class DashboardManager {
await this.executeTask(action, task.host);
}
async cancelTask(taskId) {
if (!confirm('Êtes-vous sûr de vouloir annuler cette tâche ?')) {
return;
}
try {
const response = await fetch(`/api/tasks/${taskId}/cancel`, {
method: 'POST',
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Erreur lors de l\'annulation');
}
const result = await response.json();
this.showNotification(result.message || 'Tâche annulée avec succès', 'success');
// Mettre à jour la liste des tâches
const task = this.tasks.find(t => String(t.id) === String(taskId));
if (task) {
task.status = 'cancelled';
task.error = 'Tâche annulée par l\'utilisateur';
}
// Rafraîchir l'affichage
this.pollRunningTasks();
this.renderTasks();
} catch (error) {
console.error('Erreur annulation tâche:', error);
this.showNotification(error.message || 'Erreur lors de l\'annulation de la tâche', 'error');
}
}
showAdHocConsole() {
console.log('Opening Ad-Hoc Console with:', {
adhocHistory: this.adhocHistory,
@ -4860,7 +4923,9 @@ class DashboardManager {
command: formData.get('command'),
module: formData.get('module'),
become: formData.get('become') === 'on',
timeout: parseInt(formData.get('timeout')) || 60
timeout: parseInt(formData.get('timeout')) || 60,
// catégorie d'historique choisie dans le select
category: formData.get('save_category') || 'default'
};
const resultDiv = document.getElementById('adhoc-result');

View File

@ -33,12 +33,15 @@ DATABASE_URL = os.environ.get("DATABASE_URL", f"sqlite+aiosqlite:///{DEFAULT_DB_
def _ensure_sqlite_dir(db_url: str) -> None:
if not db_url.startswith("sqlite"):
return
parsed = urlparse(db_url.replace("sqlite+aiosqlite", "sqlite"))
if parsed.scheme != "sqlite":
return
db_path = Path(parsed.path)
if db_path.parent:
db_path.parent.mkdir(parents=True, exist_ok=True)
# Extraire le chemin après sqlite+aiosqlite:///
# Sur Windows, le chemin peut être C:\... donc on ne peut pas utiliser urlparse
prefix = "sqlite+aiosqlite:///"
if db_url.startswith(prefix):
path_str = db_url[len(prefix):]
# Sur Windows, le chemin peut commencer par une lettre de lecteur (C:)
db_path = Path(path_str)
if db_path.parent and str(db_path.parent) != ".":
db_path.parent.mkdir(parents=True, exist_ok=True)
DEFAULT_DB_PATH.parent.mkdir(parents=True, exist_ok=True)
_ensure_sqlite_dir(DATABASE_URL)

View File

@ -1,9 +1,9 @@
from __future__ import annotations
from datetime import datetime
from typing import List, Optional
from typing import Any, Dict, List, Optional
from sqlalchemy import Boolean, DateTime, String, Text
from sqlalchemy import Boolean, DateTime, Integer, JSON, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from sqlalchemy.sql import func
@ -15,18 +15,30 @@ class Schedule(Base):
id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String, nullable=False)
description: Mapped[Optional[str]] = mapped_column(Text)
playbook: Mapped[str] = mapped_column(String, nullable=False)
target_type: Mapped[Optional[str]] = mapped_column(String, default="group")
target: Mapped[str] = mapped_column(String, nullable=False)
extra_vars: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON)
schedule_type: Mapped[str] = mapped_column(String, nullable=False)
schedule_time: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
recurrence_type: Mapped[Optional[str]] = mapped_column(String)
recurrence_time: Mapped[Optional[str]] = mapped_column(String)
recurrence_days: Mapped[Optional[str]] = mapped_column(Text)
cron_expression: Mapped[Optional[str]] = mapped_column(String)
timezone: Mapped[Optional[str]] = mapped_column(String, default="America/Montreal")
start_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
end_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
tags: Mapped[Optional[str]] = mapped_column(Text)
next_run: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
last_run: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
last_status: Mapped[Optional[str]] = mapped_column(String, default="never")
retry_on_failure: Mapped[Optional[int]] = mapped_column(Integer, default=0)
timeout: Mapped[Optional[int]] = mapped_column(Integer, default=3600)
run_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
success_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
failure_count: Mapped[Optional[int]] = mapped_column(Integer, default=0)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now())
deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))

View File

@ -20,6 +20,7 @@ class ScheduleRun(Base):
started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False)
completed_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True))
duration: Mapped[Optional[float]] = mapped_column(Float)
hosts_impacted: Mapped[Optional[int]] = mapped_column(Integer, default=0)
error_message: Mapped[Optional[str]] = mapped_column(Text)
output: Mapped[Optional[str]] = mapped_column(Text)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False, server_default=func.now())

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,79 +0,0 @@
{
"hosts": {
"dev.lab.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T20:20:03.555927+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"media.labb.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T01:52:34.259129+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"ali2v.xeon.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T14:35:47.874004+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"raspi.4gb.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T16:09:22.961007+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"raspi.8gb.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T16:10:53.117121+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"orangepi.pc.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T16:11:47.008381+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"jump.point.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T18:56:57.635706+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"hp.nas.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T20:25:44.595352+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"hp2.i7.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T20:25:51.895846+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"hp3.i5.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T20:25:59.998069+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"mimi.pc.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T20:26:08.419143+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"dev.prod.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T21:02:48.893923+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"automate.prod.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T21:03:44.363353+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"ali2v.truenas.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-02T21:47:48.804941+00:00",
"details": "Bootstrap réussi via API (user: automation)"
},
"hp.truenas.home": {
"bootstrap_ok": true,
"bootstrap_date": "2025-12-03T00:43:57.196419+00:00",
"details": "Bootstrap réussi via API (user: automation)"
}
}
}

View File

@ -1,436 +0,0 @@
{
"runs": [
{
"id": "run_e16db5ac6f5c",
"schedule_id": "sched_110c001afe0c",
"task_id": "2",
"started_at": "2025-12-05 02:35:00.012993+00:00",
"finished_at": "2025-12-05 02:35:32.549542+00:00",
"status": "success",
"duration_seconds": 32.45821054699991,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_6c434a169263",
"schedule_id": "sched_110c001afe0c",
"task_id": "1",
"started_at": "2025-12-05 02:30:00.004595+00:00",
"finished_at": "2025-12-05 02:30:30.003032+00:00",
"status": "success",
"duration_seconds": 29.95905439800117,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_debf96da90dd",
"schedule_id": "sched_110c001afe0c",
"task_id": "2",
"started_at": "2025-12-05 02:25:00.016354+00:00",
"finished_at": "2025-12-05 02:25:27.580495+00:00",
"status": "success",
"duration_seconds": 27.521959419998893,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_bda871b98a7c",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": "1",
"started_at": "2025-12-05 02:20:00.004169+00:00",
"finished_at": "2025-12-05 02:20:28.118352+00:00",
"status": "success",
"duration_seconds": 28.0753927859987,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_9acaf3ee6040",
"schedule_id": "sched_d5370726086b",
"task_id": "4",
"started_at": "2025-12-05 02:05:01.066895+00:00",
"finished_at": null,
"status": "running",
"duration_seconds": null,
"hosts_impacted": 0,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_25dee59d8f54",
"schedule_id": "sched_178b8e511908",
"task_id": "3",
"started_at": "2025-12-05 02:05:00.942939+00:00",
"finished_at": null,
"status": "running",
"duration_seconds": null,
"hosts_impacted": 0,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_06b2fe4c75f9",
"schedule_id": "sched_d5370726086b",
"task_id": "2",
"started_at": "2025-12-05 02:00:00.048675+00:00",
"finished_at": "2025-12-05 02:00:31.174698+00:00",
"status": "success",
"duration_seconds": 31.10493237799892,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_5a3ada10451e",
"schedule_id": "sched_178b8e511908",
"task_id": "1",
"started_at": "2025-12-05 02:00:00.004396+00:00",
"finished_at": "2025-12-05 02:00:30.956215+00:00",
"status": "success",
"duration_seconds": 30.92840002899902,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_484f67657ee4",
"schedule_id": "sched_d5370726086b",
"task_id": "3",
"started_at": "2025-12-05 01:55:00.084088+00:00",
"finished_at": "2025-12-05 01:55:32.096250+00:00",
"status": "success",
"duration_seconds": 31.975180113000533,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_7c9cbee2fe69",
"schedule_id": "sched_178b8e511908",
"task_id": "2",
"started_at": "2025-12-05 01:55:00.018967+00:00",
"finished_at": "2025-12-05 01:55:32.306141+00:00",
"status": "success",
"duration_seconds": 32.26106233700193,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_a45e3d80323d",
"schedule_id": "sched_d5370726086b",
"task_id": "1",
"started_at": "2025-12-05 01:50:00.003670+00:00",
"finished_at": "2025-12-05 01:50:27.635237+00:00",
"status": "success",
"duration_seconds": 27.58177596600217,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_6ebb5bb47219",
"schedule_id": "sched_d5370726086b",
"task_id": "2",
"started_at": "2025-12-05 01:45:00.003641+00:00",
"finished_at": "2025-12-05 01:45:26.015984+00:00",
"status": "success",
"duration_seconds": 25.9568110279979,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_f07c8820abcf",
"schedule_id": "sched_d5370726086b",
"task_id": "1",
"started_at": "2025-12-05 01:40:00.003609+00:00",
"finished_at": "2025-12-05 01:40:27.800302+00:00",
"status": "success",
"duration_seconds": 27.77215807200264,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_c831165b16d9",
"schedule_id": "sched_d5370726086b",
"task_id": null,
"started_at": "2025-12-05 01:35:00.003976+00:00",
"finished_at": null,
"status": "running",
"duration_seconds": null,
"hosts_impacted": 0,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_9eaff32da049",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 24,
"started_at": "2025-12-04 20:30:00.003167+00:00",
"finished_at": "2025-12-04 20:30:23.731178+00:00",
"status": "success",
"duration_seconds": 23.703570538998974,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_17e5d474fa3b",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 23,
"started_at": "2025-12-04 20:25:00.003921+00:00",
"finished_at": "2025-12-04 20:25:33.861123+00:00",
"status": "success",
"duration_seconds": 33.836465951999344,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_ac3b635e2ca0",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 22,
"started_at": "2025-12-04 20:20:00.002730+00:00",
"finished_at": "2025-12-04 20:20:24.021329+00:00",
"status": "success",
"duration_seconds": 23.990758482001183,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_ae3840e2d42a",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 21,
"started_at": "2025-12-04 20:15:00.003766+00:00",
"finished_at": "2025-12-04 20:15:30.504433+00:00",
"status": "success",
"duration_seconds": 30.471468816998822,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_c747bc5687ab",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 20,
"started_at": "2025-12-04 20:10:00.003667+00:00",
"finished_at": "2025-12-04 20:10:23.552886+00:00",
"status": "success",
"duration_seconds": 23.524676467999598,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_45014bc6cc03",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 19,
"started_at": "2025-12-04 20:05:00.002690+00:00",
"finished_at": "2025-12-04 20:05:23.450713+00:00",
"status": "success",
"duration_seconds": 23.4251933380001,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_e44e428d9a2c",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 18,
"started_at": "2025-12-04 20:00:00.003643+00:00",
"finished_at": "2025-12-04 20:00:23.328830+00:00",
"status": "success",
"duration_seconds": 23.302303992000816,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_a25301c67cc5",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 17,
"started_at": "2025-12-04 19:55:00.003143+00:00",
"finished_at": "2025-12-04 19:55:23.603376+00:00",
"status": "success",
"duration_seconds": 23.577402816999893,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_565da9652657",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 16,
"started_at": "2025-12-04 19:50:00.003353+00:00",
"finished_at": "2025-12-04 19:50:24.356543+00:00",
"status": "failed",
"duration_seconds": 24.328575002000434,
"hosts_impacted": 15,
"error_message": "",
"retry_attempt": 0
},
{
"id": "run_3b74b1e74163",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 15,
"started_at": "2025-12-04 19:45:00.002519+00:00",
"finished_at": "2025-12-04 19:45:23.751357+00:00",
"status": "success",
"duration_seconds": 23.722472203999132,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_dbde0be5bc63",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 14,
"started_at": "2025-12-04 19:40:00.003007+00:00",
"finished_at": "2025-12-04 19:40:23.751729+00:00",
"status": "success",
"duration_seconds": 23.723020589999578,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_15a1ad527d50",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 13,
"started_at": "2025-12-04 19:35:00.003399+00:00",
"finished_at": "2025-12-04 19:35:32.533102+00:00",
"status": "success",
"duration_seconds": 32.507498991999455,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_be8bcb150d04",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 12,
"started_at": "2025-12-04 19:30:00.003063+00:00",
"finished_at": "2025-12-04 19:30:23.472387+00:00",
"status": "success",
"duration_seconds": 23.440744194000217,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_f4d7d06f0c37",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 11,
"started_at": "2025-12-04 19:25:00.004160+00:00",
"finished_at": "2025-12-04 19:25:23.853468+00:00",
"status": "success",
"duration_seconds": 23.823963333001302,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_66dc096ac544",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 10,
"started_at": "2025-12-04 19:20:00.003618+00:00",
"finished_at": "2025-12-04 19:20:24.884824+00:00",
"status": "success",
"duration_seconds": 24.857591686999513,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_64b0ef374c3e",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 8,
"started_at": "2025-12-04 19:15:00.003439+00:00",
"finished_at": "2025-12-04 19:15:23.510149+00:00",
"status": "success",
"duration_seconds": 23.48368282699994,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_2e7db2553a2b",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 7,
"started_at": "2025-12-04 19:10:00.003912+00:00",
"finished_at": "2025-12-04 19:10:23.758800+00:00",
"status": "success",
"duration_seconds": 23.731919878000554,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_f11f2032c99d",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 6,
"started_at": "2025-12-04 19:05:00.005462+00:00",
"finished_at": "2025-12-04 19:05:27.950812+00:00",
"status": "success",
"duration_seconds": 27.921534645000065,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_6654ede83db4",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 5,
"started_at": "2025-12-04 19:00:00.002793+00:00",
"finished_at": "2025-12-04 19:00:19.946667+00:00",
"status": "success",
"duration_seconds": 19.924723819000064,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_98742a32df11",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 4,
"started_at": "2025-12-04 18:59:33.945907+00:00",
"finished_at": "2025-12-04 18:59:58.108811+00:00",
"status": "success",
"duration_seconds": 24.139778345000195,
"hosts_impacted": 15,
"error_message": null,
"retry_attempt": 0
},
{
"id": "run_079e0bef133a",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 3,
"started_at": "2025-12-04 18:55:00.002791+00:00",
"finished_at": "2025-12-04 18:55:39.881649+00:00",
"status": "failed",
"duration_seconds": 39.856552030000785,
"hosts_impacted": 15,
"error_message": "",
"retry_attempt": 0
},
{
"id": "run_719217dc687f",
"schedule_id": "sched_31a7ffb99bfd",
"task_id": 1,
"started_at": "2025-12-04 18:50:00.002822+00:00",
"finished_at": "2025-12-04 18:50:40.203643+00:00",
"status": "failed",
"duration_seconds": 40.181820916000106,
"hosts_impacted": 15,
"error_message": "",
"retry_attempt": 0
}
]
}

View File

@ -1,38 +0,0 @@
{
"schedules": [
{
"id": "sched_110c001afe0c",
"name": "Health-check-5min",
"description": null,
"playbook": "health-check.yml",
"target_type": "group",
"target": "all",
"extra_vars": null,
"schedule_type": "recurring",
"recurrence": {
"type": "custom",
"time": "02:00",
"days": null,
"day_of_month": null,
"cron_expression": "*/5 * * * *"
},
"timezone": "America/Montreal",
"start_at": null,
"end_at": null,
"next_run_at": "2025-12-04T21:40:00-05:00",
"last_run_at": "2025-12-05 02:35:00.012919+00:00",
"last_status": "success",
"enabled": true,
"retry_on_failure": 0,
"timeout": 3600,
"tags": [
"Test"
],
"run_count": 3,
"success_count": 3,
"failure_count": 0,
"created_at": "2025-12-05 02:24:06.110100+00:00",
"updated_at": "2025-12-05 02:35:32.549928+00:00"
}
]
}