F1ATB forum
Petit prog pour esp32 - Version imprimable

+- F1ATB forum (https://f1atb.fr/forum_f1atb)
+-- Forum : Forum de F1ATB (https://f1atb.fr/forum_f1atb/forum-3.html)
+--- Forum : Routeur Photovoltaïque (https://f1atb.fr/forum_f1atb/forum-4.html)
+--- Sujet : Petit prog pour esp32 (/thread-1485.html)

Pages : 1 2 3 4 5


RE: Petit prog pour esp32 - 59jag - 30-05-2025

bonjour 

je voulais faire un peu pres la meme que ton html.
je donc récupérer le tiens pour faire quelques modif.
enregistrement de la config dans le localstorage.
ajustement du zoom par rapport taille ecran

https://drive.google.com/file/d/12HfRLeThuHE79mP5-TFDGhlNyRSfLxTe/view?usp=drivesdk

Code :
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Multi-Interfaces</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            background: #222;
        }

        /* Styles pour le mode configuration */
        .config-mode {
            background-color: #f5f5f5 !important;
        }

        .config-container {
            display: none;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }

        .config-mode .config-container {
            display: block;
        }

        .config-panel {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }

        .ip-list {
            margin-bottom: 20px;
        }

        .ip-group {
            margin-bottom: 15px;
            padding: 15px;
            background-color: #f9f9f9;
            border-radius: 5px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .ip-number {
            font-weight: bold;
            color: #555;
            min-width: 30px;
        }

        input[type="text"] {
            flex: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
        }

        input[type="text"]:focus {
            outline: none;
            border-color: #4CAF50;
        }

        .remove-btn {
            background-color: #f44336;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .remove-btn:hover {
            background-color: #d32f2f;
        }

        .add-btn {
            background-color: #2196F3;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin-bottom: 20px;
            width: 100%;
        }

        .add-btn:hover {
            background-color: #1976D2;
        }

        .button-group {
            margin-top: 30px;
            display: flex;
            gap: 10px;
            justify-content: center;
        }

        button {
            padding: 12px 30px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .save-btn {
            background-color: #4CAF50;
            color: white;
        }

        .save-btn:hover {
            background-color: #45a049;
        }

        .message {
            margin-top: 20px;
            padding: 10px;
            border-radius: 5px;
            text-align: center;
            display: none;
        }

        .success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .layout-preview {
            margin-top: 20px;
            padding: 15px;
            background-color: #e3f2fd;
            border-radius: 5px;
            text-align: center;
            font-size: 14px;
            color: #1976D2;
        }

        .help-text {
            font-size: 12px;
            color: #666;
            margin-top: 15px;
            padding: 10px;
            background-color: #f0f0f0;
            border-radius: 5px;
            line-height: 1.6;
        }

        /* Styles pour le mode dashboard */
        .dashboard-container {
            display: none;
            height: 100vh;
            gap: 2px;
            background: #222;
        }

        .dashboard-mode .dashboard-container {
            display: grid;
        }

        .frame-container {
            position: relative;
            overflow: hidden;
            background: white;
        }

        .frame-wrapper {
            width: 100%;
            height: 100%;
            transform-origin: top left;
            transition: transform 0.3s ease;
        }

        iframe {
            width: 100%;
            height: 100%;
            border: none;
            background: white;
        }

        /* Icône de configuration fixe */
        .config-icon {
            position: fixed;
            top: 5px;
            left: 5px;
            z-index: 1000;
            color: rgba(255, 255, 255, 0.8);
            font-size: 5vw;
            cursor: pointer;
            transition: all 0.3s ease;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
            user-select: none;
            display: none;
        }

        .dashboard-mode .config-icon {
            display: block;
        }

        .config-icon:hover {
            color: #FF9800;
            transform: rotate(45deg) scale(1.1);
        }




        /* Fenêtre popup de configuration */
        .config-popup {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            z-index: 2000;
            overflow-y: auto;
        }

        .config-popup-content {
            background: white;
            margin: 50px auto;
            padding: 30px;
            border-radius: 10px;
            max-width: 800px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
            position: relative;
            animation: popupSlideIn 0.3s ease-out;
        }

@keyframes popupSlideIn {
            from {
                opacity: 0;
                transform: translateY(-50px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .close-popup {
            position: absolute;
            top: 15px;
            right: 20px;
            font-size: 30px;
            color: #999;
            cursor: pointer;
            transition: color 0.3s;
        }

        .close-popup:hover {
            color: #333;
        }

        /* Contrôles de zoom */
        .zoom-controls {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(0,0,0,0.8);
            padding: 5px;
            border-radius: 3px;
            z-index: 10;
            display: flex;
            gap: 5px;
        }

        .zoom-btn {
            width: 25px;
            height: 25px;
            border: none;
            background: #444;
            color: white;
            cursor: pointer;
            border-radius: 3px;
            font-size: 16px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .zoom-btn:hover {
            background: #666;
        }

        .zoom-fit {
            background: #2196F3;
            width: auto;
            min-width: 25px;
            padding: 0 8px;
            font-size: 11px;
        }

        .zoom-fit:hover {
            background: #1976D2;
        }

        .zoom-value {
            color: white;
            font-family: Arial, sans-serif;
            font-size: 12px;
            min-width: 45px;
            text-align: center;
            display: flex;
            align-items: center;
        }

        /* Label optionnel */
        .frame-label {
            position: absolute;
            top: 5px;
            left: 5px;
            background: rgba(0,0,0,0.7);
            color: white;
            padding: 5px 10px;
            font-family: Arial, sans-serif;
            font-size: 12px;
            border-radius: 3px;
            z-index: 10;
            pointer-events: none;
            max-width: 70%;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }


        /* Message d'accueil */
        .welcome-message {
            text-align: center;
            padding: 40px;
            background-color: #e3f2fd;
            border-radius: 10px;
            margin-bottom: 30px;
        }

        .welcome-message h2 {
            color: #1976D2;
            margin-bottom: 10px;
        }

        /* Message d'erreur iframe */
        .iframe-error {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #f8d7da;
            color: #721c24;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
            max-width: 80%;
            z-index: 20;
            display: none;
        }

        .iframe-error h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
        }

        .iframe-error p {
            margin: 5px 0;
            font-size: 14px;
        }

        .iframe-error code {
            background: rgba(0,0,0,0.1);
            padding: 2px 5px;
            border-radius: 3px;
            font-size: 12px;
        }


        /* Boutons d'action de configuration */
        .config-actions {
            display: flex;
            gap: 10px;
            margin-top: 20px;
            flex-wrap: wrap;
        }

        .clear-btn {
            background-color: #F44336;
        }

        .clear-btn:hover {
            background-color: #D32F2F;
        }
    </style>
</head>
<body class="config-mode">
    <!-- Icône de configuration fixe -->
    <div class="config-icon" onclick="openConfigPopup()" title="Configuration">
        ⚙️
    </div>

    <!-- Mode Configuration -->
    <div class="config-container">
        <div class="config-panel">
            <h1>Dashboard Multi-Interfaces</h1>

            <div id="welcomeMessage" class="welcome-message">
                <h2>Bienvenue !</h2>
                <p>
                    Configurez vos adresses ci-dessous pour commencer.
                </p>
            </div>

            <div id="ipList" class="ip-list"></div>

            <button class="add-btn" onclick="addIP()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="layoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveAndShowDashboard()">Sauvegarder et afficher</button>
            </div>

            <div class="config-actions">
                <button class="save-btn clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="message" class="message"></div>
        </div>
    </div>

    <!-- Popup de configuration -->
    <div class="config-popup" id="configPopup">
        <div class="config-popup-content">
            <span class="close-popup" onclick="closeConfigPopup()">&times;</span>
            <h1>Configuration Dashboard</h1>

            <div id="popupIpList" class="ip-list"></div>

            <button class="add-btn" onclick="addIPPopup()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="popupLayoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveConfigFromPopup()">Sauvegarder et appliquer</button>
                <button class="save-btn" style="background-color: #666;" onclick="closeConfigPopup()">Annuler</button>
            </div>

            <div class="config-actions">
                <button class="save-btn clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="popupMessage" class="message"></div>
        </div>
    </div>

    <!-- Mode Dashboard -->
    <div id="dashboardContainer" class="dashboard-container"></div>


    <script>
        // Configuration des adresses - Chargée depuis localStorage
        let addresses = [];
        let zoomLevels = {};
        let currentMode = 'config';
        let autoFitEnabled = true;
        let isPopupMode = false;

        // Clé de stockage localStorage
        const STORAGE_KEY = 'dashboard_config';

        // Initialisation
        window.onload = function() {
            loadConfig();

            if (addresses.length > 0 && addresses[0] !== '') {
                document.getElementById('welcomeMessage').style.display = 'none';
                // Aller directement au dashboard si configuré
                showDashboard();
            } else {
                renderIPList();
            }
        };

        // Charger la configuration depuis localStorage
        function loadConfig() {
            try {
                const savedConfig = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                if (Array.isArray(savedConfig) && savedConfig.length > 0) {
                    addresses = savedConfig;
                } else {
                    addresses = [''];
                }
            } catch (e) {
                console.error('Erreur lors du chargement de la configuration:', e);
                addresses = [''];
            }
        }

        // Sauvegarder la configuration dans localStorage
        function saveConfig() {
            try {
                const cleanAddresses = addresses.filter(addr => addr.trim() !== '');
                localStorage.setItem(STORAGE_KEY, JSON.stringify(cleanAddresses));
                return true;
            } catch (e) {
                console.error('Erreur lors de la sauvegarde:', e);
                showMessage('Erreur lors de la sauvegarde de la configuration', 'error');
                return false;
            }
        }

        // Ouvrir la popup de configuration
        function openConfigPopup() {
            isPopupMode = true;
            addresses = [...addresses]; // Copie pour éviter les modifications directes
            renderIPListPopup();
            document.getElementById('configPopup').style.display = 'block';
            document.body.style.overflow = 'hidden'; // Désactiver le scroll du body
        }

        // Fermer la popup de configuration
        function closeConfigPopup() {
            isPopupMode = false;
            document.getElementById('configPopup').style.display = 'none';
            document.body.style.overflow = 'auto'; // Réactiver le scroll du body
            loadConfig(); // Recharger la config originale
        }

        // Afficher la liste des adresses (mode principal)
        function renderIPList() {
            const container = document.getElementById('ipList');
            renderIPListInContainer(container, false);
        }

        // Afficher la liste des adresses (popup)
        function renderIPListPopup() {
            const container = document.getElementById('popupIpList');
            renderIPListInContainer(container, true);
        }

        // Fonction générique pour rendre la liste d'IPs
        function renderIPListInContainer(container, isPopup) {
            container.innerHTML = '';

            addresses.forEach((address, index) => {
                const div = document.createElement('div');
                div.className = 'ip-group';
                div.innerHTML = `
                <span class="ip-number">${index + 1}.</span>
                <input type="text" id="${isPopup ? 'popup': ''}address${index}" value="${address}" placeholder="Ex: 192.168.1.100, example.com, https://app.example.com">
                ${addresses.length > 1 ? `<button class="remove-btn" onclick="${isPopup ? 'removeIPPopup': 'removeIP'}(${index})">Supprimer</button>`: ''}
                `;
                container.appendChild(div);
            });

            updateLayoutPreview(isPopup);
        }

        // Ajouter une adresse
        function addIP() {
            addresses.push('');
            renderIPList();
        }

        // Ajouter une adresse (popup)
        function addIPPopup() {
            addresses.push('');
            renderIPListPopup();
        }

        // Supprimer une adresse
        function removeIP(index) {
            addresses.splice(index, 1);
            renderIPList();
        }

        // Supprimer une adresse (popup)
        function removeIPPopup(index) {
            addresses.splice(index, 1);
            renderIPListPopup();
        }

        // Mettre à jour l'aperçu
        function updateLayoutPreview(isPopup = false) {
            const preview = document.getElementById(isPopup ? 'popupLayoutPreview': 'layoutPreview');
            const count = addresses.length;

            let layout = '';
            if (count === 1) {
                layout = 'Plein écran';
            } else if (count === 2) {
                layout = '2 colonnes côte à côte';
            } else if (count === 3) {
                layout = '1 grande vue à gauche + 2 petites à droite';
            } else if (count === 4) {
                layout = 'Grille 2x2';
            } else if (count <= 6) {
                layout = 'Grille 2x3';
            } else if (count <= 9) {
                layout = 'Grille 3x3';
            } else {
                layout = `Grille ${Math.ceil(Math.sqrt(count))}x${Math.ceil(count / Math.ceil(Math.sqrt(count)))}`;
            }

            preview.innerHTML = `<strong>Disposition :</strong> ${count} interface${count > 1 ? 's': ''} - ${layout}`;
        }

        // Sauvegarder la configuration depuis la popup
        function saveConfigFromPopup() {
            // Récupérer et valider les adresses depuis la popup
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const addressValue = document.getElementById(`popupaddress${i}`).value.trim();
                if (addressValue) {
                    const validation = validateAndFormatAddress(addressValue);
                    if (!validation.valid) {
                        showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error', true);
                        return;
                    }
                    newAddresses.push(addressValue);
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error', true);
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée avec succès !', 'success', true);
                setTimeout(() => {
                    closeConfigPopup();
                    showDashboard();
                }, 1000);
            }
        }

        // Sauvegarder et afficher le dashboard (mode principal)
        function saveAndShowDashboard() {
            // Récupérer et valider les adresses
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const addressInput = document.getElementById(`address${i}`);
                const addressValue = addressInput ? addressInput.value.trim(): addresses[i];

                if (addressValue) {
                    const validation = validateAndFormatAddress(addressValue);
                    if (!validation.valid) {
                        showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                        return;
                    }
                    newAddresses.push(addressValue);
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error');
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée !', 'success');
                setTimeout(showDashboard, 500);
            }
        }

        // Effacer toute la configuration
        function clearConfig() {
            if (confirm('Êtes-vous sûr de vouloir effacer toute la configuration ?')) {
                localStorage.removeItem(STORAGE_KEY);
                addresses = [''];

                if (isPopupMode) {
                    renderIPListPopup();
                } else {
                    renderIPList();
                }

                showMessage('Configuration effacée', 'success', isPopupMode);
            }
        }

        // Valider et formater une adresse
        function validateAndFormatAddress(address) {
            address = address.trim();

            if (!address) {
                return {
                    valid: false,
                    error: "L'adresse ne peut pas être vide"
                };
            }

            if (address.match(/^https?:\/\//)) {
                return {
                    valid: true,
                    formatted: address,
                    display: address
                };
            }

            const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::\d{1,5})?$/;
            if (ipPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            const domainPattern = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(:\d{1,5})?(\/.*)?$/;
            const localhostPattern = /^localhost(:\d{1,5})?(\/.*)?$/;

            if (domainPattern.test(address) || localhostPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            if (/^[a-zA-Z0-9-]+(:\d{1,5})?(\/.*)?$/.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            return {
                valid: false,
                error: "Format d'adresse non valide"
            };
        }

        // Afficher un message
        function showMessage(text, type, inPopup = false) {
            const messageEl = document.getElementById(inPopup ? 'popupMessage': 'message');
            messageEl.textContent = text;
            messageEl.className = 'message ' + type;
            messageEl.style.display = 'block';

            setTimeout(() => {
                messageEl.style.display = 'none';
            }, 3000);
        }

        // Calculer la disposition
        function calculateGridLayout(count) {
            if (count === 1) return {
                cols: 1,
                rows: 1,
                special: 'single'
            };
            if (count === 2) return {
                cols: 2,
                rows: 1,
                special: 'two-columns'
            };
            if (count === 3) return {
                cols: 2,
                rows: 2,
                special: 'three-special'
            };
            if (count === 4) return {
                cols: 2,
                rows: 2,
                special: 'grid'
            };

            const cols = Math.ceil(Math.sqrt(count));
            const rows = Math.ceil(count / cols);
            return {
                cols,
                rows,
                special: 'grid'
            };
        }

        function calculateOptimalZoom(frameContainer) {
            const containerWidth = frameContainer.clientWidth;

            // Utiliser la largeur actuelle en tenant compte de l'orientation
            const referenceWidth = Math.max(window.innerWidth, window.innerHeight);

            const optimalZoom = containerWidth / referenceWidth;

            return Math.max(0.2, Math.min(2, optimalZoom));
        }



        // Appliquer le zoom automatique à une iframe
        function autoFitFrame(frameNum) {
            const frameContainer = document.querySelector(`#frame${frameNum}`).parentElement;
            const optimalZoom = calculateOptimalZoom(frameContainer);

            zoomLevels[frameNum] = optimalZoom;
            updateZoom(frameNum);
        }

        // Appliquer le zoom automatique à toutes les iframes
        function autoFitAll() {
            for (let i = 1; i <= addresses.length; i++) {
                autoFitFrame(i);
            }
        }

        // Afficher le dashboard
        function showDashboard() {
            // Valider les adresses
            const validatedAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const validation = validateAndFormatAddress(addresses[i]);
                if (!validation.valid) {
                    showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                    return;
                }
                validatedAddresses.push(validation);
            }

            // Construire le dashboard avec les adresses formatées
            buildDashboard(validatedAddresses);

            // Basculer en mode dashboard
            document.body.className = 'dashboard-mode';
            currentMode = 'dashboard';

            // Appliquer le zoom automatique après un délai
            setTimeout(() => {
                if (autoFitEnabled) {
                    for (let i = 1; i <= addresses.length; i++) {
                        autoFitFrame(i);
                    }
                }
            },
                500);
        }

        // Construire le dashboard
        function buildDashboard(validatedAddresses) {
            const container = document.getElementById('dashboardContainer');
            const layout = calculateGridLayout(validatedAddresses.length);

            // Appliquer les styles de grille
            if (layout.special === 'single') {
                container.style.gridTemplateColumns = '1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'two-columns') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'three-special') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr 1fr';
            } else {
                container.style.gridTemplateColumns = `repeat(${layout.cols}, 1fr)`;
                container.style.gridTemplateRows = `repeat(${layout.rows}, 1fr)`;
            }

            // Créer les conteneurs
            container.innerHTML = '';
            validatedAddresses.forEach((addressInfo, index) => {
                const frameDiv = document.createElement('div');
                frameDiv.className = 'frame-container';

                // Style spécial pour 3 interfaces
                if (layout.special === 'three-special' && index === 0) {
                    frameDiv.style.gridRow = 'span 2';
                }

                frameDiv.innerHTML = `
                <span class="frame-label" title="${addressInfo.display}">${addressInfo.display}</span>
                <div class="zoom-controls">
                <button class="zoom-btn" onclick="zoom(${index + 1}, -0.1)">−</button>
                <span class="zoom-value" id="zoom${index + 1}">100%</span>
                <button class="zoom-btn" onclick="zoom(${index + 1}, 0.1)">+</button>
                <button class="zoom-btn zoom-fit" onclick="autoFitFrame(${index + 1})" title="Ajuster à l'écran">FIT</button>
                </div>
                <div class="frame-wrapper" id="frame${index + 1}">
                <iframe
                src="${addressInfo.formatted}"
                sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals allow-downloads allow-presentation allow-top-navigation"
                referrerpolicy="no-referrer"
                id="iframe${index + 1}"
                onerror="handleIframeError(${index + 1}, '${addressInfo.display}')"
                ></iframe>
                </div>
                <div class="iframe-error" id="error${index + 1}">
                <h3>⚠️ Impossible de charger ${addressInfo.display}</h3>
                <p>Cette interface ne peut pas être affichée dans une iframe.</p>
                <p><small>Pour Home Assistant, ajoutez dans configuration.yaml :</small></p>
                <code>http:<br>&nbsp;&nbsp;use_x_frame_options: false</code>
                <p style="margin-top: 15px;">
                <a href="${addressInfo.formatted}" target="_blank" style="color: #0056b3;">Ouvrir dans un nouvel onglet →</a>
                </p>
                </div>
                `;

                container.appendChild(frameDiv);
            });

            // Initialiser les niveaux de zoom
            zoomLevels = {};
            for (let i = 1; i <= validatedAddresses.length; i++) {
                zoomLevels[i] = 1;
            }
        }

        // Retourner à la configuration
        function showConfig() {
            document.body.className = 'config-mode';
            currentMode = 'config';
            renderIPList();
        }

        // Fonctions de zoom
        function zoom(frameNum, delta) {
            zoomLevels[frameNum] = Math.max(0.2, Math.min(2, zoomLevels[frameNum] + delta));
            updateZoom(frameNum);
        }

        function updateZoom(frameNum) {
            const frame = document.getElementById(`frame${frameNum}`);
            const zoomDisplay = document.getElementById(`zoom${frameNum}`);
            const scale = zoomLevels[frameNum];

            frame.style.transform = `scale(${scale})`;
            frame.style.width = `${100 / scale}%`;
            frame.style.height = `${100 / scale}%`;
            zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
        }

        // Gestion des erreurs d'iframe
        function handleIframeError(frameNum, address) {
            console.error(`Erreur de chargement pour l'iframe ${frameNum}: ${address}`);
            const errorDiv = document.getElementById(`error${frameNum}`);
            if (errorDiv) {
                errorDiv.style.display = 'block';
            }
        }

        // Détection des erreurs de chargement après un délai
        function checkIframeLoading() {
            if (currentMode === 'dashboard') {
                for (let i = 1; i <= addresses.length; i++) {
                    const iframe = document.getElementById(`iframe${i}`);
                    if (iframe) {
                        iframe.addEventListener('load', function() {
                            try {
                                // Tenter d'accéder au document pour vérifier si c'est bloqué
                                const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                                // Si on arrive ici, l'iframe est chargée correctement
                            } catch (e) {
                                // Erreur de cross-origin, probablement bloqué
                                console.warn(`L'iframe ${i} pourrait être bloquée par des politiques de sécurité`);
                            }
                        });
                    }
                }
            }
        }

        // Gestion du redimensionnement de la fenêtre
        window.addEventListener('resize', function() {
            if (currentMode === 'dashboard' && autoFitEnabled) {
                // Réappliquer le zoom automatique après redimensionnement
                setTimeout(() => {
                    for (let i = 1; i <= addresses.length; i++) {
                        autoFitFrame(i);
                    }
                },
                    100);
            }
        });

        // Fermer la popup avec Escape
        window.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && document.getElementById('configPopup').style.display === 'block') {
                closeConfigPopup();
            }
        });

        // Fermer la popup en cliquant en dehors
        document.getElementById('configPopup').addEventListener('click', function(e) {
            if (e.target === this) {
                closeConfigPopup();
            }
        });

        // Appeler la vérification après le chargement du dashboard
        setTimeout(checkIframeLoading, 1000);



    </script>
</body>
</html>



RE: Petit prog pour esp32 - lucky - 31-05-2025

(30-05-2025, 10:03 PM)59jag a écrit : bonjour 

je voulais faire un peu pres la meme que ton html.
je donc récupérer le tiens pour faire quelques modif.
enregistrement de la config dans le localstorage.
ajustement du zoom par rapport taille ecran
https://drive.google.com/file/d/11epyxRZH7QQxCgYnl3ro3QW5Q3xsbpjG/view?usp=drivesdk
Code :
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Multi-Interfaces</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            background: #222;
        }

        /* Styles pour le mode configuration */
        .config-mode {
            background-color: #f5f5f5 !important;
        }

        .config-container {
            display: none;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }

        .config-mode .config-container {
            display: block;
        }

        .config-panel {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }

        .ip-list {
            margin-bottom: 20px;
        }

        .ip-group {
            margin-bottom: 15px;
            padding: 15px;
            background-color: #f9f9f9;
            border-radius: 5px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .ip-number {
            font-weight: bold;
            color: #555;
            min-width: 30px;
        }

        input[type="text"] {
            flex: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
        }

        input[type="text"]:focus {
            outline: none;
            border-color: #4CAF50;
        }

        .remove-btn {
            background-color: #f44336;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .remove-btn:hover {
            background-color: #d32f2f;
        }

        .add-btn {
            background-color: #2196F3;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin-bottom: 20px;
            width: 100%;
        }

        .add-btn:hover {
            background-color: #1976D2;
        }

        .button-group {
            margin-top: 30px;
            display: flex;
            gap: 10px;
            justify-content: center;
        }

        button {
            padding: 12px 30px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .save-btn {
            background-color: #4CAF50;
            color: white;
        }

        .save-btn:hover {
            background-color: #45a049;
        }

        .message {
            margin-top: 20px;
            padding: 10px;
            border-radius: 5px;
            text-align: center;
            display: none;
        }

        .success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .layout-preview {
            margin-top: 20px;
            padding: 15px;
            background-color: #e3f2fd;
            border-radius: 5px;
            text-align: center;
            font-size: 14px;
            color: #1976D2;
        }

        .help-text {
            font-size: 12px;
            color: #666;
            margin-top: 15px;
            padding: 10px;
            background-color: #f0f0f0;
            border-radius: 5px;
            line-height: 1.6;
        }

        /* Styles pour le mode dashboard */
        .dashboard-container {
            display: none;
            height: 100vh;
            gap: 2px;
            background: #222;
        }

        .dashboard-mode .dashboard-container {
            display: grid;
        }

        .frame-container {
            position: relative;
            overflow: hidden;
            background: white;
        }

        .frame-wrapper {
            width: 100%;
            height: 100%;
            transform-origin: top left;
            transition: transform 0.3s ease;
        }

        iframe {
            width: 100%;
            height: 100%;
            border: none;
            background: white;
        }

        /* Icône de configuration fixe */
        .config-icon {
            position: fixed;
            top: 5px;
            left: 5px;
            z-index: 1000;
            color: rgba(255, 255, 255, 0.8);
            font-size: 5vw;
            cursor: pointer;
            transition: all 0.3s ease;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
            user-select: none;
            display: none;
        }

        .dashboard-mode .config-icon {
            display: block;
        }

        .config-icon:hover {
            color: #FF9800;
            transform: rotate(45deg) scale(1.1);
        }




        /* Fenêtre popup de configuration */
        .config-popup {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            z-index: 2000;
            overflow-y: auto;
        }

        .config-popup-content {
            background: white;
            margin: 50px auto;
            padding: 30px;
            border-radius: 10px;
            max-width: 800px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
            position: relative;
            animation: popupSlideIn 0.3s ease-out;
        }

@keyframes popupSlideIn {
            from {
                opacity: 0;
                transform: translateY(-50px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .close-popup {
            position: absolute;
            top: 15px;
            right: 20px;
            font-size: 30px;
            color: #999;
            cursor: pointer;
            transition: color 0.3s;
        }

        .close-popup:hover {
            color: #333;
        }

        /* Contrôles de zoom */
        .zoom-controls {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(0,0,0,0.8);
            padding: 5px;
            border-radius: 3px;
            z-index: 10;
            display: flex;
            gap: 5px;
        }

        .zoom-btn {
            width: 25px;
            height: 25px;
            border: none;
            background: #444;
            color: white;
            cursor: pointer;
            border-radius: 3px;
            font-size: 16px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .zoom-btn:hover {
            background: #666;
        }

        .zoom-fit {
            background: #2196F3;
            width: auto;
            min-width: 25px;
            padding: 0 8px;
            font-size: 11px;
        }

        .zoom-fit:hover {
            background: #1976D2;
        }

        .zoom-value {
            color: white;
            font-family: Arial, sans-serif;
            font-size: 12px;
            min-width: 45px;
            text-align: center;
            display: flex;
            align-items: center;
        }

        /* Label optionnel */
        .frame-label {
            position: absolute;
            top: 5px;
            left: 5px;
            background: rgba(0,0,0,0.7);
            color: white;
            padding: 5px 10px;
            font-family: Arial, sans-serif;
            font-size: 12px;
            border-radius: 3px;
            z-index: 10;
            pointer-events: none;
            max-width: 70%;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }


        /* Message d'accueil */
        .welcome-message {
            text-align: center;
            padding: 40px;
            background-color: #e3f2fd;
            border-radius: 10px;
            margin-bottom: 30px;
        }

        .welcome-message h2 {
            color: #1976D2;
            margin-bottom: 10px;
        }

        /* Message d'erreur iframe */
        .iframe-error {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #f8d7da;
            color: #721c24;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
            max-width: 80%;
            z-index: 20;
            display: none;
        }

        .iframe-error h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
        }

        .iframe-error p {
            margin: 5px 0;
            font-size: 14px;
        }

        .iframe-error code {
            background: rgba(0,0,0,0.1);
            padding: 2px 5px;
            border-radius: 3px;
            font-size: 12px;
        }


        /* Boutons d'action de configuration */
        .config-actions {
            display: flex;
            gap: 10px;
            margin-top: 20px;
            flex-wrap: wrap;
        }

        .clear-btn {
            background-color: #F44336;
        }

        .clear-btn:hover {
            background-color: #D32F2F;
        }
    </style>
</head>
<body class="config-mode">
    <!-- Icône de configuration fixe -->
    <div class="config-icon" onclick="openConfigPopup()" title="Configuration">
        ⚙️
    </div>

    <!-- Mode Configuration -->
    <div class="config-container">
        <div class="config-panel">
            <h1>Dashboard Multi-Interfaces</h1>

            <div id="welcomeMessage" class="welcome-message">
                <h2>Bienvenue !</h2>
                <p>
                    Configurez vos adresses ci-dessous pour commencer.
                </p>
            </div>

            <div id="ipList" class="ip-list"></div>

            <button class="add-btn" onclick="addIP()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="layoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveAndShowDashboard()">Sauvegarder et afficher</button>
            </div>

            <div class="config-actions">
                <button class="save-btn clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="message" class="message"></div>
        </div>
    </div>

    <!-- Popup de configuration -->
    <div class="config-popup" id="configPopup">
        <div class="config-popup-content">
            <span class="close-popup" onclick="closeConfigPopup()">&times;</span>
            <h1>Configuration Dashboard</h1>

            <div id="popupIpList" class="ip-list"></div>

            <button class="add-btn" onclick="addIPPopup()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="popupLayoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveConfigFromPopup()">Sauvegarder et appliquer</button>
                <button class="save-btn" style="background-color: #666;" onclick="closeConfigPopup()">Annuler</button>
            </div>

            <div class="config-actions">
                <button class="save-btn clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="popupMessage" class="message"></div>
        </div>
    </div>

    <!-- Mode Dashboard -->
    <div id="dashboardContainer" class="dashboard-container"></div>


    <script>
        // Configuration des adresses - Chargée depuis localStorage
        let addresses = [];
        let zoomLevels = {};
        let currentMode = 'config';
        let autoFitEnabled = true;
        let isPopupMode = false;

        // Clé de stockage localStorage
        const STORAGE_KEY = 'dashboard_config';

        // Initialisation
        window.onload = function() {
            loadConfig();

            if (addresses.length > 0 && addresses[0] !== '') {
                document.getElementById('welcomeMessage').style.display = 'none';
                // Aller directement au dashboard si configuré
                showDashboard();
            } else {
                renderIPList();
            }
        };

        // Charger la configuration depuis localStorage
        function loadConfig() {
            try {
                const savedConfig = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                if (Array.isArray(savedConfig) && savedConfig.length > 0) {
                    addresses = savedConfig;
                } else {
                    addresses = [''];
                }
            } catch (e) {
                console.error('Erreur lors du chargement de la configuration:', e);
                addresses = [''];
            }
        }

        // Sauvegarder la configuration dans localStorage
        function saveConfig() {
            try {
                const cleanAddresses = addresses.filter(addr => addr.trim() !== '');
                localStorage.setItem(STORAGE_KEY, JSON.stringify(cleanAddresses));
                return true;
            } catch (e) {
                console.error('Erreur lors de la sauvegarde:', e);
                showMessage('Erreur lors de la sauvegarde de la configuration', 'error');
                return false;
            }
        }

        // Ouvrir la popup de configuration
        function openConfigPopup() {
            isPopupMode = true;
            addresses = [...addresses]; // Copie pour éviter les modifications directes
            renderIPListPopup();
            document.getElementById('configPopup').style.display = 'block';
            document.body.style.overflow = 'hidden'; // Désactiver le scroll du body
        }

        // Fermer la popup de configuration
        function closeConfigPopup() {
            isPopupMode = false;
            document.getElementById('configPopup').style.display = 'none';
            document.body.style.overflow = 'auto'; // Réactiver le scroll du body
            loadConfig(); // Recharger la config originale
        }

        // Afficher la liste des adresses (mode principal)
        function renderIPList() {
            const container = document.getElementById('ipList');
            renderIPListInContainer(container, false);
        }

        // Afficher la liste des adresses (popup)
        function renderIPListPopup() {
            const container = document.getElementById('popupIpList');
            renderIPListInContainer(container, true);
        }

        // Fonction générique pour rendre la liste d'IPs
        function renderIPListInContainer(container, isPopup) {
            container.innerHTML = '';

            addresses.forEach((address, index) => {
                const div = document.createElement('div');
                div.className = 'ip-group';
                div.innerHTML = `
                <span class="ip-number">${index + 1}.</span>
                <input type="text" id="${isPopup ? 'popup': ''}address${index}" value="${address}" placeholder="Ex: 192.168.1.100, example.com, https://app.example.com">
                ${addresses.length > 1 ? `<button class="remove-btn" onclick="${isPopup ? 'removeIPPopup': 'removeIP'}(${index})">Supprimer</button>`: ''}
                `;
                container.appendChild(div);
            });

            updateLayoutPreview(isPopup);
        }

        // Ajouter une adresse
        function addIP() {
            addresses.push('');
            renderIPList();
        }

        // Ajouter une adresse (popup)
        function addIPPopup() {
            addresses.push('');
            renderIPListPopup();
        }

        // Supprimer une adresse
        function removeIP(index) {
            addresses.splice(index, 1);
            renderIPList();
        }

        // Supprimer une adresse (popup)
        function removeIPPopup(index) {
            addresses.splice(index, 1);
            renderIPListPopup();
        }

        // Mettre à jour l'aperçu
        function updateLayoutPreview(isPopup = false) {
            const preview = document.getElementById(isPopup ? 'popupLayoutPreview': 'layoutPreview');
            const count = addresses.length;

            let layout = '';
            if (count === 1) {
                layout = 'Plein écran';
            } else if (count === 2) {
                layout = '2 colonnes côte à côte';
            } else if (count === 3) {
                layout = '1 grande vue à gauche + 2 petites à droite';
            } else if (count === 4) {
                layout = 'Grille 2x2';
            } else if (count <= 6) {
                layout = 'Grille 2x3';
            } else if (count <= 9) {
                layout = 'Grille 3x3';
            } else {
                layout = `Grille ${Math.ceil(Math.sqrt(count))}x${Math.ceil(count / Math.ceil(Math.sqrt(count)))}`;
            }

            preview.innerHTML = `<strong>Disposition :</strong> ${count} interface${count > 1 ? 's': ''} - ${layout}`;
        }

        // Sauvegarder la configuration depuis la popup
        function saveConfigFromPopup() {
            // Récupérer et valider les adresses depuis la popup
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const addressValue = document.getElementById(`popupaddress${i}`).value.trim();
                if (addressValue) {
                    const validation = validateAndFormatAddress(addressValue);
                    if (!validation.valid) {
                        showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error', true);
                        return;
                    }
                    newAddresses.push(addressValue);
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error', true);
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée avec succès !', 'success', true);
                setTimeout(() => {
                    closeConfigPopup();
                    showDashboard();
                }, 1000);
            }
        }

        // Sauvegarder et afficher le dashboard (mode principal)
        function saveAndShowDashboard() {
            // Récupérer et valider les adresses
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const addressInput = document.getElementById(`address${i}`);
                const addressValue = addressInput ? addressInput.value.trim(): addresses[i];

                if (addressValue) {
                    const validation = validateAndFormatAddress(addressValue);
                    if (!validation.valid) {
                        showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                        return;
                    }
                    newAddresses.push(addressValue);
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error');
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée !', 'success');
                setTimeout(showDashboard, 500);
            }
        }

        // Effacer toute la configuration
        function clearConfig() {
            if (confirm('Êtes-vous sûr de vouloir effacer toute la configuration ?')) {
                localStorage.removeItem(STORAGE_KEY);
                addresses = [''];

                if (isPopupMode) {
                    renderIPListPopup();
                } else {
                    renderIPList();
                }

                showMessage('Configuration effacée', 'success', isPopupMode);
            }
        }

        // Valider et formater une adresse
        function validateAndFormatAddress(address) {
            address = address.trim();

            if (!address) {
                return {
                    valid: false,
                    error: "L'adresse ne peut pas être vide"
                };
            }

            if (address.match(/^https?:\/\//)) {
                return {
                    valid: true,
                    formatted: address,
                    display: address
                };
            }

            const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::\d{1,5})?$/;
            if (ipPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            const domainPattern = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(:\d{1,5})?(\/.*)?$/;
            const localhostPattern = /^localhost(:\d{1,5})?(\/.*)?$/;

            if (domainPattern.test(address) || localhostPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            if (/^[a-zA-Z0-9-]+(:\d{1,5})?(\/.*)?$/.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            return {
                valid: false,
                error: "Format d'adresse non valide"
            };
        }

        // Afficher un message
        function showMessage(text, type, inPopup = false) {
            const messageEl = document.getElementById(inPopup ? 'popupMessage': 'message');
            messageEl.textContent = text;
            messageEl.className = 'message ' + type;
            messageEl.style.display = 'block';

            setTimeout(() => {
                messageEl.style.display = 'none';
            }, 3000);
        }

        // Calculer la disposition
        function calculateGridLayout(count) {
            if (count === 1) return {
                cols: 1,
                rows: 1,
                special: 'single'
            };
            if (count === 2) return {
                cols: 2,
                rows: 1,
                special: 'two-columns'
            };
            if (count === 3) return {
                cols: 2,
                rows: 2,
                special: 'three-special'
            };
            if (count === 4) return {
                cols: 2,
                rows: 2,
                special: 'grid'
            };

            const cols = Math.ceil(Math.sqrt(count));
            const rows = Math.ceil(count / cols);
            return {
                cols,
                rows,
                special: 'grid'
            };
        }

        // Calculer le zoom optimal pour une iframe
        function calculateOptimalZoom(frameContainer) {
            const containerWidth = frameContainer.clientWidth;

            // Utiliser la largeur réelle de l'écran comme référence
            const referenceWidth = window.screen.width*2;

            // Calculer le ratio de zoom basé uniquement sur la largeur
            const optimalZoom = containerWidth / referenceWidth;

            // Limiter entre 0.2 et 2
            return Math.max(0.2, Math.min(2, optimalZoom));
        }


        // Appliquer le zoom automatique à une iframe
        function autoFitFrame(frameNum) {
            const frameContainer = document.querySelector(`#frame${frameNum}`).parentElement;
            const optimalZoom = calculateOptimalZoom(frameContainer);

            zoomLevels[frameNum] = optimalZoom;
            updateZoom(frameNum);
        }

        // Appliquer le zoom automatique à toutes les iframes
        function autoFitAll() {
            for (let i = 1; i <= addresses.length; i++) {
                autoFitFrame(i);
            }
        }

        // Afficher le dashboard
        function showDashboard() {
            // Valider les adresses
            const validatedAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const validation = validateAndFormatAddress(addresses[i]);
                if (!validation.valid) {
                    showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                    return;
                }
                validatedAddresses.push(validation);
            }

            // Construire le dashboard avec les adresses formatées
            buildDashboard(validatedAddresses);

            // Basculer en mode dashboard
            document.body.className = 'dashboard-mode';
            currentMode = 'dashboard';

            // Appliquer le zoom automatique après un délai
            setTimeout(() => {
                if (autoFitEnabled) {
                    for (let i = 1; i <= addresses.length; i++) {
                        autoFitFrame(i);
                    }
                }
            },
                500);
        }

        // Construire le dashboard
        function buildDashboard(validatedAddresses) {
            const container = document.getElementById('dashboardContainer');
            const layout = calculateGridLayout(validatedAddresses.length);

            // Appliquer les styles de grille
            if (layout.special === 'single') {
                container.style.gridTemplateColumns = '1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'two-columns') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'three-special') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr 1fr';
            } else {
                container.style.gridTemplateColumns = `repeat(${layout.cols}, 1fr)`;
                container.style.gridTemplateRows = `repeat(${layout.rows}, 1fr)`;
            }

            // Créer les conteneurs
            container.innerHTML = '';
            validatedAddresses.forEach((addressInfo, index) => {
                const frameDiv = document.createElement('div');
                frameDiv.className = 'frame-container';

                // Style spécial pour 3 interfaces
                if (layout.special === 'three-special' && index === 0) {
                    frameDiv.style.gridRow = 'span 2';
                }

                frameDiv.innerHTML = `
                <span class="frame-label" title="${addressInfo.display}">${addressInfo.display}</span>
                <div class="zoom-controls">
                <button class="zoom-btn" onclick="zoom(${index + 1}, -0.1)">−</button>
                <span class="zoom-value" id="zoom${index + 1}">100%</span>
                <button class="zoom-btn" onclick="zoom(${index + 1}, 0.1)">+</button>
                <button class="zoom-btn zoom-fit" onclick="autoFitFrame(${index + 1})" title="Ajuster à l'écran">FIT</button>
                </div>
                <div class="frame-wrapper" id="frame${index + 1}">
                <iframe
                src="${addressInfo.formatted}"
                sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals allow-downloads allow-presentation allow-top-navigation"
                referrerpolicy="no-referrer"
                id="iframe${index + 1}"
                onerror="handleIframeError(${index + 1}, '${addressInfo.display}')"
                ></iframe>
                </div>
                <div class="iframe-error" id="error${index + 1}">
                <h3>⚠️ Impossible de charger ${addressInfo.display}</h3>
                <p>Cette interface ne peut pas être affichée dans une iframe.</p>
                <p><small>Pour Home Assistant, ajoutez dans configuration.yaml :</small></p>
                <code>http:<br>&nbsp;&nbsp;use_x_frame_options: false</code>
                <p style="margin-top: 15px;">
                <a href="${addressInfo.formatted}" target="_blank" style="color: #0056b3;">Ouvrir dans un nouvel onglet →</a>
                </p>
                </div>
                `;

                container.appendChild(frameDiv);
            });

            // Initialiser les niveaux de zoom
            zoomLevels = {};
            for (let i = 1; i <= validatedAddresses.length; i++) {
                zoomLevels[i] = 1;
            }
        }

        // Retourner à la configuration
        function showConfig() {
            document.body.className = 'config-mode';
            currentMode = 'config';
            renderIPList();
        }

        // Fonctions de zoom
        function zoom(frameNum, delta) {
            zoomLevels[frameNum] = Math.max(0.2, Math.min(2, zoomLevels[frameNum] + delta));
            updateZoom(frameNum);
        }

        function updateZoom(frameNum) {
            const frame = document.getElementById(`frame${frameNum}`);
            const zoomDisplay = document.getElementById(`zoom${frameNum}`);
            const scale = zoomLevels[frameNum];

            frame.style.transform = `scale(${scale})`;
            frame.style.width = `${100 / scale}%`;
            frame.style.height = `${100 / scale}%`;
            zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
        }

        // Gestion des erreurs d'iframe
        function handleIframeError(frameNum, address) {
            console.error(`Erreur de chargement pour l'iframe ${frameNum}: ${address}`);
            const errorDiv = document.getElementById(`error${frameNum}`);
            if (errorDiv) {
                errorDiv.style.display = 'block';
            }
        }

        // Détection des erreurs de chargement après un délai
        function checkIframeLoading() {
            if (currentMode === 'dashboard') {
                for (let i = 1; i <= addresses.length; i++) {
                    const iframe = document.getElementById(`iframe${i}`);
                    if (iframe) {
                        iframe.addEventListener('load', function() {
                            try {
                                // Tenter d'accéder au document pour vérifier si c'est bloqué
                                const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                                // Si on arrive ici, l'iframe est chargée correctement
                            } catch (e) {
                                // Erreur de cross-origin, probablement bloqué
                                console.warn(`L'iframe ${i} pourrait être bloquée par des politiques de sécurité`);
                            }
                        });
                    }
                }
            }
        }

        // Gestion du redimensionnement de la fenêtre
        window.addEventListener('resize', function() {
            if (currentMode === 'dashboard' && autoFitEnabled) {
                // Réappliquer le zoom automatique après redimensionnement
                setTimeout(() => {
                    for (let i = 1; i <= addresses.length; i++) {
                        autoFitFrame(i);
                    }
                },
                    100);
            }
        });

        // Fermer la popup avec Escape
        window.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && document.getElementById('configPopup').style.display === 'block') {
                closeConfigPopup();
            }
        });

        // Fermer la popup en cliquant en dehors
        document.getElementById('configPopup').addEventListener('click', function(e) {
            if (e.target === this) {
                closeConfigPopup();
            }
        });

        // Appeler la vérification après le chargement du dashboard
        setTimeout(checkIframeLoading, 1000);



    </script>
</body>
</html>
reste quelques bug d affichage

oui pas mal 
bien l ajustement ....je retiens
l enregistrement aussi d ailleur, mais valable que sur un nav il faut recommencer pour un autre
avec une config sauve l html reste valable pour tous


RE: Petit prog pour esp32 - lucky - 31-05-2025

Code :
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Multi-Interfaces</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            background: #222;
        }

        /* Styles pour le mode configuration */
        .config-mode {
            background-color: #f5f5f5 !important;
        }

        .config-container {
            display: none;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }

        .config-mode .config-container {
            display: block;
        }

        .config-panel {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }

        .ip-list {
            margin-bottom: 20px;
        }

        .ip-group {
            margin-bottom: 15px;
            padding: 15px;
            background-color: #f9f9f9;
            border-radius: 5px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .ip-number {
            font-weight: bold;
            color: #555;
            min-width: 30px;
        }

        input[type="text"] {
            flex: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
        }

        input[type="text"]:focus {
            outline: none;
            border-color: #4CAF50;
        }

        .remove-btn {
            background-color: #f44336;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .remove-btn:hover {
            background-color: #d32f2f;
        }

        .add-btn {
            background-color: #2196F3;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin-bottom: 20px;
            width: 100%;
        }

        .add-btn:hover {
            background-color: #1976D2;
        }

        .button-group {
            margin-top: 30px;
            display: flex;
            gap: 10px;
            justify-content: center;
        }

        button {
            padding: 12px 30px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .save-btn {
            background-color: #4CAF50;
            color: white;
        }

        .save-btn:hover {
            background-color: #45a049;
        }

        .message {
            margin-top: 20px;
            padding: 10px;
            border-radius: 5px;
            text-align: center;
            display: none;
        }

        .success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .layout-preview {
            margin-top: 20px;
            padding: 15px;
            background-color: #e3f2fd;
            border-radius: 5px;
            text-align: center;
            font-size: 14px;
            color: #1976D2;
        }

        .help-text {
            font-size: 12px;
            color: #666;
            margin-top: 15px;
            padding: 10px;
            background-color: #f0f0f0;
            border-radius: 5px;
            line-height: 1.6;
        }

        /* Styles pour le mode dashboard */
        .dashboard-container {
            display: none;
            height: 100vh;
            gap: 2px;
            background: #222;
        }

        .dashboard-mode .dashboard-container {
            display: grid;
        }

        .frame-container {
            position: relative;
            overflow: hidden;
            background: white;
        }

        .frame-wrapper {
            width: 100%;
            height: 100%;
            transform-origin: top left;
            transition: transform 0.3s ease;
        }

        iframe {
            width: 100%;
            height: 100%;
            border: none;
            background: white;
        }

        /* Icône de configuration fixe */
        .config-icon {
            position: fixed;
            top: 5px;
            left: 5px;
            z-index: 1000;
            color: rgba(255, 255, 255, 0.8);
            font-size: min(5vw, 40px);
            cursor: pointer;
            transition: all 0.3s ease;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
            user-select: none;
            display: none;
        }

        .dashboard-mode .config-icon {
            display: block;
        }

        .config-icon:hover {
            color: #FF9800;
            transform: rotate(45deg) scale(1.1);
        }

        /* Fenêtre popup de configuration */
        .config-popup {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            z-index: 2000;
            overflow-y: auto;
        }

        .config-popup-content {
            background: white;
            margin: 50px auto;
            padding: 30px;
            border-radius: 10px;
            max-width: 800px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
            position: relative;
            animation: popupSlideIn 0.3s ease-out;
        }

        @keyframes popupSlideIn {
            from {
                opacity: 0;
                transform: translateY(-50px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .close-popup {
            position: absolute;
            top: 15px;
            right: 20px;
            font-size: 30px;
            color: #999;
            cursor: pointer;
            transition: color 0.3s;
        }

        .close-popup:hover {
            color: #333;
        }

        /* Contrôles de zoom */
        .zoom-controls {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(0,0,0,0.8);
            padding: 5px;
            border-radius: 3px;
            z-index: 10;
            display: flex;
            gap: 5px;
        }

        .zoom-btn {
            width: 25px;
            height: 25px;
            border: none;
            background: #444;
            color: white;
            cursor: pointer;
            border-radius: 3px;
            font-size: 16px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .zoom-btn:hover {
            background: #666;
        }

        .zoom-fit {
            background: #2196F3;
            width: auto;
            min-width: 25px;
            padding: 0 8px;
            font-size: 11px;
        }

        .zoom-fit:hover {
            background: #1976D2;
        }

        .zoom-value {
            color: white;
            font-family: Arial, sans-serif;
            font-size: 12px;
            min-width: 45px;
            text-align: center;
            display: flex;
            align-items: center;
        }

        /* Label optionnel */
        .frame-label {
            position: absolute;
            top: 5px;
            left: 5px;
            background: rgba(0,0,0,0.7);
            color: white;
            padding: 5px 10px;
            font-family: Arial, sans-serif;
            font-size: 12px;
            border-radius: 3px;
            z-index: 10;
            pointer-events: none;
            max-width: 70%;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        /* Message d'accueil */
        .welcome-message {
            text-align: center;
            padding: 40px;
            background-color: #e3f2fd;
            border-radius: 10px;
            margin-bottom: 30px;
        }

        .welcome-message h2 {
            color: #1976D2;
            margin-bottom: 10px;
        }

        /* Message d'erreur iframe */
        .iframe-error {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #f8d7da;
            color: #721c24;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
            max-width: 80%;
            z-index: 20;
            display: none;
        }

        .iframe-error h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
        }

        .iframe-error p {
            margin: 5px 0;
            font-size: 14px;
        }

        .iframe-error code {
            background: rgba(0,0,0,0.1);
            padding: 2px 5px;
            border-radius: 3px;
            font-size: 12px;
        }

        /* Boutons d'action de configuration */
        .config-actions {
            display: flex;
            gap: 10px;
            margin-top: 30px;
            flex-wrap: wrap;
            justify-content: center;
        }

        .clear-btn {
            background-color: #F44336;
            flex: 1;
            max-width: 200px;
        }

        .clear-btn:hover {
            background-color: #D32F2F;
        }

        .cancel-btn {
            background-color: #666;
        }

        .cancel-btn:hover {
            background-color: #555;
        }
    </style>
</head>
<body class="config-mode">
    <!-- Icône de configuration fixe -->
    <div class="config-icon" onclick="openConfigPopup()" title="Configuration">
        ⚙️
    </div>

    <!-- Mode Configuration -->
    <div class="config-container">
        <div class="config-panel">
            <h1>Dashboard Multi-Interfaces</h1>

            <div id="welcomeMessage" class="welcome-message">
                <h2>Bienvenue !</h2>
                <p>
                    Configurez vos adresses ci-dessous pour commencer.
                </p>
            </div>

            <div id="ipList" class="ip-list"></div>

            <button class="add-btn" onclick="addIP()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="layoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveAndShowDashboard()">Sauvegarder et afficher</button>
            </div>

            <div class="config-actions">
                <button class="clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="message" class="message"></div>
        </div>
    </div>

    <!-- Popup de configuration -->
    <div class="config-popup" id="configPopup">
        <div class="config-popup-content">
            <span class="close-popup" onclick="closeConfigPopup()">&times;</span>
            <h1>Configuration Dashboard</h1>

            <div id="popupIpList" class="ip-list"></div>

            <button class="add-btn" onclick="addIPPopup()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="popupLayoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveConfigFromPopup()">Sauvegarder et appliquer</button>
                <button class="cancel-btn" onclick="closeConfigPopup()">Annuler</button>
            </div>

            <div class="config-actions">
                <button class="clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="popupMessage" class="message"></div>
        </div>
    </div>

    <!-- Mode Dashboard -->
    <div id="dashboardContainer" class="dashboard-container"></div>

    <script>
        // Configuration des adresses - Chargée depuis localStorage
        let addresses = [];
        let zoomLevels = {};
        let currentMode = 'config';
        let autoFitEnabled = true;
        let isPopupMode = false;

        // Clé de stockage localStorage
        const STORAGE_KEY = 'dashboard_config';

        // Initialisation
        window.onload = function() {
            loadConfig();

            if (addresses.length > 0 && addresses[0] !== '') {
                document.getElementById('welcomeMessage').style.display = 'none';
                // Aller directement au dashboard si configuré
                showDashboard();
            } else {
                renderIPList();
            }
        };

        // Charger la configuration depuis localStorage
        function loadConfig() {
            try {
                const savedConfig = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                if (Array.isArray(savedConfig) && savedConfig.length > 0) {
                    addresses = savedConfig;
                } else {
                    addresses = [''];
                }
            } catch (e) {
                console.error('Erreur lors du chargement de la configuration:', e);
                addresses = [''];
            }
        }

        // Sauvegarder la configuration dans localStorage
        function saveConfig() {
            try {
                const cleanAddresses = addresses.filter(addr => addr.trim() !== '');
                localStorage.setItem(STORAGE_KEY, JSON.stringify(cleanAddresses));
                return true;
            } catch (e) {
                console.error('Erreur lors de la sauvegarde:', e);
                showMessage('Erreur lors de la sauvegarde de la configuration', 'error');
                return false;
            }
        }

        // Ouvrir la popup de configuration
        function openConfigPopup() {
            isPopupMode = true;
            // Copie profonde des adresses pour éviter les modifications directes
            addresses = JSON.parse(JSON.stringify(addresses));
            renderIPListPopup();
            document.getElementById('configPopup').style.display = 'block';
            document.body.style.overflow = 'hidden'; // Désactiver le scroll du body
        }

        // Fermer la popup de configuration
        function closeConfigPopup() {
            isPopupMode = false;
            document.getElementById('configPopup').style.display = 'none';
            document.body.style.overflow = 'auto'; // Réactiver le scroll du body
            loadConfig(); // Recharger la config originale
        }

        // Afficher la liste des adresses (mode principal)
        function renderIPList() {
            const container = document.getElementById('ipList');
            renderIPListInContainer(container, false);
        }

        // Afficher la liste des adresses (popup)
        function renderIPListPopup() {
            const container = document.getElementById('popupIpList');
            renderIPListInContainer(container, true);
        }

        // Fonction générique pour rendre la liste d'IPs
        function renderIPListInContainer(container, isPopup) {
            container.innerHTML = '';

            addresses.forEach((address, index) => {
                const div = document.createElement('div');
                div.className = 'ip-group';
                div.innerHTML = `
                    <span class="ip-number">${index + 1}.</span>
                    <input type="text" id="${isPopup ? 'popup' : ''}address${index}" value="${address}" placeholder="Ex: 192.168.1.100, example.com, https://app.example.com">
                    ${addresses.length > 1 ? `<button class="remove-btn" onclick="${isPopup ? 'removeIPPopup' : 'removeIP'}(${index})">Supprimer</button>` : ''}
                `;
                container.appendChild(div);
            });

            updateLayoutPreview(isPopup);
        }

        // Ajouter une adresse
        function addIP() {
            addresses.push('');
            renderIPList();
        }

        // Ajouter une adresse (popup)
        function addIPPopup() {
            addresses.push('');
            renderIPListPopup();
        }

        // Supprimer une adresse
        function removeIP(index) {
            addresses.splice(index, 1);
            if (addresses.length === 0) {
                addresses.push('');
            }
            renderIPList();
        }

        // Supprimer une adresse (popup)
        function removeIPPopup(index) {
            addresses.splice(index, 1);
            if (addresses.length === 0) {
                addresses.push('');
            }
            renderIPListPopup();
        }

        // Mettre à jour l'aperçu
        function updateLayoutPreview(isPopup = false) {
            const preview = document.getElementById(isPopup ? 'popupLayoutPreview' : 'layoutPreview');
            const count = addresses.filter(addr => addr.trim() !== '').length;

            let layout = '';
            if (count === 0) {
                layout = 'Aucune interface configurée';
            } else if (count === 1) {
                layout = 'Plein écran';
            } else if (count === 2) {
                layout = '2 colonnes côte à côte';
            } else if (count === 3) {
                layout = '1 grande vue à gauche + 2 petites à droite';
            } else if (count === 4) {
                layout = 'Grille 2x2';
            } else if (count <= 6) {
                layout = 'Grille 2x3';
            } else if (count <= 9) {
                layout = 'Grille 3x3';
            } else {
                const cols = Math.ceil(Math.sqrt(count));
                const rows = Math.ceil(count / cols);
                layout = `Grille ${cols}x${rows}`;
            }

            preview.innerHTML = `<strong>Disposition :</strong> ${count} interface${count > 1 ? 's' : ''} - ${layout}`;
        }

        // Sauvegarder la configuration depuis la popup
        function saveConfigFromPopup() {
            // Récupérer et valider les adresses depuis la popup
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const input = document.getElementById(`popupaddress${i}`);
                if (input) {
                    const addressValue = input.value.trim();
                    if (addressValue) {
                        const validation = validateAndFormatAddress(addressValue);
                        if (!validation.valid) {
                            showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error', true);
                            return;
                        }
                        newAddresses.push(addressValue);
                    }
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error', true);
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée avec succès !', 'success', true);
                setTimeout(() => {
                    closeConfigPopup();
                    showDashboard();
                }, 1000);
            }
        }

        // Sauvegarder et afficher le dashboard (mode principal)
        function saveAndShowDashboard() {
            // Récupérer et valider les adresses
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const addressInput = document.getElementById(`address${i}`);
                const addressValue = addressInput ? addressInput.value.trim() : addresses[i];

                if (addressValue) {
                    const validation = validateAndFormatAddress(addressValue);
                    if (!validation.valid) {
                        showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                        return;
                    }
                    newAddresses.push(addressValue);
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error');
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée !', 'success');
                setTimeout(showDashboard, 500);
            }
        }

        // Effacer toute la configuration
        function clearConfig() {
            if (confirm('Êtes-vous sûr de vouloir effacer toute la configuration ?')) {
                localStorage.removeItem(STORAGE_KEY);
                addresses = [''];
                zoomLevels = {};

                if (isPopupMode) {
                    renderIPListPopup();
                } else {
                    renderIPList();
                    document.getElementById('welcomeMessage').style.display = 'block';
                }

                showMessage('Configuration effacée', 'success', isPopupMode);
            }
        }

        // Valider et formater une adresse
        function validateAndFormatAddress(address) {
            address = address.trim();

            if (!address) {
                return {
                    valid: false,
                    error: "L'adresse ne peut pas être vide"
                };
            }

            // URL complète avec protocole
            if (address.match(/^https?:\/\//)) {
                return {
                    valid: true,
                    formatted: address,
                    display: address
                };
            }

            // Pattern IP avec ou sans port
            const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::\d{1,5})?$/;
            if (ipPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            // Pattern domaine avec ou sans port et chemin
            const domainPattern = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(:\d{1,5})?(\/.*)?$/;
            const localhostPattern = /^localhost(:\d{1,5})?(\/.*)?$/;

            if (domainPattern.test(address) || localhostPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            // Pattern nom simple avec port optionnel (pour réseaux locaux)
            if (/^[a-zA-Z0-9-]+(:\d{1,5})?(\/.*)?$/.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            return {
                valid: false,
                error: "Format d'adresse non valide"
            };
        }

        // Afficher un message
        function showMessage(text, type, inPopup = false) {
            const messageEl = document.getElementById(inPopup ? 'popupMessage' : 'message');
            messageEl.textContent = text;
            messageEl.className = 'message ' + type;
            messageEl.style.display = 'block';

            setTimeout(() => {
                messageEl.style.display = 'none';
            }, 3000);
        }

        // Calculer la disposition
        function calculateGridLayout(count) {
            if (count === 1) return {
                cols: 1,
                rows: 1,
                special: 'single'
            };
            if (count === 2) return {
                cols: 2,
                rows: 1,
                special: 'two-columns'
            };
            if (count === 3) return {
                cols: 2,
                rows: 2,
                special: 'three-special'
            };
            if (count === 4) return {
                cols: 2,
                rows: 2,
                special: 'grid'
            };

            const cols = Math.ceil(Math.sqrt(count));
            const rows = Math.ceil(count / cols);
            return {
                cols,
                rows,
                special: 'grid'
            };
        }

        // Calculer le zoom optimal pour une iframe
        function calculateOptimalZoom(frameContainer) {
            const containerWidth = frameContainer.clientWidth;
            const containerHeight = frameContainer.clientHeight;

            // Utiliser la plus petite dimension de l'écran comme référence
            const referenceWidth = Math.min(window.screen.width, 1920);
            const referenceHeight = Math.min(window.screen.height, 1080);

            // Calculer le ratio de zoom basé sur la largeur et la hauteur
            const widthRatio = containerWidth / referenceWidth;
            const heightRatio = containerHeight / referenceHeight;

            // Prendre le plus petit ratio pour s'assurer que tout rentre
            const optimalZoom = Math.min(widthRatio, heightRatio);

            // Limiter entre 0.2 et 2
            return Math.max(0.2, Math.min(2, optimalZoom));
        }

        // Appliquer le zoom automatique à une iframe
        function autoFitFrame(frameNum) {
            const frameContainer = document.querySelector(`#frame${frameNum}`);
            if (frameContainer && frameContainer.parentElement) {
                const optimalZoom = calculateOptimalZoom(frameContainer.parentElement);
                zoomLevels[frameNum] = optimalZoom;
                updateZoom(frameNum);
            }
        }

        // Appliquer le zoom automatique à toutes les iframes
        function autoFitAll() {
            for (let i = 1; i <= addresses.length; i++) {
                autoFitFrame(i);
            }
        }

        // Afficher le dashboard
        function showDashboard() {
            // Valider les adresses
            const validatedAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const validation = validateAndFormatAddress(addresses[i]);
                if (!validation.valid) {
                    showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                    return;
                }
                validatedAddresses.push(validation);
            }

            // Construire le dashboard avec les adresses formatées
            buildDashboard(validatedAddresses);

            // Basculer en mode dashboard
            document.body.className = 'dashboard-mode';
            currentMode = 'dashboard';

            // Appliquer le zoom automatique après un délai
            setTimeout(() => {
                if (autoFitEnabled) {
                    autoFitAll();
                }
            }, 500);
        }

        // Construire le dashboard
        function buildDashboard(validatedAddresses) {
            const container = document.getElementById('dashboardContainer');
            const layout = calculateGridLayout(validatedAddresses.length);

            // Appliquer les styles de grille
            if (layout.special === 'single') {
                container.style.gridTemplateColumns = '1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'two-columns') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'three-special') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr 1fr';
            } else {
                container.style.gridTemplateColumns = `repeat(${layout.cols}, 1fr)`;
                container.style.gridTemplateRows = `repeat(${layout.rows}, 1fr)`;
            }

            // Créer les conteneurs
            container.innerHTML = '';
            validatedAddresses.forEach((addressInfo, index) => {
                const frameDiv = document.createElement('div');
                frameDiv.className = 'frame-container';

                // Style spécial pour 3 interfaces
                if (layout.special === 'three-special' && index === 0) {
                    frameDiv.style.gridRow = 'span 2';
                }

                const frameNum = index + 1;
                frameDiv.innerHTML = `
                    <span class="frame-label" title="${addressInfo.display}">${addressInfo.display}</span>
                    <div class="zoom-controls">
                        <button class="zoom-btn" onclick="zoom(${frameNum}, -0.1)">−</button>
                        <span class="zoom-value" id="zoom${frameNum}">100%</span>
                        <button class="zoom-btn" onclick="zoom(${frameNum}, 0.1)">+</button>
                        <button class="zoom-btn zoom-fit" onclick="autoFitFrame(${frameNum})" title="Ajuster à l'écran">FIT</button>
                    </div>
                    <div class="frame-wrapper" id="frame${frameNum}">
                        <iframe
                            src="${addressInfo.formatted}"
                            sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals allow-downloads allow-presentation allow-top-navigation"
                            referrerpolicy="no-referrer"
                            id="iframe${frameNum}"
                            onerror="handleIframeError(${frameNum}, '${addressInfo.display}')"
                        ></iframe>
                    </div>
                    <div class="iframe-error" id="error${frameNum}">
                        <h3>⚠️ Impossible de charger ${addressInfo.display}</h3>
                        <p>Cette interface ne peut pas être affichée dans une iframe.</p>
                        <p><small>Pour Home Assistant, ajoutez dans configuration.yaml :</small></p>
                        <code>http:<br>&nbsp;&nbsp;use_x_frame_options: false</code>
                        <p style="margin-top: 15px;">
                            <a href="${addressInfo.formatted}" target="_blank" style="color: #0056b3;">Ouvrir dans un nouvel onglet →</a>
                        </p>
                    </div>
                `;

                container.appendChild(frameDiv);
            });

            // Initialiser les niveaux de zoom
            zoomLevels = {};
            for (let i = 1; i <= validatedAddresses.length; i++) {
                zoomLevels[i] = 1;
            }

            // Vérifier le chargement des iframes après un délai
            setTimeout(checkIframeLoading, 2000);
        }

        // Retourner à la configuration
        function showConfig() {
            document.body.className = 'config-mode';
            currentMode = 'config';
            renderIPList();
        }

        // Fonctions de zoom
        function zoom(frameNum, delta) {
            const currentZoom = zoomLevels[frameNum] || 1;
            zoomLevels[frameNum] = Math.max(0.2, Math.min(2, currentZoom + delta));
            updateZoom(frameNum);
        }

        function updateZoom(frameNum) {
            const frame = document.getElementById(`frame${frameNum}`);
            const zoomDisplay = document.getElementById(`zoom${frameNum}`);
           
            if (!frame || !zoomDisplay) return;
           
            const scale = zoomLevels[frameNum] || 1;

            frame.style.transform = `scale(${scale})`;
            frame.style.width = `${100 / scale}%`;
            frame.style.height = `${100 / scale}%`;
            zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
        }

        // Gestion des erreurs d'iframe
        function handleIframeError(frameNum, address) {
            console.error(`Erreur de chargement pour l'iframe ${frameNum}: ${address}`);
            const errorDiv = document.getElementById(`error${frameNum}`);
            if (errorDiv) {
                errorDiv.style.display = 'block';
            }
        }

        // Détection des erreurs de chargement après un délai
        function checkIframeLoading() {
            if (currentMode === 'dashboard') {
                for (let i = 1; i <= addresses.length; i++) {
                    const iframe = document.getElementById(`iframe${i}`);
                    if (iframe) {
                        iframe.addEventListener('load', function() {
                            try {
                                // Tenter d'accéder au document pour vérifier si c'est bloqué
                                const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                                // Si on arrive ici, l'iframe est chargée correctement
                                console.log(`Iframe ${i} chargée avec succès`);
                            } catch (e) {
                                // Erreur de cross-origin, probablement bloqué
                                console.warn(`L'iframe ${i} pourrait être bloquée par des politiques de sécurité`);
                            }
                        });

                        iframe.addEventListener('error', function() {
                            handleIframeError(i, addresses[i - 1]);
                        });
                    }
                }
            }
        }

        // Gestion du redimensionnement de la fenêtre
        let resizeTimeout;
        window.addEventListener('resize', function() {
            if (currentMode === 'dashboard' && autoFitEnabled) {
                clearTimeout(resizeTimeout);
                resizeTimeout = setTimeout(() => {
                    autoFitAll();
                }, 100);
            }
        });

        // Fermer la popup avec Escape
        window.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && document.getElementById('configPopup').style.display === 'block') {
                closeConfigPopup();
            }
        });

        // Fermer la popup en cliquant en dehors
        document.getElementById('configPopup').addEventListener('click', function(e) {
            if (e.target === this) {
                closeConfigPopup();
            }
        });
    </script>
</body>
</html>



RE: Petit prog pour esp32 - 59jag - 31-05-2025

j ai modifie le zoom pour n importe quel orientation de l ecran et barres de zoom plus superposés sur les iframes

https://drive.google.com/file/d/12rkKS3TrXMMQUkImj5WCeDMZV9-rUt2f/view?usp=drivesdk

Code :
lucky<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Dashboard Multi-Interfaces</title>
    <style>
        body {
            margin: 0;
            padding: 0;
            font-family: Arial, sans-serif;
            background: #222;
        }

        /* Styles pour le mode configuration */
        .config-mode {
            background-color: #f5f5f5 !important;
        }

        .config-container {
            display: none;
            max-width: 800px;
            margin: 50px auto;
            padding: 20px;
        }

        .config-mode .config-container {
            display: block;
        }

        .config-panel {
            background: white;
            padding: 30px;
            border-radius: 10px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }

        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }

        .ip-list {
            margin-bottom: 20px;
        }

        .ip-group {
            margin-bottom: 15px;
            padding: 15px;
            background-color: #f9f9f9;
            border-radius: 5px;
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .ip-number {
            font-weight: bold;
            color: #555;
            min-width: 30px;
        }

        input[type="text"] {
            flex: 1;
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 16px;
        }

        input[type="text"]:focus {
            outline: none;
            border-color: #4CAF50;
        }

        .remove-btn {
            background-color: #f44336;
            color: white;
            border: none;
            padding: 8px 15px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }

        .remove-btn:hover {
            background-color: #d32f2f;
        }

        .add-btn {
            background-color: #2196F3;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin-bottom: 20px;
            width: 100%;
        }

        .add-btn:hover {
            background-color: #1976D2;
        }

        .button-group {
            margin-top: 30px;
            display: flex;
            gap: 10px;
            justify-content: center;
        }

        button {
            padding: 12px 30px;
            border: none;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: background-color 0.3s;
        }

        .save-btn {
            background-color: #4CAF50;
            color: white;
        }

        .save-btn:hover {
            background-color: #45a049;
        }

        .message {
            margin-top: 20px;
            padding: 10px;
            border-radius: 5px;
            text-align: center;
            display: none;
        }

        .success {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }

        .error {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }

        .layout-preview {
            margin-top: 20px;
            padding: 15px;
            background-color: #e3f2fd;
            border-radius: 5px;
            text-align: center;
            font-size: 14px;
            color: #1976D2;
        }

        .help-text {
            font-size: 12px;
            color: #666;
            margin-top: 15px;
            padding: 10px;
            background-color: #f0f0f0;
            border-radius: 5px;
            line-height: 1.6;
        }

        /* Styles pour le mode dashboard */
        .dashboard-container {
            display: none;
            height: 100vh;
            gap: 2px;
            background: #222;
        }

        .dashboard-mode .dashboard-container {
            display: grid;
        }
.frame-container {
    position: relative;
    overflow: hidden;
    background: white;
    padding-top: 40px; /* Espace pour les contrôles */
}

.frame-wrapper {
    width: 100%;
    height: calc(100% - 40px); /* Ajuster la hauteur */
    transform-origin: top left;
    transition: transform 0.3s ease;
}

.zoom-controls {
    position: absolute;
    top: 5px; /* Reste en haut mais dans l'espace réservé */
    right: 5px;
    background: rgba(0,0,0,0.8);
    padding: 5px;
    border-radius: 3px;
    z-index: 10;
    display: flex;
    gap: 5px;
}


        iframe {
            width: 100%;
            height: 100%;
            border: none;
            background: white;
        }

        /* Icône de configuration fixe */
        .config-icon {
            position: fixed;
            top: 5px;
            left: 5px;
            z-index: 1000;
            color: rgba(255, 255, 255, 0.8);
            font-size: min(5vw, 40px);
            cursor: pointer;
            transition: all 0.3s ease;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.5);
            user-select: none;
            display: none;
        }

        .dashboard-mode .config-icon {
            display: block;
        }

        .config-icon:hover {
            color: #FF9800;
            transform: rotate(45deg) scale(1.1);
        }

        /* Fenêtre popup de configuration */
        .config-popup {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: rgba(0, 0, 0, 0.8);
            z-index: 2000;
            overflow-y: auto;
        }

        .config-popup-content {
            background: white;
            margin: 50px auto;
            padding: 30px;
            border-radius: 10px;
            max-width: 800px;
            width: 90%;
            max-height: 80vh;
            overflow-y: auto;
            position: relative;
            animation: popupSlideIn 0.3s ease-out;
        }

@keyframes popupSlideIn {
            from {
                opacity: 0;
                transform: translateY(-50px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }

        .close-popup {
            position: absolute;
            top: 15px;
            right: 20px;
            font-size: 30px;
            color: #999;
            cursor: pointer;
            transition: color 0.3s;
        }

        .close-popup:hover {
            color: #333;
        }

        /* Contrôles de zoom */
        .zoom-controls {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(0,0,0,0.8);
            padding: 5px;
            border-radius: 3px;
            z-index: 10;
            display: flex;
            gap: 5px;
        }

        .zoom-btn {
            width: 25px;
            height: 25px;
            border: none;
            background: #444;
            color: white;
            cursor: pointer;
            border-radius: 3px;
            font-size: 16px;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .zoom-btn:hover {
            background: #666;
        }

        .zoom-fit {
            background: #2196F3;
            width: auto;
            min-width: 25px;
            padding: 0 8px;
            font-size: 11px;
        }

        .zoom-fit:hover {
            background: #1976D2;
        }

        .zoom-value {
            color: white;
            font-family: Arial, sans-serif;
            font-size: 12px;
            min-width: 45px;
            text-align: center;
            display: flex;
            align-items: center;
        }

        /* Label optionnel */
        .frame-label {
            position: absolute;
            top: 5px;
            left: 5px;
            background: rgba(0,0,0,0.7);
            color: white;
            padding: 5px 10px;
            font-family: Arial, sans-serif;
            font-size: 12px;
            border-radius: 3px;
            z-index: 10;
            pointer-events: none;
            max-width: 70%;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        /* Message d'accueil */
        .welcome-message {
            text-align: center;
            padding: 40px;
            background-color: #e3f2fd;
            border-radius: 10px;
            margin-bottom: 30px;
        }

        .welcome-message h2 {
            color: #1976D2;
            margin-bottom: 10px;
        }

        /* Message d'erreur iframe */
        .iframe-error {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: #f8d7da;
            color: #721c24;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
            max-width: 80%;
            z-index: 20;
            display: none;
        }

        .iframe-error h3 {
            margin: 0 0 10px 0;
            font-size: 16px;
        }

        .iframe-error p {
            margin: 5px 0;
            font-size: 14px;
        }

        .iframe-error code {
            background: rgba(0,0,0,0.1);
            padding: 2px 5px;
            border-radius: 3px;
            font-size: 12px;
        }

        /* Boutons d'action de configuration */
        .config-actions {
            display: flex;
            gap: 10px;
            margin-top: 30px;
            flex-wrap: wrap;
            justify-content: center;
        }

        .clear-btn {
            background-color: #F44336;
            flex: 1;
            max-width: 200px;
        }

        .clear-btn:hover {
            background-color: #D32F2F;
        }

        .cancel-btn {
            background-color: #666;
        }

        .cancel-btn:hover {
            background-color: #555;
        }
    </style>
</head>
<body class="config-mode">
    <!-- Icône de configuration fixe -->
    <div class="config-icon" onclick="openConfigPopup()" title="Configuration">
        ⚙️
    </div>

    <!-- Mode Configuration -->
    <div class="config-container">
        <div class="config-panel">
            <h1>Dashboard Multi-Interfaces</h1>

            <div id="welcomeMessage" class="welcome-message">
                <h2>Bienvenue !</h2>
                <p>
                    Configurez vos adresses ci-dessous pour commencer.
                </p>
            </div>

            <div id="ipList" class="ip-list"></div>

            <button class="add-btn" onclick="addIP()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="layoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveAndShowDashboard()">Sauvegarder et afficher</button>
            </div>

            <div class="config-actions">
                <button class="clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="message" class="message"></div>
        </div>
    </div>

    <!-- Popup de configuration -->
    <div class="config-popup" id="configPopup">
        <div class="config-popup-content">
            <span class="close-popup" onclick="closeConfigPopup()">&times;</span>
            <h1>Configuration Dashboard</h1>

            <div id="popupIpList" class="ip-list"></div>

            <button class="add-btn" onclick="addIPPopup()">+ Ajouter une interface</button>

            <div class="help-text">
                <strong>Formats acceptés :</strong><br>
                • IP : 192.168.1.100<br>
                • Domaine : example.com<br>
                • Sous-domaine : app.example.com<br>
                • URL complète : https://example.com/dashboard<br>
                • Port personnalisé : 192.168.1.100:8080 ou example.com:3000
            </div>

            <div id="popupLayoutPreview" class="layout-preview"></div>

            <div class="button-group">
                <button class="save-btn" onclick="saveConfigFromPopup()">Sauvegarder et appliquer</button>
                <button class="cancel-btn" onclick="closeConfigPopup()">Annuler</button>
            </div>

            <div class="config-actions">
                <button class="clear-btn" onclick="clearConfig()">?️ Effacer tout</button>
            </div>

            <div id="popupMessage" class="message"></div>
        </div>
    </div>

    <!-- Mode Dashboard -->
    <div id="dashboardContainer" class="dashboard-container"></div>

    <script>
        // Configuration des adresses - Chargée depuis localStorage
        let addresses = [];
        let zoomLevels = {};
        let currentMode = 'config';
        let autoFitEnabled = true;
        let isPopupMode = false;

        // Clé de stockage localStorage
        const STORAGE_KEY = 'dashboard_config';

        // Initialisation
        window.onload = function() {
            loadConfig();

            if (addresses.length > 0 && addresses[0] !== '') {
                document.getElementById('welcomeMessage').style.display = 'none';
                // Aller directement au dashboard si configuré
                showDashboard();
            } else {
                renderIPList();
            }
        };

        // Charger la configuration depuis localStorage
        function loadConfig() {
            try {
                const savedConfig = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
                if (Array.isArray(savedConfig) && savedConfig.length > 0) {
                    addresses = savedConfig;
                } else {
                    addresses = [''];
                }
            } catch (e) {
                console.error('Erreur lors du chargement de la configuration:', e);
                addresses = [''];
            }
        }

        // Sauvegarder la configuration dans localStorage
        function saveConfig() {
            try {
                const cleanAddresses = addresses.filter(addr => addr.trim() !== '');
                localStorage.setItem(STORAGE_KEY, JSON.stringify(cleanAddresses));
                return true;
            } catch (e) {
                console.error('Erreur lors de la sauvegarde:', e);
                showMessage('Erreur lors de la sauvegarde de la configuration', 'error');
                return false;
            }
        }

        // Ouvrir la popup de configuration
        function openConfigPopup() {
            isPopupMode = true;
            // Copie profonde des adresses pour éviter les modifications directes
            addresses = JSON.parse(JSON.stringify(addresses));
            renderIPListPopup();
            document.getElementById('configPopup').style.display = 'block';
            document.body.style.overflow = 'hidden'; // Désactiver le scroll du body
        }

        // Fermer la popup de configuration
        function closeConfigPopup() {
            isPopupMode = false;
            document.getElementById('configPopup').style.display = 'none';
            document.body.style.overflow = 'auto'; // Réactiver le scroll du body
            loadConfig(); // Recharger la config originale
        }

        // Afficher la liste des adresses (mode principal)
        function renderIPList() {
            const container = document.getElementById('ipList');
            renderIPListInContainer(container, false);
        }

        // Afficher la liste des adresses (popup)
        function renderIPListPopup() {
            const container = document.getElementById('popupIpList');
            renderIPListInContainer(container, true);
        }

        // Fonction générique pour rendre la liste d'IPs
        function renderIPListInContainer(container, isPopup) {
            container.innerHTML = '';

            addresses.forEach((address, index) => {
                const div = document.createElement('div');
                div.className = 'ip-group';
                div.innerHTML = `
                <span class="ip-number">${index + 1}.</span>
                <input type="text" id="${isPopup ? 'popup': ''}address${index}" value="${address}" placeholder="Ex: 192.168.1.100, example.com, https://app.example.com">
                ${addresses.length > 1 ? `<button class="remove-btn" onclick="${isPopup ? 'removeIPPopup': 'removeIP'}(${index})">Supprimer</button>`: ''}
                `;
                container.appendChild(div);
            });

            updateLayoutPreview(isPopup);
        }

        // Ajouter une adresse
        function addIP() {
            addresses.push('');
            renderIPList();
        }

        // Ajouter une adresse (popup)
        function addIPPopup() {
            addresses.push('');
            renderIPListPopup();
        }

        // Supprimer une adresse
        function removeIP(index) {
            addresses.splice(index, 1);
            if (addresses.length === 0) {
                addresses.push('');
            }
            renderIPList();
        }

        // Supprimer une adresse (popup)
        function removeIPPopup(index) {
            addresses.splice(index, 1);
            if (addresses.length === 0) {
                addresses.push('');
            }
            renderIPListPopup();
        }

        // Mettre à jour l'aperçu
        function updateLayoutPreview(isPopup = false) {
            const preview = document.getElementById(isPopup ? 'popupLayoutPreview': 'layoutPreview');
            const count = addresses.filter(addr => addr.trim() !== '').length;

            let layout = '';
            if (count === 0) {
                layout = 'Aucune interface configurée';
            } else if (count === 1) {
                layout = 'Plein écran';
            } else if (count === 2) {
                layout = '2 colonnes côte à côte';
            } else if (count === 3) {
                layout = '1 grande vue à gauche + 2 petites à droite';
            } else if (count === 4) {
                layout = 'Grille 2x2';
            } else if (count <= 6) {
                layout = 'Grille 2x3';
            } else if (count <= 9) {
                layout = 'Grille 3x3';
            } else {
                const cols = Math.ceil(Math.sqrt(count));
                const rows = Math.ceil(count / cols);
                layout = `Grille ${cols}x${rows}`;
            }

            preview.innerHTML = `<strong>Disposition :</strong> ${count} interface${count > 1 ? 's': ''} - ${layout}`;
        }

        // Sauvegarder la configuration depuis la popup
        function saveConfigFromPopup() {
            // Récupérer et valider les adresses depuis la popup
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const input = document.getElementById(`popupaddress${i}`);
                if (input) {
                    const addressValue = input.value.trim();
                    if (addressValue) {
                        const validation = validateAndFormatAddress(addressValue);
                        if (!validation.valid) {
                            showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error', true);
                            return;
                        }
                        newAddresses.push(addressValue);
                    }
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error', true);
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée avec succès !', 'success', true);
                setTimeout(() => {
                    closeConfigPopup();
                    showDashboard();
                }, 1000);
            }
        }

        // Sauvegarder et afficher le dashboard (mode principal)
        function saveAndShowDashboard() {
            // Récupérer et valider les adresses
            const newAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const addressInput = document.getElementById(`address${i}`);
                const addressValue = addressInput ? addressInput.value.trim(): addresses[i];

                if (addressValue) {
                    const validation = validateAndFormatAddress(addressValue);
                    if (!validation.valid) {
                        showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                        return;
                    }
                    newAddresses.push(addressValue);
                }
            }

            if (newAddresses.length === 0) {
                showMessage('Ajoutez au moins une adresse valide', 'error');
                return;
            }

            addresses = newAddresses;

            if (saveConfig()) {
                showMessage('Configuration sauvegardée !', 'success');
                setTimeout(showDashboard, 500);
            }
        }

        // Effacer toute la configuration
        function clearConfig() {
            if (confirm('Êtes-vous sûr de vouloir effacer toute la configuration ?')) {
                localStorage.removeItem(STORAGE_KEY);
                addresses = [''];
                zoomLevels = {};

                if (isPopupMode) {
                    renderIPListPopup();
                } else {
                    renderIPList();
                    document.getElementById('welcomeMessage').style.display = 'block';
                }

                showMessage('Configuration effacée', 'success', isPopupMode);
            }
        }

        // Valider et formater une adresse
        function validateAndFormatAddress(address) {
            address = address.trim();

            if (!address) {
                return {
                    valid: false,
                    error: "L'adresse ne peut pas être vide"
                };
            }

            // URL complète avec protocole
            if (address.match(/^https?:\/\//)) {
                return {
                    valid: true,
                    formatted: address,
                    display: address
                };
            }

            // Pattern IP avec ou sans port
            const ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(?::\d{1,5})?$/;
            if (ipPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            // Pattern domaine avec ou sans port et chemin
            const domainPattern = /^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}(:\d{1,5})?(\/.*)?$/;
            const localhostPattern = /^localhost(:\d{1,5})?(\/.*)?$/;

            if (domainPattern.test(address) || localhostPattern.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            // Pattern nom simple avec port optionnel (pour réseaux locaux)
            if (/^[a-zA-Z0-9-]+(:\d{1,5})?(\/.*)?$/.test(address)) {
                return {
                    valid: true,
                    formatted: `http://${address}`,
                    display: address
                };
            }

            return {
                valid: false,
                error: "Format d'adresse non valide"
            };
        }

        // Afficher un message
        function showMessage(text, type, inPopup = false) {
            const messageEl = document.getElementById(inPopup ? 'popupMessage': 'message');
            messageEl.textContent = text;
            messageEl.className = 'message ' + type;
            messageEl.style.display = 'block';

            setTimeout(() => {
                messageEl.style.display = 'none';
            }, 3000);
        }

        // Calculer la disposition
        function calculateGridLayout(count) {
            if (count === 1) return {
                cols: 1,
                rows: 1,
                special: 'single'
            };
            if (count === 2) return {
                cols: 2,
                rows: 1,
                special: 'two-columns'
            };
            if (count === 3) return {
                cols: 2,
                rows: 2,
                special: 'three-special'
            };
            if (count === 4) return {
                cols: 2,
                rows: 2,
                special: 'grid'
            };

            const cols = Math.ceil(Math.sqrt(count));
            const rows = Math.ceil(count / cols);
            return {
                cols,
                rows,
                special: 'grid'
            };
        }
        function calculateOptimalZoom(frameContainer) {
            const containerWidth = frameContainer.clientWidth;

            // Utiliser la largeur actuelle en tenant compte de l'orientation
            const referenceWidth = Math.max(window.innerWidth, window.innerHeight);

            const optimalZoom = containerWidth / referenceWidth;

            return Math.max(0.2, Math.min(2, optimalZoom));
        }


        // Appliquer le zoom automatique à une iframe
        function autoFitFrame(frameNum) {
            const frameContainer = document.querySelector(`#frame${frameNum}`);
            if (frameContainer && frameContainer.parentElement) {
                const optimalZoom = calculateOptimalZoom(frameContainer.parentElement);
                zoomLevels[frameNum] = optimalZoom;
                updateZoom(frameNum);
            }
        }

        // Appliquer le zoom automatique à toutes les iframes
        function autoFitAll() {
            for (let i = 1; i <= addresses.length; i++) {
                autoFitFrame(i);
            }
        }

        // Afficher le dashboard
        function showDashboard() {
            // Valider les adresses
            const validatedAddresses = [];
            for (let i = 0; i < addresses.length; i++) {
                const validation = validateAndFormatAddress(addresses[i]);
                if (!validation.valid) {
                    showMessage(`Adresse n°${i + 1} : ${validation.error}`, 'error');
                    return;
                }
                validatedAddresses.push(validation);
            }

            // Construire le dashboard avec les adresses formatées
            buildDashboard(validatedAddresses);

            // Basculer en mode dashboard
            document.body.className = 'dashboard-mode';
            currentMode = 'dashboard';

            // Appliquer le zoom automatique après un délai
            setTimeout(() => {
                if (autoFitEnabled) {
                    autoFitAll();
                }
            },
                500);
        }

        // Construire le dashboard
        function buildDashboard(validatedAddresses) {
            const container = document.getElementById('dashboardContainer');
            const layout = calculateGridLayout(validatedAddresses.length);

            // Appliquer les styles de grille
            if (layout.special === 'single') {
                container.style.gridTemplateColumns = '1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'two-columns') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr';
            } else if (layout.special === 'three-special') {
                container.style.gridTemplateColumns = '1fr 1fr';
                container.style.gridTemplateRows = '1fr 1fr';
            } else {
                container.style.gridTemplateColumns = `repeat(${layout.cols}, 1fr)`;
                container.style.gridTemplateRows = `repeat(${layout.rows}, 1fr)`;
            }

            // Créer les conteneurs
            container.innerHTML = '';
            validatedAddresses.forEach((addressInfo, index) => {
                const frameDiv = document.createElement('div');
                frameDiv.className = 'frame-container';

                // Style spécial pour 3 interfaces
                if (layout.special === 'three-special' && index === 0) {
                    frameDiv.style.gridRow = 'span 2';
                }

                const frameNum = index + 1;
                frameDiv.innerHTML = `
                <span class="frame-label" title="${addressInfo.display}">${addressInfo.display}</span>
                <div class="zoom-controls">
                <button class="zoom-btn" onclick="zoom(${frameNum}, -0.1)">−</button>
                <span class="zoom-value" id="zoom${frameNum}">100%</span>
                <button class="zoom-btn" onclick="zoom(${frameNum}, 0.1)">+</button>
                <button class="zoom-btn zoom-fit" onclick="autoFitFrame(${frameNum})" title="Ajuster à l'écran">FIT</button>
                </div>
                <div class="frame-wrapper" id="frame${frameNum}">
                <iframe
                src="${addressInfo.formatted}"
                sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals allow-downloads allow-presentation allow-top-navigation"
                referrerpolicy="no-referrer"
                id="iframe${frameNum}"
                onerror="handleIframeError(${frameNum}, '${addressInfo.display}')"
                ></iframe>
                </div>
                <div class="iframe-error" id="error${frameNum}">
                <h3>⚠️ Impossible de charger ${addressInfo.display}</h3>
                <p>Cette interface ne peut pas être affichée dans une iframe.</p>
                <p><small>Pour Home Assistant, ajoutez dans configuration.yaml :</small></p>
                <code>http:<br>&nbsp;&nbsp;use_x_frame_options: false</code>
                <p style="margin-top: 15px;">
                <a href="${addressInfo.formatted}" target="_blank" style="color: #0056b3;">Ouvrir dans un nouvel onglet →</a>
                </p>
                </div>
                `;

                container.appendChild(frameDiv);
            });

            // Initialiser les niveaux de zoom
            zoomLevels = {};
            for (let i = 1; i <= validatedAddresses.length; i++) {
                zoomLevels[i] = 1;
            }

            // Vérifier le chargement des iframes après un délai
            setTimeout(checkIframeLoading, 2000);
        }

        // Retourner à la configuration
        function showConfig() {
            document.body.className = 'config-mode';
            currentMode = 'config';
            renderIPList();
        }

        // Fonctions de zoom
        function zoom(frameNum, delta) {
            const currentZoom = zoomLevels[frameNum] || 1;
            zoomLevels[frameNum] = Math.max(0.2, Math.min(2, currentZoom + delta));
            updateZoom(frameNum);
        }

        function updateZoom(frameNum) {
            const frame = document.getElementById(`frame${frameNum}`);
            const zoomDisplay = document.getElementById(`zoom${frameNum}`);

            if (!frame || !zoomDisplay) return;

            const scale = zoomLevels[frameNum] || 1;

            frame.style.transform = `scale(${scale})`;
            frame.style.width = `${100 / scale}%`;
            frame.style.height = `${100 / scale}%`;
            zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
        }

        // Gestion des erreurs d'iframe
        function handleIframeError(frameNum, address) {
            console.error(`Erreur de chargement pour l'iframe ${frameNum}: ${address}`);
            const errorDiv = document.getElementById(`error${frameNum}`);
            if (errorDiv) {
                errorDiv.style.display = 'block';
            }
        }

        // Détection des erreurs de chargement après un délai
        function checkIframeLoading() {
            if (currentMode === 'dashboard') {
                for (let i = 1; i <= addresses.length; i++) {
                    const iframe = document.getElementById(`iframe${i}`);
                    if (iframe) {
                        iframe.addEventListener('load', function() {
                            try {
                                // Tenter d'accéder au document pour vérifier si c'est bloqué
                                const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                                // Si on arrive ici, l'iframe est chargée correctement
                                console.log(`Iframe ${i} chargée avec succès`);
                            } catch (e) {
                                // Erreur de cross-origin, probablement bloqué
                                console.warn(`L'iframe ${i} pourrait être bloquée par des politiques de sécurité`);
                            }
                        });

                        iframe.addEventListener('error', function() {
                            handleIframeError(i, addresses[i - 1]);
                        });
                    }
                }
            }
        }

        // Gestion du redimensionnement de la fenêtre
        let resizeTimeout;
        window.addEventListener('resize', function() {
            if (currentMode === 'dashboard' && autoFitEnabled) {
                clearTimeout(resizeTimeout);
                resizeTimeout = setTimeout(() => {
                    autoFitAll();
                }, 100);
            }
        });

        // Fermer la popup avec Escape
        window.addEventListener('keydown', function(e) {
            if (e.key === 'Escape' && document.getElementById('configPopup').style.display === 'block') {
                closeConfigPopup();
            }
        });

        // Fermer la popup en cliquant en dehors
        document.getElementById('configPopup').addEventListener('click', function(e) {
            if (e.target === this) {
                closeConfigPopup();
            }
        });
    </script>
</body>
</html>



RE: Petit prog pour esp32 - Sgb31 - 31-05-2025

Bonjour,
Testé ce jour la version HTML ( pas eu le temps de tester la version ESP), franchement top et simple de mise en œuvre.
Merci Lucky de cette excellente idée
Bonne journée à tous ;-)


RE: Petit prog pour esp32 - lucky - 31-05-2025

version esp32 sur RMS 14.23

je fais une modif version esp rms revient bientôt, voili un ptit problème de mémoire

https://mega.nz/file/wIIE3boL#JGMRjtR_jOcBV_ZmGuxfimkRzaxnD9GuBs0GMyCYyJ4

faites moi un retour svp


RE: Petit prog pour esp32 - pdunet - 02-06-2025

(31-05-2025, 12:01 PM)lucky a écrit : version esp32 sur RMS 14.23

je fais une modif version esp rms revient bientôt, voili un ptit problème de mémoire

https://mega.nz/file/wIIE3boL#JGMRjtR_jOcBV_ZmGuxfimkRzaxnD9GuBs0GMyCYyJ4

faites moi un retour svp

Merci lucky, ce fichier fonctionne très bien.

L'esp hôte héberge donc un routeur et permet de monitorer 4 routeurs supplémentaires.
Donc 2 pages :
- Routeur hébergeur
- Dashboard de 4 routeurs supplémentaires

J'utilise un tout petit bout du code html pour suivre mes deux routeurs sur le téléphone et c'est bien agréable.

Par ailleurs, je n'ai pas vu si un état de besoins d'affichages a été fait.
Mon souhait serait un Dashboard sur un CYD/esp écran qui ferait une synthèse des infos de deux esp/routeurs, un JSY et un production solaire ET je suis certain qu’il y a beaucoup d'autres idées/besoins ET que cela demande beaucoup de temps de développement.

En tout cas merci pour cette avancée,
Très bonne journée,
Paul