Mailix / templates /static_deploy.html
ernestmindres's picture
Update templates/static_deploy.html
633257d verified
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Mailix | Déploiement Statique</title>
<link rel="icon" type="image/png" href="https://i.imgur.com/7Gn3toV.png">
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@24,400,0,0" />
<script src="https://cdn.tailwindcss.com"></script>
<style>
/* -------------------------------------- */
/* CSS pour la zone de Glisser-Déposer */
/* -------------------------------------- */
.dropzone {
border: 3px dashed #007bff;
background-color: #f8f9fa;
padding: 40px;
text-align: center;
cursor: pointer;
transition: background-color 0.2s, border-color 0.2s;
border-radius: 12px;
margin-bottom: 20px;
user-select: none;
}
.dropzone.dragover {
background-color: #e2f0ff;
border-color: #0056b3;
}
.dropzone-icon {
font-size: 3rem;
color: #007bff;
margin-bottom: 10px;
}
.dropzone-text {
font-size: 1.2rem;
font-weight: 600;
color: #343a40;
}
.dropzone-hint {
font-size: 0.9rem;
color: #6c757d;
margin-top: 5px;
}
/* -------------------------------------- */
/* Style pour le Responsive (Mobile) */
/* -------------------------------------- */
@media (max-width: 576px) {
.dropzone {
padding: 25px;
}
.dropzone-icon {
font-size: 2.5rem;
}
.dropzone-text {
font-size: 1rem;
}
.file-list {
font-size: 0.9rem;
}
}
/* Style pour la liste de fichiers */
.file-list {
list-style-type: none;
padding: 0;
margin-top: 15px;
text-align: left;
}
.file-list li {
padding: 8px 10px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.file-list li:last-child {
border-bottom: none;
}
.file-icon {
margin-right: 8px;
color: #007bff;
}
/* Style pour le bouton de copie */
#copy-url-button {
width: 50px; /* Fixer la largeur du bouton de copie */
}
/* Nouveau style pour l'animation de chargement */
.spinner-border {
display: inline-block;
width: 1rem;
height: 1rem;
vertical-align: middle;
border: .2em solid currentColor;
border-right-color: transparent;
border-radius: 50%;
animation: spinner-border .75s linear infinite;
}
@keyframes spinner-border {
to { transform: rotate(360deg); }
}
/* -------------------------------------- */
/* Styles du Mode Sombre */
/* -------------------------------------- */
/* Le corps principal devient sombre */
body.dark-mode {
background-color: #1a1a1a;
color: #f1f1f1;
}
/* Conteneurs et cartes (e.g., action-zone, url-container) */
.dark-mode .bg-white {
background-color: #2c2c2c !important;
color: #f1f1f1;
}
/* Zone de Drop */
.dark-mode .dropzone {
border-color: #00bcd4; /* Cyan pour le contraste */
background-color: #2c2c2c;
}
.dark-mode .dropzone.dragover {
background-color: #404040;
border-color: #00e5ff;
}
.dark-mode .dropzone-icon, .dark-mode .file-icon {
color: #00bcd4;
}
.dark-mode .dropzone-text {
color: #f1f1f1;
}
.dark-mode .dropzone-hint {
color: #ccc;
}
/* Liste de fichiers */
.dark-mode .file-list li {
border-bottom: 1px solid #444;
}
/* Statut de téléversement */
.dark-mode .text-muted {
color: #b0b0b0 !important;
}
/* Couleurs des URL (Ajustement pour le fond sombre) */
.dark-mode #permanent-url-container {
background-color: #0f5132 !important; /* Vert foncé */
border-color: #198754 !important;
}
.dark-mode .text-success {
color: #92b8aa !important; /* Vert clair */
}
.dark-mode #temp-url-container {
background-color: #664d03 !important; /* Jaune foncé */
border-color: #ffc107 !important;
}
.dark-mode .text-warning {
color: #ffd700 !important; /* Jaune clair */
}
</style>
<div class="d-flex justify-content-end p-3">
<button id="theme-toggle" class="btn btn-outline-secondary" title="Changer le thème">
<span id="theme-icon" class="material-symbols-rounded">dark_mode</span>
</button>
</div>
<input type="file" id="file-input" multiple style="display: none;">
<div class="container my-5 mx-auto px-4 lg:px-0">
<div class="col-12 col-lg-8 mx-auto">
<h2 class="text-center mb-4 text-3xl font-bold"> Déploiement Statique Instantané</h2>
<div id="drop-zone" class="dropzone" data-is-dropping="false">
<span class="material-symbols-rounded dropzone-icon">cloud_upload</span>
<p class="dropzone-text">Glissez et déposez vos fichiers ici (HTML, CSS, JS, Images)</p>
<p class="dropzone-hint">Ou cliquez pour sélectionner des fichiers. Un fichier **index.html** est requis.</p>
<button id="select-files-button" class="btn bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded mt-3">Sélectionner des Fichiers</button>
</div>
<div id="file-upload-status" class="mt-3 text-muted text-center">
Aucun fichier téléversé.
</div>
<ul id="uploaded-file-list" class="file-list p-3 border rounded shadow-sm bg-white" style="display: none;">
</ul>
<div id="action-zone" class="mt-4 p-4 border rounded shadow-sm bg-white">
<div class="flex flex-wrap justify-between items-center space-y-2 md:space-y-0">
<button id="launch-button" class="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded flex items-center" disabled>
<span class="material-symbols-rounded align-middle me-1">open_in_new</span> Lancer l'Aperçu (Temp)
</button>
<button id="deploy-button" class="bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-4 rounded flex items-center" disabled>
<span class="material-symbols-rounded align-middle me-1">cloud_upload</span> Déployer le Site Web (Permanent)
</button>
<button id="clear-button" class="bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded flex items-center" disabled>
<span class="material-symbols-rounded align-middle me-1">delete_forever</span> Vider
</button>
</div>
</div>
<div id="permanent-url-container" class="mt-4 p-3 bg-success-light border rounded" style="display: none; background-color: #d1e7dd !important; border-color: #badbcc !important;">
<label class="font-bold d-block text-success" style="color: #0f5132 !important;">
<span class="material-symbols-rounded align-middle me-1">link</span> Lien Permanent du Site
</label>
<div class="flex mt-2">
<input type="text" id="permanent-url-display" class="w-full p-2 border rounded-l" readonly value="">
<button class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-3 rounded-r" type="button" id="copy-permanent-button" title="Copier le lien permanent">
<span class="material-symbols-rounded">content_copy</span>
</button>
</div>
<small class="mt-1 d-block" style="color: #0f5132 !important;"> ✅ Ce lien est sauvegardé sur Hugging Face et est permanent. </small>
</div>
<div id="temp-url-container" class="mt-4 p-3 bg-light border rounded" style="display: none; background-color: #fff3cd !important; border-color: #ffc107 !important;">
<label class="font-bold d-block text-warning" style="color: #664d03 !important;">
<span class="material-symbols-rounded align-middle me-1">share</span> Lien d'Aperçu Temporaire
</label>
<div class="flex mt-2">
<input type="text" id="temp-url-display" class="w-full p-2 border rounded-l" readonly value="{{ temp_launch_url if temp_launch_url else '' }}">
<button class="bg-gray-200 hover:bg-gray-300 text-gray-800 font-bold py-2 px-3 rounded-r" type="button" id="copy-temp-button" title="Copier le lien temporaire">
<span class="material-symbols-rounded">content_copy</span>
</button>
</div>
<small class="mt-1 d-block"> ⚠️ Ce lien est temporaire. Cliquez sur "**Déployer le Site Web**" pour le rendre permanent. </small>
</div>
</div>
</div>
<script>
// Récupération des éléments du DOM
const body = document.body;
const themeToggle = document.getElementById('theme-toggle');
const themeIcon = document.getElementById('theme-icon');
const dropZone = document.getElementById('drop-zone');
const fileInput = document.getElementById('file-input');
const selectFilesButton = document.getElementById('select-files-button');
const uploadStatus = document.getElementById('file-upload-status');
const uploadedFileList = document.getElementById('uploaded-file-list');
const launchButton = document.getElementById('launch-button');
const clearButton = document.getElementById('clear-button');
const deployButton = document.getElementById('deploy-button');
const permanentUrlContainer = document.getElementById('permanent-url-container');
const permanentUrlInput = document.getElementById('permanent-url-display');
const copyPermanentButton = document.getElementById('copy-permanent-button');
const tempUrlContainer = document.getElementById('temp-url-container');
const tempUrlInput = document.getElementById('temp-url-display');
const copyTempButton = document.getElementById('copy-temp-button');
let selectedFiles = []; // Stocke les fichiers à téléverser
// Données injectées par Flask (Assurez-vous que votre route Flask passe ces variables)
let currentTempUrl = '{{ temp_launch_url if temp_launch_url else "" }}';
let currentPermanentUrl = '{{ permanent_deployment_url if permanent_deployment_url else "" }}';
let initialIndexFilePresent = {{ 'true' if 'index.html' in files else 'false' }};
let initialFileCount = {{ files|length }};
const deployId = '{{ deploy_id }}';
const initialFiles = JSON.parse('{{ files | tojson | safe }}');
// État initial de l'interface
let currentIndexFilePresent = initialIndexFilePresent;
// -------------------------------------------------------------
// --- Fonctions d'Utilitaires et de Mise à Jour de l'Interface ---
// -------------------------------------------------------------
/** Affiche la liste des fichiers téléversés ou en attente */
function renderFileList(fileList) {
uploadedFileList.innerHTML = '';
if (fileList.length === 0) {
uploadedFileList.style.display = 'none';
return;
}
fileList.forEach(filename => {
const li = document.createElement('li');
li.innerHTML = `<span class="material-symbols-rounded file-icon">file_present</span> ${filename}`;
uploadedFileList.appendChild(li);
});
uploadedFileList.style.display = 'block';
}
/** Met à jour l'état des boutons et des URL */
function updateUIState() {
clearButton.disabled = initialFileCount === 0 && selectedFiles.length === 0;
if (currentPermanentUrl) {
// État final: URL permanente
permanentUrlInput.value = currentPermanentUrl;
permanentUrlContainer.style.display = 'block';
tempUrlContainer.style.display = 'none';
deployButton.disabled = true;
deployButton.innerHTML = '<span class="material-symbols-rounded align-middle me-1">cloud_done</span> Déployé !';
deployButton.classList.remove('bg-green-500', 'hover:bg-green-600');
deployButton.classList.add('bg-gray-500', 'cursor-not-allowed');
launchButton.disabled = false;
uploadStatus.textContent = `Site déployé (${initialFileCount} fichiers).`;
uploadStatus.className = 'mt-3 text-success fw-bold text-center';
} else if (currentTempUrl && currentIndexFilePresent) {
// État temporaire: prêt à être déployé
tempUrlInput.value = currentTempUrl;
tempUrlContainer.style.display = 'block';
permanentUrlContainer.style.display = 'none';
deployButton.disabled = false;
deployButton.innerHTML = '<span class="material-symbols-rounded align-middle me-1">cloud_upload</span> Déployer le Site Web (Permanent)';
deployButton.classList.remove('bg-gray-500', 'cursor-not-allowed');
deployButton.classList.add('bg-green-500', 'hover:bg-green-600');
launchButton.disabled = false;
uploadStatus.textContent = `Fichiers téléversés temporairement (${initialFileCount} fichiers. Index.html: Oui). Cliquez sur "Déployer" pour rendre permanent.`;
uploadStatus.className = 'mt-3 text-info fw-bold text-center';
} else {
// Aucun fichier ou index.html manquant
tempUrlContainer.style.display = 'none';
permanentUrlContainer.style.display = 'none';
deployButton.disabled = true;
deployButton.innerHTML = '<span class="material-symbols-rounded align-middle me-1">cloud_upload</span> Déployer le Site Web (Permanent)';
deployButton.classList.remove('bg-green-500', 'hover:bg-green-600');
deployButton.classList.add('bg-gray-500', 'cursor-not-allowed');
launchButton.disabled = true;
if (selectedFiles.length > 0) {
uploadStatus.textContent = `Prêt à téléverser (${selectedFiles.length} fichiers). Index.html: ${currentIndexFilePresent ? 'Oui' : 'Non'}.`;
uploadStatus.className = 'mt-3 text-muted text-center';
} else {
uploadStatus.textContent = "Aucun fichier téléversé.";
uploadStatus.className = 'mt-3 text-muted text-center';
}
}
}
/** Initialise l'état au chargement de la page */
function updateInitialState() {
renderFileList(initialFiles);
updateUIState();
loadTheme(); // Appliquer le thème sauvegardé
}
// -------------------------------------------------------------
// --- Logique du Mode Sombre/Clair ---
// -------------------------------------------------------------
/** Change l'icône et la classe du corps */
function toggleTheme() {
const isDarkMode = body.classList.toggle('dark-mode');
if (isDarkMode) {
themeIcon.textContent = 'light_mode';
localStorage.setItem('theme', 'dark');
} else {
themeIcon.textContent = 'dark_mode';
localStorage.setItem('theme', 'light');
}
}
/** Charge le thème depuis le localStorage */
function loadTheme() {
const savedTheme = localStorage.getItem('theme') || 'light';
if (savedTheme === 'dark') {
body.classList.add('dark-mode');
themeIcon.textContent = 'light_mode';
} else {
body.classList.remove('dark-mode');
themeIcon.textContent = 'dark_mode';
}
}
themeToggle.addEventListener('click', toggleTheme);
// -------------------------------------------------------------
// --- Logique de Glisser-Déposer et Sélection de Fichiers ---
// -------------------------------------------------------------
selectFilesButton.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
handleFiles(e.target.files);
});
// Prévention du comportement par défaut
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults (e) {
e.preventDefault();
e.stopPropagation();
}
// Gestion de l'état 'dragover'
dropZone.addEventListener('dragenter', highlight, false);
dropZone.addEventListener('dragover', highlight, false);
dropZone.addEventListener('dragleave', unhighlight, false);
dropZone.addEventListener('drop', unhighlight, false);
function highlight() {
dropZone.classList.add('dragover');
}
function unhighlight() {
dropZone.classList.remove('dragover');
}
dropZone.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
let dt = e.dataTransfer;
let files = dt.files;
handleFiles(files);
}
/** Traite la liste des fichiers sélectionnés ou déposés */
function handleFiles(files) {
selectedFiles = Array.from(files);
// Vérification de index.html
currentIndexFilePresent = selectedFiles.some(file => file.name.toLowerCase() === 'index.html');
// Affichage des fichiers en attente
const filenames = selectedFiles.map(file => file.name);
renderFileList(filenames);
// Mise à jour de l'état (prêt à téléverser)
currentTempUrl = '';
currentPermanentUrl = '';
initialFileCount = selectedFiles.length; // Pour l'état actuel
updateUIState();
// Lancer le téléversement temporaire immédiatement
if (selectedFiles.length > 0) {
uploadFilesToTempEndpoint(selectedFiles);
}
}
// -------------------------------------------------------------
// --- Logique du Téléversement TEMPORAIRE vers /upload-static ---
// -------------------------------------------------------------
async function uploadFilesToTempEndpoint(filesToUpload) {
if (filesToUpload.length === 0) return;
// 1. Préparer l'animation et l'état
uploadStatus.className = 'mt-3 text-warning fw-bold text-center';
uploadStatus.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> Téléversement des fichiers temporaires...';
launchButton.disabled = true;
deployButton.disabled = true;
const formData = new FormData();
filesToUpload.forEach(file => {
formData.append('files[]', file); // 'files[]' doit correspondre à la manière dont Flask reçoit les fichiers multiples
});
try {
const response = await fetch('/upload-static', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.status === 'success') {
// SUCCÈS du téléversement temporaire
currentTempUrl = data.launch_url;
currentIndexFilePresent = data.index_present;
initialFileCount = data.file_count;
// L'état initial du déploiement est maintenant basé sur les données du backend
initialFiles.length = 0; // Vider la liste affichée pour les fichiers fraichement uploadés
filesToUpload.forEach(file => initialFiles.push(file.name));
renderFileList(initialFiles);
updateUIState(); // Passer à l'état "Temporaire"
uploadStatus.className = 'mt-3 text-success fw-bold text-center';
uploadStatus.textContent = data.message;
} else {
// ERREUR
currentTempUrl = '';
currentIndexFilePresent = false;
updateUIState(); // Revenir à l'état "Aucun fichier" ou "Index manquant"
uploadStatus.className = 'mt-3 text-danger fw-bold text-center';
uploadStatus.textContent = `Échec du téléversement: ${data.message}`;
}
} catch (error) {
console.error('Erreur réseau/inattendue lors du téléversement temporaire:', error);
currentTempUrl = '';
currentIndexFilePresent = false;
updateUIState();
uploadStatus.className = 'mt-3 text-danger fw-bold text-center';
uploadStatus.textContent = `Erreur réseau ou inattendue lors du téléversement temporaire: ${error.message}`;
}
}
// -------------------------------------------------------------
// --- Logique du Bouton de Déploiement Permanent (Commit) ---
// -------------------------------------------------------------
deployButton.addEventListener('click', async () => {
// 1. Préparer l'animation et désactiver le bouton
deployButton.disabled = true;
deployButton.innerHTML = '<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span> Déploiement en cours...';
uploadStatus.className = 'mt-3 text-info fw-bold text-center';
uploadStatus.textContent = 'Persistance des fichiers sur Hugging Face et Baserow en cours... (Cela peut prendre quelques instants)';
// 2. Appel AJAX à la route de commit
try {
const response = await fetch('/commit_static_deployment', {
method: 'POST',
});
const data = await response.json();
// 3. Traiter la réponse
if (data.success) {
// SUCCÈS
currentPermanentUrl = data.url; // Mise à jour de l'URL permanente
currentTempUrl = '';
updateUIState(); // Passer à l'état "Permanent"
uploadStatus.className = 'mt-3 text-success fw-bold text-center';
uploadStatus.textContent = data.message;
} else {
// ERREUR
// Rétablir les boutons à l'état "temporaire" (si l'échec n'est pas critique)
uploadStatus.className = 'mt-3 text-danger fw-bold text-center';
uploadStatus.textContent = `Échec du déploiement: ${data.message}`;
updateUIState(); // Rétablit les boutons en se basant sur currentTempUrl
}
} catch (error) {
console.error('Erreur réseau/inattendue:', error);
uploadStatus.className = 'mt-3 text-danger fw-bold text-center';
uploadStatus.textContent = `Erreur réseau ou inattendue lors du déploiement: ${error.message}`;
updateUIState(); // Rétablit les boutons
}
});
// -------------------------------------------------------------
// --- Logique de Copie d'URL et Lancement ---
// -------------------------------------------------------------
function setupCopyButton(button, input) {
button.addEventListener('click', () => {
if (input.value) {
input.select();
input.setSelectionRange(0, 99999);
navigator.clipboard.writeText(input.value)
.then(() => {
button.innerHTML = '<span class="material-symbols-rounded">done</span>';
button.classList.remove('bg-gray-200');
button.classList.add('bg-green-500', 'text-white');
setTimeout(() => {
button.innerHTML = '<span class="material-symbols-rounded">content_copy</span>';
button.classList.remove('bg-green-500', 'text-white');
button.classList.add('bg-gray-200');
}, 2000);
})
.catch(err => {
console.error('Erreur lors de la copie: ', err);
alert('Erreur lors de la copie du lien.');
});
}
});
}
setupCopyButton(copyTempButton, tempUrlInput);
setupCopyButton(copyPermanentButton, permanentUrlInput);
// Gère l'action du bouton Lancer (Ouvre l'URL temporaire ou permanente)
launchButton.addEventListener('click', () => {
const url = permanentUrlInput.value || tempUrlInput.value;
if (url) {
window.open(url, '_blank');
}
});
// Gère le bouton Vider (doit être implémenté au backend pour la suppression du dossier)
clearButton.addEventListener('click', () => {
// Si l'utilisateur clique sur Vider, on reset l'état local.
// NOTE: La suppression du dossier temporaire sur le serveur (route /clear_deployment) doit être implémentée au backend.
// Réinitialisation de l'état de l'interface
selectedFiles = [];
currentTempUrl = '';
currentPermanentUrl = '';
initialFileCount = 0;
currentIndexFilePresent = false;
initialFiles.length = 0;
renderFileList(initialFiles);
updateUIState();
// Optionnel : Ajouter un appel AJAX à une route de nettoyage backend ici
// fetch('/clear_deployment', { method: 'POST' });
alert("L'état local a été vidé. Pour supprimer le dossier temporaire du serveur, la route '/clear_deployment' doit être implémentée dans Flask.");
});
// Initialise l'état au chargement de la page
window.onload = updateInitialState;
</script>
</body>
</html>