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:
+1087
-581
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
|
||||
+67
-2
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
|
||||
+14
-2
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user