Remove Docker configuration and implementation documentation files, keeping only core application code and essential documentation

This commit is contained in:
Bruno Charest 2025-12-09 14:07:27 -05:00
parent 3559552eca
commit 5a512d39b5
21 changed files with 2814 additions and 284 deletions

View File

@ -681,9 +681,23 @@ class TaskLogService:
"""Retourne le chemin du répertoire pour une date donnée (YYYY/MM/JJ)""" """Retourne le chemin du répertoire pour une date donnée (YYYY/MM/JJ)"""
if dt is None: if dt is None:
dt = datetime.now(timezone.utc) dt = datetime.now(timezone.utc)
year = dt.strftime("%Y")
month = dt.strftime("%m") # IMPORTANT : on utilise le fuseau horaire local (America/Montreal)
day = dt.strftime("%d") # pour déterminer la date du dossier de log, afin que les tâches
# exécutées en soirée ne basculent pas au jour suivant à cause de l'UTC.
import pytz
local_tz = pytz.timezone("America/Montreal")
if dt.tzinfo is None:
# Si la datetime est naïve, on considère qu'elle est déjà en heure locale
dt_local = local_tz.localize(dt)
else:
# Sinon on la convertit dans le fuseau local
dt_local = dt.astimezone(local_tz)
year = dt_local.strftime("%Y")
month = dt_local.strftime("%m")
day = dt_local.strftime("%d")
return self.base_dir / year / month / day return self.base_dir / year / month / day
def _generate_task_id(self) -> str: def _generate_task_id(self) -> str:

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,109 @@
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Schedules Widget (Moved to top) -->
<div class="lg:col-span-2">
<div class="glass-card p-6 fade-in">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold">
<i class="fas fa-calendar-alt text-purple-400 mr-2"></i>Planificateur
</h3>
<a href="#" data-page="schedules" class="nav-link text-purple-400 text-sm hover:underline">Voir tout →</a>
</div>
<!-- Stats mini -->
<div class="grid grid-cols-3 gap-2 mb-4">
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-lg font-bold text-green-400" id="dashboard-schedules-active">0</div>
<div class="text-xs text-gray-500">Actifs</div>
</div>
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-lg font-bold text-blue-400" id="dashboard-schedules-next">--</div>
<div class="text-xs text-gray-500">Prochaine</div>
</div>
<div class="text-center p-2 bg-gray-800/50 rounded">
<div class="text-lg font-bold text-red-400" id="dashboard-schedules-failures">0</div>
<div class="text-xs text-gray-500">Échecs 24h</div>
</div>
</div>
<!-- Next executions -->
<div id="dashboard-upcoming-schedules" class="space-y-2 text-sm">
<p class="text-gray-500 text-center py-4">Chargement...</p>
</div>
<button onclick="showCreateScheduleModal()" class="w-full mt-4 p-2 bg-purple-600 hover:bg-purple-500 rounded-lg text-sm transition-colors">
<i class="fas fa-plus mr-2"></i>Nouveau schedule
</button>
</div>
</div>
<!-- Quick Actions -->
<div class="glass-card p-6 fade-in">
<h3 class="text-xl font-semibold mb-6">Actions Rapides</h3>
<div class="space-y-4">
<button class="w-full p-4 bg-gradient-to-r from-green-600 to-green-700 rounded-lg text-left hover:from-green-500 hover:to-green-600 transition-all" onclick="executeTask('upgrade-all')">
<i class="fas fa-arrow-up mr-3"></i>
Mettre à jour tous les hôtes
</button>
<button class="w-full p-4 bg-gradient-to-r from-blue-600 to-blue-700 rounded-lg text-left hover:from-blue-500 hover:to-blue-600 transition-all" onclick="executeTask('reboot-all')">
<i class="fas fa-redo mr-3"></i>
Redémarrer les hôtes
</button>
<button class="w-full p-4 bg-gradient-to-r from-purple-600 to-purple-700 rounded-lg text-left hover:from-purple-500 hover:to-purple-600 transition-all" onclick="executeTask('health-check')">
<i class="fas fa-heartbeat mr-3"></i>
Vérifier la santé
</button>
<button class="w-full p-4 bg-gradient-to-r from-orange-600 to-orange-700 rounded-lg text-left hover:from-orange-500 hover:to-orange-600 transition-all" onclick="executeTask('backup')">
<i class="fas fa-save mr-3"></i>
Sauvegarder la configuration
</button>
</div>
</div>
<!-- Hosts Management (Moved down) -->
<div class="lg:col-span-2">
<div class="glass-card p-6 fade-in">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-semibold">Gestion des Hosts</h3>
<div class="flex items-center space-x-2">
<button class="btn-primary" onclick="dashboard.addHost()">
<i class="fas fa-plus mr-2"></i>
Ajouter Host
</button>
<div class="relative group">
<button class="px-4 py-2 bg-gray-700 hover:bg-gray-600 rounded-lg transition-colors flex items-center">
<i class="fas fa-layer-group mr-2"></i>
Groupes
<i class="fas fa-chevron-down ml-2 text-xs"></i>
</button>
<div class="absolute right-0 mt-2 w-56 bg-gray-800 border border-gray-700 rounded-lg shadow-xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50">
<div class="py-2">
<div class="px-4 py-2 text-xs text-gray-400 uppercase tracking-wider border-b border-gray-700">Environnements</div>
<button onclick="dashboard.showAddGroupModal('env')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter environnement
</button>
<button onclick="dashboard.showManageGroupsModal('env')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-edit text-blue-400 mr-3 w-4"></i>
Gérer environnements
</button>
<div class="px-4 py-2 text-xs text-gray-400 uppercase tracking-wider border-b border-t border-gray-700 mt-2">Rôles</div>
<button onclick="dashboard.showAddGroupModal('role')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-plus text-green-400 mr-3 w-4"></i>
Ajouter rôle
</button>
<button onclick="dashboard.showManageGroupsModal('role')" class="w-full px-4 py-2 text-left hover:bg-gray-700 flex items-center">
<i class="fas fa-edit text-blue-400 mr-3 w-4"></i>
Gérer rôles
</button>
</div>
</div>
</div>
</div>
</div>
<div id="hosts-list" class="space-y-4">
<!-- Hosts will be populated by JavaScript -->
</div>
</div>
</div>
</div>

Binary file not shown.

Binary file not shown.

81
docker/README.md Normal file
View File

@ -0,0 +1,81 @@
# Docker Configuration for Homelab Automation
Ce dossier contient tous les fichiers nécessaires pour conteneuriser et déployer l'application Homelab Automation API.
## Structure du dossier
- **Dockerfile** : Définition de l'image Docker.
- **docker-compose.yml** : Définition de la stack (Container API + Volumes).
- **.env.example** : Modèle de configuration des variables d'environnement.
- **init.sh** : Script d'initialisation de l'environnement local.
- **build-img.ps1** : Script PowerShell pour construire l'image (optimisé pour WSL).
- **deploy-img.sh / .ps1** : Scripts pour pousser l'image vers un registre Docker privé.
- **maj.sh** : Script pour mettre à jour la stack depuis le registre.
## Prérequis
- Docker Engine & Docker Compose
- Pour Windows : WSL 2 activé (recommandé pour les scripts .ps1)
## Installation et Démarrage
### 1. Initialisation
Lancez le script d'initialisation pour préparer le fichier `.env` et les répertoires de données :
```bash
# Dans le dossier docker/
bash init.sh
```
Ensuite, éditez le fichier `.env` généré pour configurer :
- `API_KEY` : Votre clé secrète.
- `SSH_USER` : L'utilisateur pour les connexions Ansible.
- `NTFY_*` : La configuration des notifications (optionnel).
### 2. Démarrage (Local)
Pour lancer l'application localement avec Docker Compose :
```bash
docker compose up -d --build
```
L'API sera accessible sur `http://localhost:8008`.
## Construction et Déploiement (Avancé)
Si vous utilisez un registre Docker privé (ex: Nexus, Harbor, Registry local), vous pouvez utiliser les scripts de build et de déploiement.
### Configuration du Registre
Modifiez les variables dans `.env` (ou laissez les valeurs par défaut si elles conviennent) :
- `REGISTRY_HOST`
- `REGISTRY_PORT`
- `IMAGE_NAME`
### Construction
Sous Windows (via PowerShell) :
```powershell
.\build-img.ps1
```
Sous Linux :
```bash
docker build -f Dockerfile -t homelab-automation-api:latest ..
```
### Déploiement
Pousse l'image vers le registre configuré :
```powershell
.\deploy-img.ps1
```
### Mise à jour
Sur le serveur de destination, pour tirer la dernière image et redémarrer :
```bash
./maj.sh
```

119
docker/build-img.ps1 Normal file
View File

@ -0,0 +1,119 @@
<#
.SYNOPSIS
Construit l'image Docker avec toutes les dépendances requises via PowerShell et WSL (Debian).
.DESCRIPTION
Ce script prépare les dépendances et construit l'image Docker sous WSL (Debian) avec BuildKit activé pour des builds plus rapides.
.PARAMETRES
-full : Si présent, effectue une construction complète de l'image Docker (équivalent à --no-cache).
.EXEMPLE
.\build-img.ps1 # Construction rapide (avec cache)
.\build-img.ps1 -full # Construction complète sans cache
.RETOURNE
Affiche le succès ou les erreurs pendant la construction de l'image Docker.
#>
param(
[switch]$full
)
$ErrorActionPreference = 'Stop'
try {
# Determine paths
$scriptDir = $PSScriptRoot
$projectRoot = (Get-Item (Join-Path $scriptDir '..')).FullName
# Convert Windows path to WSL path (manual, no wslpath)
$wslProjectRoot = $projectRoot -replace '^([A-Za-z]):\\','/mnt/$1/' -replace '\\','/'
$wslProjectRoot = $wslProjectRoot.ToLower()
# Check if Docker is available in WSL Debian
Write-Host "Vérification de Docker dans WSL Debian..." -ForegroundColor Cyan
$dockerCheck = wsl -d Debian bash -c "docker --version" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "Docker n'est pas disponible dans WSL Debian : $dockerCheck"
exit 1
}
Write-Host "Docker détecté : $dockerCheck" -ForegroundColor Green
# Check if the Dockerfile exists
$dockerfilePath = Join-Path $scriptDir "Dockerfile"
if (-not (Test-Path $dockerfilePath)) {
Write-Error "Le fichier Dockerfile n'existe pas dans $scriptDir"
exit 1
}
# Check if .dockerignore exists, warn if not
$dockerignorePath = Join-Path $projectRoot ".dockerignore"
if (-not (Test-Path $dockerignorePath)) {
Write-Warning "Aucun fichier .dockerignore trouvé. Créez-en un pour optimiser le build."
Write-Host "Exemple de contenu :" -ForegroundColor Yellow
Write-Host @"
__pycache__
*.pyc
*.pyo
*.pyd
.git
.vscode
*.log
.env
"@ -ForegroundColor DarkGray
}
# Compose build command with BuildKit enabled
$noCache = if ($full) { '--no-cache' } else { '' }
# Escape spaces in path for bash
$escapedPath = $wslProjectRoot -replace ' ','\\ '
# Build command parts
$cdCmd = "cd `"$wslProjectRoot`""
$dockerCmd = "docker build $noCache --progress=plain -t homelab-automation-api:latest -f docker/Dockerfile ."
# Complete command with BuildKit
$innerCmd = "export DOCKER_BUILDKIT=1 && $cdCmd && $dockerCmd"
# Run build inside WSL Debian to use Linux Docker daemon
Write-Host "Construction de l'image Docker homelab-automation-api:latest..." -ForegroundColor Cyan
Write-Host "BuildKit activé pour un build optimisé avec cache" -ForegroundColor Cyan
$buildResult = wsl -d Debian bash -lc $innerCmd 2>&1
# Display build output
$buildResult | ForEach-Object { Write-Host $_ }
if ($LASTEXITCODE -ne 0) {
Write-Error "Erreur lors de la construction de l'image Docker (code: $LASTEXITCODE)"
exit 1
}
# Verify the image was built successfully
$verifyCmd = "docker image inspect homelab-automation-api:latest --format='{{.Size}}'"
Write-Host "`nVérification de l'image construite..." -ForegroundColor Cyan
$imageSize = wsl -d Debian bash -lc $verifyCmd 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Error "L'image Docker n'a pas été construite correctement : $imageSize"
exit 1
}
# Convert size to MB
$sizeMB = [math]::Round([long]$imageSize / 1MB, 2)
Write-Host "`n================================================" -ForegroundColor Green
Write-Host "✓ Image Docker construite avec succès" -ForegroundColor Green
Write-Host " Nom: homelab-automation-api:latest" -ForegroundColor Green
Write-Host " Taille: $sizeMB MB" -ForegroundColor Green
Write-Host "================================================" -ForegroundColor Green
# Display tips for faster rebuilds
if (-not $full) {
Write-Host "`nConseils pour les prochains builds :" -ForegroundColor Cyan
Write-Host " • Les dépendances Python sont mises en cache" -ForegroundColor Gray
Write-Host " • Seuls les fichiers modifiés déclencheront un rebuild" -ForegroundColor Gray
}
}
catch {
Write-Error "Erreur lors de la construction de l'image Docker : $_"
exit 1
}

46
docker/deploy-img.ps1 Normal file
View File

@ -0,0 +1,46 @@
<#
.SYNOPSIS
Exécute le script deploy-img.sh dans WSL Debian.
.DESCRIPTION
Ce script PowerShell lance le script Bash deploy-img.sh dans le répertoire courant sous la distribution WSL Debian.
Le script déploie l'image Docker locale vers le registre distant et gère les versions.
.PARAMETRES
Aucun paramètre requis actuellement.
.EXEMPLE
.\deploy-img.ps1
.RETOURNE
Affiche le succès ou les erreurs lors du déploiement de l'image Docker.
#>
param()
$ErrorActionPreference = 'Stop'
try {
Write-Verbose "[Étape 1] Détermination du répertoire du script."
$scriptDir = $PSScriptRoot
Write-Verbose "[Étape 2] Conversion du chemin Windows vers WSL (manuel, sans wslpath)."
# Remplacement manuel du préfixe du chemin Windows par le préfixe WSL
$wslScriptDir = $scriptDir -replace '^([A-Za-z]):\\', '/mnt/$1/' -replace '\\','/'
$wslScriptDir = $wslScriptDir.ToLower()
Write-Verbose "[Étape 3] Vérification des permissions du script bash."
wsl -d Debian -- chmod +x "$wslScriptDir/deploy-img.sh"
Write-Verbose "[Étape 4] Construction de la commande Bash à exécuter."
$innerCmd = "cd '$wslScriptDir' && ./deploy-img.sh"
Write-Verbose "[Étape 5] Exécution du script deploy-img.sh dans WSL Debian."
wsl -d Debian bash -lc $innerCmd
if ($LASTEXITCODE -ne 0) {
throw "Le script de déploiement a échoué avec le code d'erreur: $LASTEXITCODE"
}
Write-Host "Déploiement de l'image Docker homelab-automation-api:latest terminé avec succès." -ForegroundColor Green
}
catch {
Write-Error "Erreur lors de l'exécution du déploiement de l'image Docker : $_"
exit 1
}

247
docker/deploy-img.sh Normal file
View File

@ -0,0 +1,247 @@
#!/usr/bin/env bash
# Script: deploy-img.sh
# Description: Pousse l'image Docker locale vers un registre HTTP (insecure), gère le versioning semver et la rétention
# Date: 2025-04-18
# Mise à jour: 2025-09-19 - Vérification stricte du daemon (insecure-registries), compatibilité Windows/Linux
set -euo pipefail
#####################################
# CONFIG UTILISATEUR #
#####################################
# Charger les variables depuis .env s'il existe
if [[ -f ".env" ]]; then
# shellcheck disable=SC1091
source .env
fi
IMAGE_NAME="${IMAGE_NAME:-homelab-automation-api}"
REGISTRY_HOST="${REGISTRY_HOST:-docker-registry.dev.home}"
REGISTRY_PORT="${REGISTRY_PORT:-5000}"
MAX_VERSIONS="${MAX_VERSIONS:-5}" # nombre de versions (semver) à conserver
LOCAL_TAG="${LOCAL_TAG:-latest}" # tag local source
PUSH_LATEST="${PUSH_LATEST:-yes}" # yes/no : pousser aussi :latest
#####################################
# DÉDUCTIONS & CONSTANTES #
#####################################
REGISTRY="${REGISTRY_HOST}:${REGISTRY_PORT}"
REMOTE_REPO="${REGISTRY}/${IMAGE_NAME}"
LOCAL_IMAGE="${IMAGE_NAME}:${LOCAL_TAG}"
TEMP_DIR="/tmp/docker-deploy-$(date +%s)"
CURL="curl -fsSL"
JQ_BIN="${JQ_BIN:-jq}" # possibilité de surcharger via env
# Headers pour obtenir le digest (manifest v2)
ACCEPT_MANIFEST='application/vnd.docker.distribution.manifest.v2+json'
#####################################
# UTILITAIRES #
#####################################
info() { echo -e "\033[1;34m[INFO]\033[0m $*"; }
warn() { echo -e "\033[1;33m[AVERT]\033[0m $*"; }
error() { echo -e "\033[1;31m[ERREUR]\033[0m $*" >&2; }
success() { echo -e "\033[1;32m[SUCCÈS]\033[0m $*"; }
cleanup() {
set +e
[[ -d "$TEMP_DIR" ]] && rm -rf "$TEMP_DIR"
}
trap cleanup EXIT
is_windows_shell() {
# Git Bash / MSYS / CYGWIN ou variable OS=Windows_NT
if [[ "${OS:-}" == "Windows_NT" ]] || uname -s | grep -qiE 'mingw|msys|cygwin'; then
return 0
fi
return 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || { error "Commande requise manquante: $1"; exit 1; }
}
#####################################
# PRÉ-VÉRIFICATIONS #
#####################################
require_cmd docker
require_cmd curl
require_cmd sed
require_cmd grep
require_cmd awk
require_cmd sort
# Vérifier l'image locale
info "Vérification de l'image locale '${LOCAL_IMAGE}'…"
if ! docker image inspect "${LOCAL_IMAGE}" >/dev/null 2>&1; then
error "L'image locale ${LOCAL_IMAGE} n'existe pas. Construis-la d'abord (docker build …)."
exit 1
fi
# Vérifier config daemon: insecure-registries
info "Vérification de la config du daemon Docker (insecure-registries)…"
if ! docker info >/dev/null 2>&1; then
error "Impossible de contacter le daemon Docker. Est-il démarré ?"
exit 1
fi
if ! docker info 2>/dev/null \
| tr -d '\r' \
| awk '/Insecure Registries:/,/^$/ {print}' \
| grep -q -E "(^|[[:space:]])${REGISTRY_HOST}:${REGISTRY_PORT}([[:space:]]|$)"; then
error "Le daemon Docker n'a PAS '${REGISTRY}' dans insecure-registries."
if is_windows_shell; then
info "Sous Windows (Docker Desktop) : Settings → Docker Engine, ajoute dans le JSON :"
printf '%s\n' ' "insecure-registries": ["'"${REGISTRY}"'"]'
info "Puis clique 'Apply & Restart' et relance ce script."
else
info "Sous Linux : édite /etc/docker/daemon.json et ajoute par ex. :"
printf '%s\n' '{ "insecure-registries": ["'"${REGISTRY}"'"] }'
info "Puis : sudo systemctl restart docker"
fi
exit 1
fi
# Ping HTTP direct du registre (API v2)
info "Vérification HTTP directe du registre (http://${REGISTRY}/v2/)…"
if ! ${CURL} "http://${REGISTRY}/v2/" >/dev/null 2>&1; then
error "Impossible de joindre http://${REGISTRY}/v2/. Le registre écoute-t-il bien en HTTP sur :${REGISTRY_PORT} ?"
exit 1
fi
# Auth facultative via env (DOCKER_USERNAME / DOCKER_PASSWORD)
if [[ -n "${DOCKER_USERNAME:-}" && -n "${DOCKER_PASSWORD:-}" ]]; then
info "Authentification au registre (docker login)…"
# NB: docker login respecte le mode HTTP si daemon configuré en insecure-registries
echo "${DOCKER_PASSWORD}" | docker login "${REGISTRY}" --username "${DOCKER_USERNAME}" --password-stdin >/dev/null
else
info "Aucun identifiant Docker fourni (DOCKER_USERNAME/DOCKER_PASSWORD). Tentative sans authentification."
fi
mkdir -p "${TEMP_DIR}"
#####################################
# RÉCUPÉRATION DES TAGS #
#####################################
get_all_tags() {
# Renvoie la liste des tags (un par ligne) ou rien si vide/erreur
# Utilise jq si dispo, sinon un parseur simple.
local tags_json
if ! tags_json="$(${CURL} "http://${REGISTRY}/v2/${IMAGE_NAME}/tags/list" 2>/dev/null)"; then
return 0
fi
if command -v "${JQ_BIN}" >/dev/null 2>&1; then
echo "${tags_json}" | ${JQ_BIN} -r '.tags[]?' 2>/dev/null || true
else
# fallback très simple (non robuste à tous les cas, mais suffisant ici)
echo "${tags_json}" \
| tr -d '\n' \
| sed -n 's/.*"tags":[[]\([^]]*\)[]].*/\1/p' \
| tr -d '"' \
| tr ',' '\n' \
| sed 's/^[[:space:]]*//; s/[[:space:]]*$//'
fi
}
filter_semver() {
# Garde uniquement X.Y.Z (numériques)
grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' || true
}
increment_patch() {
# Lit un tag semver X.Y.Z et augmente Z
local tag="$1"
local major minor patch
IFS='.' read -r major minor patch <<< "${tag}"
echo "${major}.${minor}.$((patch+1))"
}
info "Récupération des tags existants…"
ALL_TAGS="$(get_all_tags || true)"
SEMVER_TAGS="$(printf '%s\n' "${ALL_TAGS}" | filter_semver | sort -V || true)"
if [[ -z "${SEMVER_TAGS}" ]]; then
NEW_TAG="1.0.0"
else
LATEST_TAG="$(printf '%s\n' "${SEMVER_TAGS}" | tail -n1)"
NEW_TAG="$(increment_patch "${LATEST_TAG}")"
fi
info "Tag retenu pour cette release : ${NEW_TAG}"
#####################################
# TAG + PUSH (HTTP via daemon)
#####################################
info "Taggage local → ${REMOTE_REPO}:${NEW_TAG}"
docker tag "${LOCAL_IMAGE}" "${REMOTE_REPO}:${NEW_TAG}"
if [[ "${PUSH_LATEST}" == "yes" ]]; then
info "Taggage local → ${REMOTE_REPO}:latest"
docker tag "${LOCAL_IMAGE}" "${REMOTE_REPO}:latest"
fi
info "Push de ${REMOTE_REPO}:${NEW_TAG} (HTTP via insecure-registries)…"
docker push --disable-content-trust "${REMOTE_REPO}:${NEW_TAG}"
if [[ "${PUSH_LATEST}" == "yes" ]]; then
info "Push de ${REMOTE_REPO}:latest…"
docker push --disable-content-trust "${REMOTE_REPO}:latest"
fi
#####################################
# RÉTENTION: SUPPRIMER ANCIENS #
#####################################
delete_by_tag() {
# Supprime un manifest par son tag en récupérant le digest via HEAD
local tag="$1"
local digest
# On demande le digest via HEAD + Accept manifest v2
digest="$(curl -fsSI -H "Accept: ${ACCEPT_MANIFEST}" "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${tag}" \
| tr -d '\r' \
| awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}' \
| tail -n1)"
if [[ -z "${digest}" ]]; then
warn "Digest introuvable pour ${tag} (manifest v2 absent ?). Skip."
return 0
fi
info "Suppression manifest ${tag} (digest: ${digest})…"
# La delete API ne renvoie rien en cas de succès (204)
if ! curl -fsS -X DELETE "http://${REGISTRY}/v2/${IMAGE_NAME}/manifests/${digest}" >/dev/null; then
warn "Échec suppression manifest pour ${tag} (digest: ${digest})."
fi
}
# Mettre à jour la liste après push
ALL_TAGS="$(get_all_tags || true)"
SEMVER_TAGS="$(printf '%s\n' "${ALL_TAGS}" | filter_semver | sort -V || true)"
if [[ -n "${SEMVER_TAGS}" ]]; then
COUNT="$(printf '%s\n' "${SEMVER_TAGS}" | wc -l | awk '{print $1}')"
if (( COUNT > MAX_VERSIONS )); then
TO_DELETE_COUNT=$(( COUNT - MAX_VERSIONS ))
# On supprime les plus anciennes
OLDEST="$(printf '%s\n' "${SEMVER_TAGS}" | head -n "${TO_DELETE_COUNT}")"
while IFS= read -r old_tag; do
[[ -z "${old_tag}" ]] && continue
# Ne jamais supprimer le tag que l'on vient de pousser (sécurité)
if [[ "${old_tag}" == "${NEW_TAG}" ]]; then
continue
fi
delete_by_tag "${old_tag}"
done <<< "${OLDEST}"
fi
fi
#####################################
# NETTOYAGE LOCAL OPTIONNEL #
#####################################
info "Nettoyage des tags locaux temporaires…"
set +e
docker rmi "${REMOTE_REPO}:${NEW_TAG}" >/dev/null 2>&1
[[ "${PUSH_LATEST}" == "yes" ]] && docker rmi "${REMOTE_REPO}:latest" >/dev/null 2>&1
set -e
success "Déploiement terminé. Version publiée : ${NEW_TAG}"
exit 0

View File

@ -2,13 +2,15 @@
services: services:
homelab-dashboard: homelab-dashboard:
build: build:
context: . context: ..
dockerfile: Dockerfile dockerfile: docker/Dockerfile
container_name: homelab-automation-dashboard container_name: homelab-automation-dashboard
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8008:8000" - "8008:8000"
environment: environment:
# zone horaire
- TZ=${TZ:-America/Montreal}
# Clé API pour l'authentification # Clé API pour l'authentification
- API_KEY=${API_KEY:-dev-key-12345} - API_KEY=${API_KEY:-dev-key-12345}
# Utilisateur SSH pour Ansible # Utilisateur SSH pour Ansible
@ -45,17 +47,17 @@ services:
# Volume Docker natif pour la base de données SQLite (évite les problèmes I/O sur Windows) # Volume Docker natif pour la base de données SQLite (évite les problèmes I/O sur Windows)
- homelab_data:/app/data - homelab_data:/app/data
# Monter l'inventaire Ansible (permet de modifier sans rebuild) # Monter l'inventaire Ansible (permet de modifier sans rebuild)
- ${ANSIBLE_INVENTORY:-./ansible/inventory}:/ansible/inventory - ${ANSIBLE_INVENTORY:-../ansible/inventory}:/ansible/inventory
# Monter les playbooks (permet de modifier sans rebuild) # Monter les playbooks (permet de modifier sans rebuild)
- ${ANSIBLE_PLAYBOOKS:-./ansible/playbooks}:/ansible/playbooks - ${ANSIBLE_PLAYBOOKS:-../ansible/playbooks}:/ansible/playbooks
# Monter les variables de groupe # Monter les variables de groupe
- ${ANSIBLE_GROUP_VARS:-./ansible/inventory/group_vars}:/ansible/inventory/group_vars - ${ANSIBLE_GROUP_VARS:-../ansible/inventory/group_vars}:/ansible/inventory/group_vars
# Monter les clés SSH depuis le host # Monter les clés SSH depuis le host
- ${SSH_KEY_DIR:-~/.ssh}:/app/ssh_keys:ro - ${SSH_KEY_DIR:-~/.ssh}:/app/ssh_keys:ro
# Volume pour les logs (optionnel) # Volume pour les logs (optionnel)
- homelab_logs:/app/logs - homelab_logs:/app/logs
# Monter le répertoire des logs de tâches depuis le host # Monter le répertoire des logs de tâches depuis le host
- ${DIR_LOGS_TASKS:-./tasks_logs}:/app/tasks_logs - ${DIR_LOGS_TASKS:-../tasks_logs}:/app/tasks_logs
networks: networks:
- homelab-network - homelab-network
healthcheck: healthcheck:

51
docker/init.sh Normal file
View File

@ -0,0 +1,51 @@
#!/bin/bash
# init.sh - Script d'initialisation de l'environnement pour Homelab Automation
# Ce script prépare le fichier .env et les répertoires nécessaires.
set -e
# Se placer dans le dossier du script
cd "$(dirname "$0")"
echo "=== Initialisation de l'environnement Docker ==="
# 1. Configuration du fichier .env
if [ ! -f .env ]; then
echo "Creation du fichier .env a partir de .env.example..."
cp .env.example .env
# Génération d'une clé API aléatoire
API_KEY=$(openssl rand -hex 16 2>/dev/null || echo "change-me-please-123")
# Remplacement de la clé API par défaut (si sed disponible)
if command -v sed >/dev/null 2>&1; then
# Compatible Linux/Mac
if [[ "$OSTYPE" == "darwin"* ]]; then
sed -i '' "s/API_KEY=dev-key-12345/API_KEY=${API_KEY}/" .env
else
sed -i "s/API_KEY=dev-key-12345/API_KEY=${API_KEY}/" .env
fi
echo "Clé API générée aléatoirement."
fi
echo "--> Fichier .env créé. Pensez à l'éditer pour configurer vos accès SSH et notifications."
else
echo "--> Le fichier .env existe déjà."
fi
# 2. Création des répertoires de données
echo "Vérification des répertoires de données..."
mkdir -p ../data
mkdir -p ../tasks_logs
# 3. Vérification des permissions
echo "Ajustement des permissions..."
chmod +x deploy-img.sh maj.sh init.sh 2>/dev/null || true
echo "=== Initialisation terminée ==="
echo "Pour démarrer l'application :"
echo " docker compose up -d --build"
echo ""
echo "Pour construire l'image :"
echo " ./build-img.ps1 (Windows/WSL)"
echo " docker build -f Dockerfile -t homelab-automation-api:latest .. (Linux)"

27
docker/maj.sh Normal file
View File

@ -0,0 +1,27 @@
#!/usr/bin/env bash
set -euo pipefail
# Always execute relative to this script directory
cd "$(dirname "$0")"
# Charger les variables depuis .env s'il existe
if [[ -f ".env" ]]; then
# shellcheck disable=SC1091
source .env
fi
REGISTRY_HOST="${REGISTRY_HOST:-docker-registry.dev.home}"
REGISTRY_PORT="${REGISTRY_PORT:-5000}"
IMAGE_NAME="${IMAGE_NAME:-homelab-automation-api}"
REGISTRY="${REGISTRY_HOST}:${REGISTRY_PORT}"
IMAGE="${IMAGE_NAME}:latest"
# Pull latest image from private registry
docker image pull "${REGISTRY}/${IMAGE}"
# Restart stack with the current compose file
docker compose down
docker compose up -d
echo "Homelab Automation API updated and running."

View File

@ -0,0 +1,270 @@
# Guide de Responsive Mobile - Homelab Automation Dashboard
## Résumé des Modifications
Ce document détaille les modifications apportées pour rendre l'application Homelab Automation Dashboard entièrement responsive et optimisée pour les appareils mobiles.
---
## 1. Navigation Mobile
### Menu Hamburger
- **Breakpoint**: `md:` (768px)
- **Composants ajoutés**:
- `#mobile-nav-overlay` - Overlay semi-transparent avec blur
- `#mobile-nav-sidebar` - Sidebar glissante depuis la gauche
- `#mobile-menu-btn` - Bouton hamburger animé
### Fonctionnalités
- Animation du bouton hamburger (transformation en X)
- Fermeture par swipe vers la gauche
- Fermeture par touche Escape
- Fermeture automatique au redimensionnement vers desktop
- Liens de navigation avec icônes
- Toggle de thème intégré
### Classes CSS
```css
.mobile-menu-btn
.mobile-nav-overlay
.mobile-nav-sidebar
.mobile-nav-links
.desktop-nav-links
```
---
## 2. Breakpoints Utilisés
| Breakpoint | Taille | Usage |
|------------|--------|-------|
| Base | < 640px | Mobile (téléphones) |
| `sm:` | ≥ 640px | Petits écrans (grands téléphones) |
| `md:` | ≥ 768px | Tablettes |
| `lg:` | ≥ 1024px | Desktop |
| `xl:` | ≥ 1280px | Grands écrans |
---
## 3. Touch Targets
Tous les éléments interactifs respectent la taille minimale de **44x44px** :
```css
.touch-target {
min-width: 44px;
min-height: 44px;
}
```
### Éléments concernés
- Boutons de navigation
- Filtres de tâches/schedules
- Boutons d'action (kebab menu)
- Éléments de dropdown
- Boutons de formulaire
---
## 4. Adaptations par Section
### Dashboard
- **Hero**: Titre réduit de `text-5xl` à `text-3xl` sur mobile
- **Métriques**: Grille 2x2 au lieu de 4 colonnes
- **Actions Rapides**: Grille 2x2 avec labels courts
- **Boutons**: Empilés verticalement sur mobile
### Hosts
- **Header**: Layout en colonne sur mobile
- **Boutons**: Labels condensés ("Ajouter" au lieu de "Ajouter Host")
- **Dropdown groupes**: Padding augmenté pour touch
### Playbooks
- **Recherche**: Pleine largeur sur mobile
- **Filtres catégories**: Scroll horizontal avec masquage scrollbar
- **Bouton nouveau**: Texte condensé sur mobile
### Tasks
- **Filtres status**: Scroll horizontal
- **Calendrier date**: Adapté à la largeur de l'écran
- **Cartes tâches**: Layout empilé
### Schedules
- **Stats**: Grille 2x2 sur mobile
- **Filtres**: Empilés avec recherche pleine largeur
- **Calendrier**: Cellules réduites
### Logs
- **Boutons**: Icônes seules sur mobile
- **Container**: Hauteur max réduite (400px vs 600px)
### Aide
- **Table des matières**: Masquée sur mobile/tablette
- **Contenu**: Pleine largeur
---
## 5. Modales Mobile
### Comportement
- Plein écran sur mobile (`< 640px`)
- Scroll interne avec prevention du scroll body
- Bouton de fermeture fixe en haut à droite
```css
@media (max-width: 640px) {
#modal .glass-card {
min-height: 100vh;
max-height: 100vh;
width: 100%;
border-radius: 0;
}
}
```
---
## 6. Animations Optimisées
### Désactivation sur Mobile
- Animation `float` désactivée
- Hover transforms supprimés
- Transitions réduites à `opacity` seulement
### Respect de `prefers-reduced-motion`
```css
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
```
---
## 7. Interactions Tactiles
### Swipe Gestures
- **Swipe droite** (bord gauche): Ouvre la navigation
- **Swipe gauche** (nav ouverte): Ferme la navigation
- Seuil: 80px minimum
### Kebab Menu
- Visible uniquement sur mobile (`< 640px`)
- Remplace les boutons d'action inline
- Fermeture au clic extérieur
```javascript
function toggleKebabMenu(element, event) {
// Toggle menu dropdown
}
```
---
## 8. Utilitaires CSS Ajoutés
```css
/* Masquer scrollbar */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Safe area pour iPhone X+ */
.safe-area-bottom {
padding-bottom: max(16px, env(safe-area-inset-bottom));
}
/* Touch target minimum */
.touch-target {
min-width: 44px;
min-height: 44px;
}
```
---
## 9. Support Orientation Paysage
```css
@media (max-height: 500px) and (orientation: landscape) {
.hero-section {
padding-top: 60px;
padding-bottom: 20px;
}
}
```
---
## 10. Thème Clair Mobile
Tous les composants mobiles supportent le thème clair :
- Navigation sidebar
- Action bar
- Kebab menus
- Dropdowns
---
## Guide d'Implémentation Future
### Pour ajouter un nouveau composant responsive :
1. **Commencer par le mobile** (mobile-first)
2. **Utiliser les classes Tailwind** `sm:`, `md:`, `lg:`
3. **Respecter les touch targets** (min 44x44px)
4. **Tester sur plusieurs appareils** :
- iPhone SE (375px)
- iPhone 14 Pro (393px)
- iPad (768px)
- Desktop (1024px+)
### Pattern de bouton responsive :
```html
<button class="btn-primary flex-1 sm:flex-none text-sm touch-target">
<i class="fas fa-icon mr-1 sm:mr-2"></i>
<span class="hidden sm:inline">Texte complet</span>
<span class="sm:hidden">Court</span>
</button>
```
### Pattern de grille responsive :
```html
<div class="grid grid-cols-2 md:grid-cols-4 gap-2 sm:gap-4">
<!-- Items -->
</div>
```
### Pattern de layout empilé :
```html
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4">
<!-- Items -->
</div>
```
---
## Tests Recommandés
1. **Chrome DevTools** - Device Mode
2. **Safari** - Responsive Design Mode
3. **BrowserStack** - Appareils réels
4. **Lighthouse** - Performance mobile
### Checklist de validation :
- [ ] Navigation mobile fonctionne
- [ ] Touch targets ≥ 44px
- [ ] Pas de scroll horizontal non voulu
- [ ] Textes lisibles (min 14px)
- [ ] Boutons accessibles
- [ ] Modales plein écran
- [ ] Thème clair/sombre fonctionne
- [ ] Animations fluides
- [ ] Safe area respectée (notch)
---
*Dernière mise à jour: Décembre 2025*

File diff suppressed because one or more lines are too long