Note de ce sujet :
  • Moyenne : 0 (0 vote(s))
  • 1
  • 2
  • 3
  • 4
  • 5
Petit prog pour esp32
#61
(08-06-2025, 07:50 PM)lucky a écrit : Bonsoir,

alors version améliorée pour tel en position verticale scrolle droite gauche pour passer d'une interface à l'autre et horizontal retour mode normal
zoom amélioré avec 2 doigts etc etc

version esp seul
https://mega.nz/file/sJwDVJYZ#L2g0jNebOn...axhBJbsZRg
version esp rms 14.23 avec timezone
https://mega.nz/file/9NAUwRDJ#Z2-yc7zIFV...wrQ5P_NyIo

faite moi un retour merci

Encore une fois merci lucky,

Je viens de tester la version esp seul pour tel.

Cependant, je préfère une version dasboard.html, pas de esp dédié et plus réactive.

Mais une version dasboard.html ne donne pas un aperçu exploitable sur tel, pas étonnant, pas prévu pour :
-fenêtre coupée en deux à la verticale
-zoom difficilement exploitable

Alors j'utilise ce petit bout de code que j'avais trouvé, puis oublié et retrouvé grâce à ton intervention :
- problème les fenêtres empilées toutes petites ne sont pas top
--------------------------------------------------------------------------------------
RMS.mhtml que j'exécute sur le tel y compris depuis l'extérieur
--------------------------------------------------------------------------------------
Code :
<!DOCTYPE html>
<html lang="fr-FR">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Test</title>
  </head>
  <body>
    <iframe src="http://xx.xxx.xx.xxx:49999/" width="1024" height="1000"></iframe>
    <iframe src="http://xx.xxx.xx.xxx:50000/" width="1024" height="1000"></iframe>
  </body>
</html>

ALORS? Et bien ta nouvelle version avec scroll droite<>gauche est beaucoup plus agréable  Smile

Une version dashboard.mhtml allégée pour tel est-elle possible ?

Très bonne journée,
Paul


Pièces jointes Miniature(s)
           
Routeur UxIx2 (Maison et CE) - Dimmer Robotdyn avec triac BTA40 - Sonde T° sur CE - 4 PV 400Wc sur 2 PowerStream
Répondre
#62
slt
voilà version html améliorée

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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            background: #1a1a2e;
            overflow: hidden;
        }
       
        /* Mode configuration */
        .config-mode {
            background: #1a1a2e !important;
            overflow: auto !important;
        }
       
        .config-container {
            display: none;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            min-height: 100vh;
        }
       
        .config-mode .config-container {
            display: block;
        }
       
        .config-panel {
            background: rgba(255,255,255,0.1);
            padding: 30px;
            border-radius: 10px;
            backdrop-filter: blur(10px);
        }
       
        h1 {
            color: #4CAF50;
            text-align: center;
            margin-bottom: 30px;
        }
       
        .section {
            background: rgba(0,0,0,0.3);
            padding: 20px;
            margin: 20px 0;
            border-radius: 5px;
        }
       
        h2 {
            color: #2196F3;
            margin-top: 0;
            font-size: 18px;
        }
       
        .interface-group {
            display: grid;
            grid-template-columns: 2fr 1fr;
            gap: 10px;
            margin: 15px 0;
            align-items: center;
        }
       
        input[type="text"], input[type="password"] {
            width: 100%;
            padding: 12px;
            border: 1px solid #555;
            border-radius: 5px;
            background: #333;
            color: white;
            font-size: 16px;
            box-sizing: border-box;
            transition: border-color 0.3s;
        }
       
        input[type="text"]:focus, input[type="password"]:focus {
            outline: none;
            border-color: #4CAF50;
            background: #444;
        }
       
        .remove-btn {
            background: #f44336;
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.3s;
            min-height: 44px;
        }
       
        .remove-btn:hover {
            background: #d32f2f;
        }
       
        .add-btn {
            background: #2196F3;
            color: white;
            border: none;
            padding: 12px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin: 20px 0;
            width: 100%;
            transition: background 0.3s;
        }
       
        .add-btn:hover {
            background: #1976D2;
        }
       
        .save-btn {
            background: #4CAF50;
            color: white;
            border: none;
            padding: 14px 30px;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s;
            width: 100%;
            margin: 10px 0;
        }
       
        .save-btn:hover {
            background: #45a049;
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(76,175,80,0.3);
        }
       
        .export-btn {
            background: #FF5722;
            color: white;
            border: none;
            padding: 14px 30px;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s;
            width: 100%;
            margin: 10px 0;
        }
       
        .export-btn:hover {
            background: #E64A19;
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(255,87,34,0.3);
        }
       
        .help-text {
            font-size: 13px;
            color: #aaa;
            margin: 15px 0;
            padding: 15px;
            background: rgba(255,255,255,0.05);
            border-radius: 5px;
            line-height: 1.6;
        }
       
        .example {
            font-family: 'Courier New', monospace;
            background: rgba(255,255,255,0.1);
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 12px;
        }
       
        .layout-preview {
            margin: 20px 0;
            padding: 20px;
            background: #2196F3;
            color: white;
            border-radius: 5px;
            text-align: center;
            font-weight: bold;
        }
       
        .message {
            margin: 20px 0;
            padding: 15px;
            border-radius: 5px;
            text-align: center;
            display: none;
            animation: slideIn 0.3s ease;
        }
       
        @keyframes slideIn {
            from {
                opacity: 0;
                transform: translateY(-10px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
       
        .success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
       
        .error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
       
        /* Mode Dashboard */
        .dashboard-container {
            display: none;
            height: 100vh;
            gap: 2px;
            background: #222;
        }
       
        .dashboard-mode .dashboard-container {
            display: grid;
        }
       
        /* Layouts responsifs */
        .layout-1 {
            grid-template-columns: 1fr;
            grid-template-rows: 1fr;
        }
       
        .layout-2 {
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr;
        }
       
        .layout-3 {
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
        }
       
        .layout-3 .frame-container:first-child {
            grid-row: span 2;
        }
       
        .layout-4 {
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
        }
       
        /* Mobile responsive layouts */
        @media (max-width: 768px) and (orientation: portrait) {
            .layout-2, .layout-3, .layout-4 {
                display: flex !important;
                overflow-x: auto !important;
                scroll-snap-type: x mandatory !important;
                gap: 0 !important;
                -webkit-overflow-scrolling: touch;
            }
           
            .frame-container {
                flex: 0 0 100vw;
                scroll-snap-align: start;
                min-height: 100vh;
            }
           
            .mobile-indicator {
                position: fixed;
                bottom: 70px;
                left: 50%;
                transform: translateX(-50%);
                display: flex;
                gap: 8px;
                z-index: 100;
                background: rgba(0,0,0,0.7);
                padding: 8px 12px;
                border-radius: 20px;
            }
           
            .dot {
                width: 8px;
                height: 8px;
                border-radius: 50%;
                background: rgba(255,255,255,0.5);
                transition: all 0.3s;
            }
           
            .dot.active {
                background: white;
                transform: scale(1.3);
            }
        }
       
        @media (max-width: 768px) and (orientation: landscape) {
            .layout-4 {
                grid-template-columns: 1fr 1fr !important;
            }
           
            .layout-3 .frame-container:first-child {
                grid-row: 1;
            }
        }
       
        .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;
        }
       
        /* Header avec bouton fullscreen uniquement */
        .frame-header {
            position: absolute;
            top: 5px;
            left: 5px;
            z-index: 10;
        }
       
        /* Bouton fullscreen */
        .fullscreen-btn {
            background: rgba(0,0,0,0.8);
            color: white;
            border: none;
            padding: 8px 10px;
            font-size: 16px;
            border-radius: 3px;
            cursor: pointer;
            transition: background 0.2s;
            min-width: 36px;
            min-height: 36px;
        }
       
        .fullscreen-btn:hover {
            background: rgba(0,0,0,0.95);
        }
       
        .fullscreen-btn.exit {
            background: rgba(220,53,69,0.9);
        }
       
        .fullscreen-btn.exit:hover {
            background: rgba(220,53,69,1);
        }
       
        /* Plein écran */
        .frame-container.fullscreen-active {
            position: fixed !important;
            top: 0 !important;
            left: 0 !important;
            width: 100vw !important;
            height: 100vh !important;
            z-index: 9999 !important;
            display: block !important;
        }
       
        /* Contrôles de zoom draggables */
        .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;
            cursor: move;
            user-select: none;
            transition: box-shadow 0.2s;
        }
       
        .zoom-controls:hover {
            background: rgba(0,0,0,0.95);
            box-shadow: 0 2px 5px rgba(0,0,0,0.5);
        }
       
        .zoom-controls.dragging {
            opacity: 0.8;
            box-shadow: 0 5px 15px rgba(0,0,0,0.5);
            z-index: 1000;
        }
       
        .zoom-btn {
            width: 30px;
            height: 30px;
            border: none;
            background: #444;
            color: white;
            cursor: pointer;
            border-radius: 3px;
            font-size: 18px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s;
        }
       
        .zoom-btn:hover {
            background: #666;
        }
       
        .zoom-value {
            color: white;
            font-size: 12px;
            min-width: 45px;
            text-align: center;
            display: flex;
            align-items: center;
            justify-content: center;
        }
       
        /* Boutons globaux */
        .global-controls {
            display: none;
            position: fixed;
            bottom: 10px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.9);
            padding: 10px 20px;
            border-radius: 25px;
            z-index: 100;
            backdrop-filter: blur(10px);
        }
       
        .dashboard-mode .global-controls {
            display: block;
        }
       
        .global-btn {
            background: #4CAF50;
            color: white;
            border: none;
            padding: 8px 15px;
            margin: 0 5px;
            border-radius: 20px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.3s;
        }
       
        .global-btn:hover {
            background: #45a049;
            transform: translateY(-2px);
        }
       
        .config-btn {
            background: #FF9800;
        }
       
        .config-btn:hover {
            background: #F57C00;
        }
       
        /* Mobile optimizations */
        @media (max-width: 768px) {
            .zoom-controls {
                display: none !important;
            }
           
            .frame-header {
                font-size: 12px;
                padding: 6px 10px;
            }
           
            .fullscreen-btn {
                min-width: 44px;
                min-height: 44px;
            }
           
            .global-controls {
                bottom: 20px;
                padding: 12px 16px;
            }
           
            .global-btn {
                padding: 10px 18px;
                font-size: 16px;
            }
           
            /* Auto-zoom mobile */
            .frame-wrapper {
                transform: scale(0.5) !important;
                width: 200% !important;
                height: 200% !important;
            }
           
            .frame-container.fullscreen-active .frame-wrapper {
                transform: scale(1) !important;
                width: 100% !important;
                height: 100% !important;
            }
        }
       
        /* Message d'erreur iframe */
        .iframe-error {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(248,215,218,0.95);
            color: #721c24;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
            max-width: 80%;
            z-index: 20;
            display: none;
            backdrop-filter: blur(5px);
        }
       
        .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: 4px 8px;
            border-radius: 3px;
            font-size: 12px;
            display: inline-block;
            margin: 5px 0;
        }
       
        .iframe-error a {
            color: #0056b3;
            text-decoration: none;
            font-weight: bold;
        }
       
        .iframe-error a:hover {
            text-decoration: underline;
        }
       
        /* Authentification */
        .auth-section {
            background: rgba(76,175,80,0.1);
            border: 1px solid rgba(76,175,80,0.3);
        }
       
        .auth-help {
            font-size: 12px;
            color: #aaa;
            margin-top: 10px;
        }
       
        /* Loading spinner */
        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            border-top-color: #4CAF50;
            animation: spin 1s ease-in-out infinite;
        }
       
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
       
        /* Welcome message */
        .welcome-message {
            text-align: center;
            padding: 40px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 10px;
            margin-bottom: 30px;
            color: white;
        }
       
        .welcome-message h2 {
            color: white;
            margin-bottom: 10px;
            font-size: 28px;
        }
       
        .welcome-message p {
            font-size: 16px;
            opacity: 0.9;
        }

        /* URL validation indicator */
        .url-status {
            position: absolute;
            right: 10px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 20px;
        }
       
        .url-valid { color: #4CAF50; }
        .url-invalid { color: #f44336; }
    </style>
</head>
<body class="config-mode">
    <!-- 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 interfaces pour créer votre dashboard personnalisé</p>
            </div>
           
            <div class="section auth-section">
                <h2>? Sécurité du Dashboard (optionnel)</h2>
                <label>Nom d'utilisateur :</label>
                <input type="text" id="authUser" placeholder="admin">
               
                <label>Mot de passe :</label>
                <input type="password" id="authPass" placeholder="Mot de passe">
               
                <p class="auth-help">Laissez vide pour désactiver l'authentification</p>
            </div>
           
            <div class="section">
                <h2>? Configuration des Interfaces</h2>
                <div class="help-text">
                    <strong>Formats acceptés :</strong><br>
                    • <span class="example">192.168.1.100</span> → IP simple<br>
                    • <span class="example">192.168.1.100:8080</span> → IP avec port<br>
                    • <span class="example">monserveur.local</span> → Domaine local<br>
                    • <span class="example">https://app.example.com</span> → URL complète<br>
                    • <span class="example">http://serveur.com:3000/dashboard</span> → URL avec chemin
                </div>
               
                <div id="interfacesList"></div>
               
                <button class="add-btn" onclick="addInterface()">➕ Ajouter une interface</button>
            </div>
           
            <div id="layoutPreview" class="layout-preview"></div>
           
            <button class="save-btn" onclick="showDashboard()">? Afficher le dashboard</button>
            <button class="export-btn" onclick="exportConfig()">? Exporter la configuration</button>
           
            <div id="message" class="message"></div>
        </div>
    </div>
   
    <!-- Mode Dashboard -->
    <div id="dashboardContainer" class="dashboard-container"></div>
   
    <!-- Contrôles globaux -->
    <div class="global-controls">
        <button class="global-btn" onclick="resetAllZoom()">↺ Réinitialiser</button>
        <button class="global-btn" onclick="zoomAll(-0.1)">➖ Réduire tout</button>
        <button class="global-btn" onclick="zoomAll(0.1)">➕ Agrandir tout</button>
        <button class="global-btn config-btn" onclick="showConfig()">⚙️ Configuration</button>
    </div>
   
    <!-- Indicateur mobile -->
    <div class="mobile-indicator" id="mobileIndicator" style="display: none;"></div>
   
    <script>
        // Configuration par défaut
        let config = {
            interfaces: [
                { url: '', name: 'Interface 1' },
                { url: '', name: 'Interface 2' }
            ],
            authUser: '',
            authPass: ''
        };
       
        let zoomLevels = {};
        let currentFullscreen = null;
        let isAuthenticated = false;
       
        // Charger la configuration sauvegardée
        function loadConfig() {
            const saved = localStorage.getItem('dashboardConfig');
            if (saved) {
                try {
                    config = JSON.parse(saved);
                    // Assurer au moins 2 interfaces
                    while (config.interfaces.length < 2) {
                        config.interfaces.push({ url: '', name: `Interface ${config.interfaces.length + 1}` });
                    }
                } catch (e) {
                    console.error('Erreur chargement config:', e);
                }
            }
        }
       
        // Sauvegarder la configuration
        function saveConfig() {
            localStorage.setItem('dashboardConfig', JSON.stringify(config));
        }
       
        // Valider et normaliser une URL
        function normalizeURL(url) {
            if (!url) return '';
           
            url = url.trim();
           
            // Si pas de protocole, ajouter http://
            if (!url.match(/^https?:\/\//)) {
                url = 'http://' + url;
            }
           
            return url;
        }
       
        // Valider une URL
        function validateURL(url) {
            if (!url) return false;
           
            try {
                new URL(normalizeURL(url));
                return true;
            } catch (e) {
                return false;
            }
        }
       
        // Afficher la liste des interfaces
        function renderInterfaces() {
            const container = document.getElementById('interfacesList');
            container.innerHTML = '';
           
            config.interfaces.forEach((interface, index) => {
                const div = document.createElement('div');
                div.className = 'interface-group';
               
                const urlInput = document.createElement('input');
                urlInput.type = 'text';
                urlInput.value = interface.url;
                urlInput.placeholder = `URL Interface ${index + 1} (ex: 192.168.1.100:8080)`;
                urlInput.oninput = (e) => {
                    config.interfaces[index].url = e.target.value;
                    updateLayoutPreview();
                    updateURLStatus(urlInput);
                };
               
                const nameInput = document.createElement('input');
                nameInput.type = 'text';
                nameInput.value = interface.name;
                nameInput.placeholder = `Nom ${index + 1}`;
                nameInput.oninput = (e) => {
                    config.interfaces[index].name = e.target.value || `Interface ${index + 1}`;
                };
               
                div.appendChild(urlInput);
                div.appendChild(nameInput);
               
                // Bouton supprimer (sauf pour les 2 premières)
                if (index >= 2) {
                    const removeBtn = document.createElement('button');
                    removeBtn.className = 'remove-btn';
                    removeBtn.textContent = '✕';
                    removeBtn.onclick = () => removeInterface(index);
                    div.appendChild(removeBtn);
                }
               
                container.appendChild(div);
               
                // Validation initiale
                updateURLStatus(urlInput);
            });
           
            updateLayoutPreview();
        }
       
        // Indicateur de validation d'URL
        function updateURLStatus(input) {
            // Supprimer l'ancien indicateur
            const oldStatus = input.parentElement.querySelector('.url-status');
            if (oldStatus) oldStatus.remove();
           
            if (input.value) {
                const status = document.createElement('span');
                status.className = 'url-status';
               
                if (validateURL(input.value)) {
                    status.className += ' url-valid';
                    status.textContent = '✓';
                } else {
                    status.className += ' url-invalid';
                    status.textContent = '✗';
                }
               
                input.parentElement.style.position = 'relative';
                input.parentElement.appendChild(status);
            }
        }
       
        // Ajouter une interface
        function addInterface() {
            if (config.interfaces.length < 4) {
                config.interfaces.push({
                    url: '',
                    name: `Interface ${config.interfaces.length + 1}`
                });
                renderInterfaces();
            } else {
                showMessage('Maximum 4 interfaces', 'error');
            }
        }
       
        // Supprimer une interface
        function removeInterface(index) {
            config.interfaces.splice(index, 1);
            renderInterfaces();
        }
       
        // Mettre à jour l'aperçu
        function updateLayoutPreview() {
            const preview = document.getElementById('layoutPreview');
            const validInterfaces = config.interfaces.filter(i => i.url).length;
           
            let layout = '';
            switch(validInterfaces) {
                case 1: layout = '?️ Plein écran'; break;
                case 2: layout = '? 2 colonnes côte à côte'; break;
                case 3: layout = '? 1 grande vue + 2 petites'; break;
                case 4: layout = '⚏ Grille 2x2'; break;
                default: layout = '❓ Configurez au moins une interface';
            }
           
            preview.innerHTML = `<strong>Disposition :</strong> ${validInterfaces} interface${validInterfaces > 1 ? 's' : ''} - ${layout}`;
        }
       
        // Afficher un message
        function showMessage(text, type) {
            const messageEl = document.getElementById('message');
            messageEl.textContent = text;
            messageEl.className = 'message ' + type;
            messageEl.style.display = 'block';
           
            setTimeout(() => {
                messageEl.style.display = 'none';
            }, 3000);
        }
       
        // Vérifier l'authentification
        function checkAuth() {
            if (!config.authUser || !config.authPass) {
                return true; // Pas d'auth configurée
            }
           
            if (isAuthenticated) {
                return true;
            }
           
            const user = prompt('Nom d\'utilisateur :');
            const pass = prompt('Mot de passe :');
           
            if (user === config.authUser && pass === config.authPass) {
                isAuthenticated = true;
                return true;
            }
           
            alert('Identifiants incorrects');
            return false;
        }
       
        // Afficher le dashboard
        function showDashboard() {
            // Sauvegarder l'auth
            config.authUser = document.getElementById('authUser').value;
            config.authPass = document.getElementById('authPass').value;
           
            // Valider les interfaces
            const validInterfaces = config.interfaces.filter(i => i.url && validateURL(i.url));
           
            if (validInterfaces.length === 0) {
                showMessage('Configurez au moins une interface valide', 'error');
                return;
            }
           
            // Sauvegarder
            saveConfig();
           
            // Vérifier l'auth
            if (!checkAuth()) {
                return;
            }
           
            // Construire le dashboard
            buildDashboard(validInterfaces);
           
            // Basculer en mode dashboard
            document.body.className = 'dashboard-mode';
            document.getElementById('welcomeMessage').style.display = 'none';
        }
       
        // Construire le dashboard
        function buildDashboard(interfaces) {
            const container = document.getElementById('dashboardContainer');
            container.innerHTML = '';
            container.className = `dashboard-container layout-${interfaces.length}`;
           
            interfaces.forEach((interface, index) => {
                const frameDiv = document.createElement('div');
                frameDiv.className = 'frame-container';
                frameDiv.id = `frame${index + 1}-container`;
               
                frameDiv.innerHTML = `
                    <div class="frame-header">
                        <button class="fullscreen-btn" onclick="toggleFullscreen(${index + 1})" id="fullscreen-btn-${index + 1}">⛶</button>
                    </div>
                    <div class="zoom-controls" id="zoom-controls-${index + 1}">
                        <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>
                    </div>
                    <div class="frame-wrapper" id="wrapper${index + 1}">
                        <iframe
                            id="frame${index + 1}"
                            src="${normalizeURL(interface.url)}"
                            sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-top-navigation"
                            referrerpolicy="no-referrer"
                        ></iframe>
                    </div>
                    <div class="iframe-error" id="error${index + 1}">
                        <h3>⚠️ Impossible de charger cette interface</h3>
                        <p>Cette interface ne peut pas être affichée dans une iframe.</p>
                        <p><small>Certains sites bloquent l'affichage dans des iframes pour des raisons de sécurité.</small></p>
                        <p style="margin-top: 15px;">
                            <a href="${normalizeURL(interface.url)}" target="_blank">Ouvrir dans un nouvel onglet →</a>
                        </p>
                    </div>
                `;
               
                container.appendChild(frameDiv);
               
                // Initialiser le zoom
                zoomLevels[index + 1] = 1;
               
                // Gérer les erreurs
                const iframe = frameDiv.querySelector('iframe');
                iframe.addEventListener('error', () => {
                    document.getElementById(`error${index + 1}`).style.display = 'block';
                });
               
                // Vérifier le chargement après un délai
                setTimeout(() => {
                    try {
                        const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                        // Si on peut accéder au document, c'est bon
                    } catch (e) {
                        // Erreur de cross-origin, possiblement bloqué
                        console.warn(`L'iframe ${index + 1} pourrait être bloquée`);
                    }
                }, 3000);
            });
           
            // Rendre les contrôles de zoom draggables
            setTimeout(makeZoomControlsDraggable, 100);
           
            // Setup mobile si nécessaire
            if (isMobile()) {
                setupMobileNavigation(interfaces.length);
            }
        }
       
        // Détection mobile
        function isMobile() {
            return window.innerWidth <= 768;
        }
       
        // Navigation mobile
        function setupMobileNavigation(count) {
            const indicator = document.getElementById('mobileIndicator');
            indicator.style.display = 'flex';
            indicator.innerHTML = '';
           
            for (let i = 0; i < count; i++) {
                const dot = document.createElement('div');
                dot.className = 'dot';
                if (i === 0) dot.classList.add('active');
                indicator.appendChild(dot);
            }
           
            const container = document.getElementById('dashboardContainer');
            container.addEventListener('scroll', () => {
                const scrollLeft = container.scrollLeft;
                const width = window.innerWidth;
                const currentIndex = Math.round(scrollLeft / width);
               
                document.querySelectorAll('.dot').forEach((dot, index) => {
                    dot.classList.toggle('active', index === currentIndex);
                });
            });
        }
       
        // Zoom
        function zoom(frameNum, delta) {
            zoomLevels[frameNum] = Math.max(0.2, Math.min(2, (zoomLevels[frameNum] || 1) + delta));
            updateZoom(frameNum);
        }
       
        function updateZoom(frameNum) {
            const wrapper = document.getElementById(`wrapper${frameNum}`);
            const zoomDisplay = document.getElementById(`zoom${frameNum}`);
            if (!wrapper || !zoomDisplay) return;
           
            const scale = zoomLevels[frameNum];
            wrapper.style.transform = `scale(${scale})`;
            wrapper.style.width = `${100 / scale}%`;
            wrapper.style.height = `${100 / scale}%`;
            zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
        }
       
        function resetAllZoom() {
            Object.keys(zoomLevels).forEach(frameNum => {
                zoomLevels[frameNum] = 1;
                updateZoom(frameNum);
            });
        }
       
        function zoomAll(delta) {
            Object.keys(zoomLevels).forEach(frameNum => {
                zoom(parseInt(frameNum), delta);
            });
        }
       
        // Plein écran
        function toggleFullscreen(frameNum) {
            const container = document.getElementById(`frame${frameNum}-container`);
            const btn = document.getElementById(`fullscreen-btn-${frameNum}`);
           
            if (!container) return;
           
            if (currentFullscreen === frameNum) {
                // Sortir du plein écran
                container.classList.remove('fullscreen-active');
                btn.textContent = '⛶';
                btn.classList.remove('exit');
                currentFullscreen = null;
               
                // Réafficher les autres
                document.querySelectorAll('.frame-container').forEach(fc => {
                    fc.style.display = '';
                });
            } else {
                // Sortir du plein écran actuel si existe
                if (currentFullscreen) {
                    toggleFullscreen(currentFullscreen);
                }
               
                // Entrer en plein écran
                container.classList.add('fullscreen-active');
                btn.textContent = '✕';
                btn.classList.add('exit');
                currentFullscreen = frameNum;
               
                // Masquer les autres
                document.querySelectorAll('.frame-container').forEach(fc => {
                    if (fc.id !== `frame${frameNum}-container`) {
                        fc.style.display = 'none';
                    }
                });
            }
        }
       
        // Rendre les contrôles de zoom draggables
        function makeZoomControlsDraggable() {
            document.querySelectorAll('.zoom-controls').forEach(controls => {
                let isDragging = false;
                let currentX;
                let currentY;
                let initialX;
                let initialY;
               
                controls.addEventListener('mousedown', dragStart);
                controls.addEventListener('touchstart', handleTouch);
               
                function handleTouch(e) {
                    const touch = e.touches[0];
                    dragStart({
                        clientX: touch.clientX,
                        clientY: touch.clientY,
                        target: e.target
                    });
                }
               
                function dragStart(e) {
                    if (e.target.tagName === 'BUTTON') return;
                   
                    isDragging = true;
                    initialX = e.clientX - controls.offsetLeft;
                    initialY = e.clientY - controls.offsetTop;
                    controls.classList.add('dragging');
                   
                    document.addEventListener('mousemove', drag);
                    document.addEventListener('mouseup', dragEnd);
                    document.addEventListener('touchmove', handleTouchMove);
                    document.addEventListener('touchend', dragEnd);
                }
               
                function handleTouchMove(e) {
                    const touch = e.touches[0];
                    drag({
                        clientX: touch.clientX,
                        clientY: touch.clientY
                    });
                }
               
                function drag(e) {
                    if (!isDragging) return;
                   
                    e.preventDefault();
                    currentX = e.clientX - initialX;
                    currentY = e.clientY - initialY;
                   
                    const container = controls.closest('.frame-container');
                    const maxX = container.offsetWidth - controls.offsetWidth - 5;
                    const maxY = container.offsetHeight - controls.offsetHeight - 5;
                   
                    currentX = Math.max(5, Math.min(maxX, currentX));
                    currentY = Math.max(5, Math.min(maxY, currentY));
                   
                    controls.style.left = currentX + 'px';
                    controls.style.top = currentY + 'px';
                    controls.style.right = 'auto';
                }
               
                function dragEnd() {
                    isDragging = false;
                    controls.classList.remove('dragging');
                    document.removeEventListener('mousemove', drag);
                    document.removeEventListener('mouseup', dragEnd);
                    document.removeEventListener('touchmove', handleTouchMove);
                    document.removeEventListener('touchend', dragEnd);
                }
            });
        }
       
        // Retour à la configuration
        function showConfig() {
            document.body.className = 'config-mode';
            renderInterfaces();
           
            // Restaurer les valeurs d'auth
            document.getElementById('authUser').value = config.authUser || '';
            document.getElementById('authPass').value = config.authPass || '';
        }
       
        // Exporter la configuration
        function exportConfig() {
            // Sauvegarder d'abord la config actuelle
            config.authUser = document.getElementById('authUser').value;
            config.authPass = document.getElementById('authPass').value;
            saveConfig();
           
            const dataStr = JSON.stringify(config, null, 2);
            const dataBlob = new Blob([dataStr], { type: 'application/json' });
           
            const link = document.createElement('a');
            link.href = URL.createObjectURL(dataBlob);
            link.download = 'dashboard-config.json';
            link.click();
           
            showMessage('Configuration exportée !', 'success');
        }
       
        // Gestion Escape pour sortir du plein écran
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && currentFullscreen) {
                toggleFullscreen(currentFullscreen);
            }
        });
       
        // Gestion du redimensionnement
        window.addEventListener('resize', () => {
            if (isMobile() && document.body.className === 'dashboard-mode') {
                const validInterfaces = config.interfaces.filter(i => i.url);
                setupMobileNavigation(validInterfaces.length);
            }
        });
       
        // Initialisation
        window.onload = () => {
            loadConfig();
            renderInterfaces();
           
            // Si config déjà présente, masquer le message de bienvenue
            if (config.interfaces.some(i => i.url)) {
                document.getElementById('welcomeMessage').style.display = 'none';
            }
        };
    </script>
</body>
</html>
ESP32Wroom, Triac 40A "BTA40", Source UxIx2, Cumulus 300L 3000W.
Sonde temperature sur radiateur triac mise en route ventilateur a 25°
réactivité 30 seuil -50
2 esp32 pour gestion charge batteries
14 panneaux de 410wcc en autoconso micro-onduleur APS DS3
Suivi sur Domoticz
Répondre
#63
La version 14.25 du routeur permet d'afficher en bas de la page d'accueil les autres routeurs déclarés dans la page paramètres.
Mode Expert / Liste des Routeurs en réseau

Cordialement
André
Répondre
#64
(10-06-2025, 07:35 PM)F1ATB a écrit : La version 14.25 du routeur permet d'afficher en bas de la page d'accueil les autres routeurs déclarés dans la page paramètres.
Mode Expert / Liste des Routeurs en réseau

Cordialement
André

super ....
l idee etait donc bonne
ESP32Wroom, Triac 40A "BTA40", Source UxIx2, Cumulus 300L 3000W.
Sonde temperature sur radiateur triac mise en route ventilateur a 25°
réactivité 30 seuil -50
2 esp32 pour gestion charge batteries
14 panneaux de 410wcc en autoconso micro-onduleur APS DS3
Suivi sur Domoticz
Répondre
#65
Oui , demandé depuis longtemps.
André
Répondre
#66
(09-06-2025, 11:52 AM)lucky a écrit : slt
voilà version html améliorée

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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            background: #1a1a2e;
            overflow: hidden;
        }
       
        /* Mode configuration */
        .config-mode {
            background: #1a1a2e !important;
            overflow: auto !important;
        }
       
        .config-container {
            display: none;
            max-width: 600px;
            margin: 0 auto;
            padding: 20px;
            min-height: 100vh;
        }
       
        .config-mode .config-container {
            display: block;
        }
       
        .config-panel {
            background: rgba(255,255,255,0.1);
            padding: 30px;
            border-radius: 10px;
            backdrop-filter: blur(10px);
        }
       
        h1 {
            color: #4CAF50;
            text-align: center;
            margin-bottom: 30px;
        }
       
        .section {
            background: rgba(0,0,0,0.3);
            padding: 20px;
            margin: 20px 0;
            border-radius: 5px;
        }
       
        h2 {
            color: #2196F3;
            margin-top: 0;
            font-size: 18px;
        }
       
        .interface-group {
            display: grid;
            grid-template-columns: 2fr 1fr;
            gap: 10px;
            margin: 15px 0;
            align-items: center;
        }
       
        input[type="text"], input[type="password"] {
            width: 100%;
            padding: 12px;
            border: 1px solid #555;
            border-radius: 5px;
            background: #333;
            color: white;
            font-size: 16px;
            box-sizing: border-box;
            transition: border-color 0.3s;
        }
       
        input[type="text"]:focus, input[type="password"]:focus {
            outline: none;
            border-color: #4CAF50;
            background: #444;
        }
       
        .remove-btn {
            background: #f44336;
            color: white;
            border: none;
            padding: 10px 15px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            transition: background 0.3s;
            min-height: 44px;
        }
       
        .remove-btn:hover {
            background: #d32f2f;
        }
       
        .add-btn {
            background: #2196F3;
            color: white;
            border: none;
            padding: 12px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            margin: 20px 0;
            width: 100%;
            transition: background 0.3s;
        }
       
        .add-btn:hover {
            background: #1976D2;
        }
       
        .save-btn {
            background: #4CAF50;
            color: white;
            border: none;
            padding: 14px 30px;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s;
            width: 100%;
            margin: 10px 0;
        }
       
        .save-btn:hover {
            background: #45a049;
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(76,175,80,0.3);
        }
       
        .export-btn {
            background: #FF5722;
            color: white;
            border: none;
            padding: 14px 30px;
            border-radius: 5px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s;
            width: 100%;
            margin: 10px 0;
        }
       
        .export-btn:hover {
            background: #E64A19;
            transform: translateY(-2px);
            box-shadow: 0 5px 15px rgba(255,87,34,0.3);
        }
       
        .help-text {
            font-size: 13px;
            color: #aaa;
            margin: 15px 0;
            padding: 15px;
            background: rgba(255,255,255,0.05);
            border-radius: 5px;
            line-height: 1.6;
        }
       
        .example {
            font-family: 'Courier New', monospace;
            background: rgba(255,255,255,0.1);
            padding: 2px 6px;
            border-radius: 3px;
            font-size: 12px;
        }
       
        .layout-preview {
            margin: 20px 0;
            padding: 20px;
            background: #2196F3;
            color: white;
            border-radius: 5px;
            text-align: center;
            font-weight: bold;
        }
       
        .message {
            margin: 20px 0;
            padding: 15px;
            border-radius: 5px;
            text-align: center;
            display: none;
            animation: slideIn 0.3s ease;
        }
       
        @keyframes slideIn {
            from {
                opacity: 0;
                transform: translateY(-10px);
            }
            to {
                opacity: 1;
                transform: translateY(0);
            }
        }
       
        .success {
            background: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
       
        .error {
            background: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
       
        /* Mode Dashboard */
        .dashboard-container {
            display: none;
            height: 100vh;
            gap: 2px;
            background: #222;
        }
       
        .dashboard-mode .dashboard-container {
            display: grid;
        }
       
        /* Layouts responsifs */
        .layout-1 {
            grid-template-columns: 1fr;
            grid-template-rows: 1fr;
        }
       
        .layout-2 {
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr;
        }
       
        .layout-3 {
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
        }
       
        .layout-3 .frame-container:first-child {
            grid-row: span 2;
        }
       
        .layout-4 {
            grid-template-columns: 1fr 1fr;
            grid-template-rows: 1fr 1fr;
        }
       
        /* Mobile responsive layouts */
        @media (max-width: 768px) and (orientation: portrait) {
            .layout-2, .layout-3, .layout-4 {
                display: flex !important;
                overflow-x: auto !important;
                scroll-snap-type: x mandatory !important;
                gap: 0 !important;
                -webkit-overflow-scrolling: touch;
            }
           
            .frame-container {
                flex: 0 0 100vw;
                scroll-snap-align: start;
                min-height: 100vh;
            }
           
            .mobile-indicator {
                position: fixed;
                bottom: 70px;
                left: 50%;
                transform: translateX(-50%);
                display: flex;
                gap: 8px;
                z-index: 100;
                background: rgba(0,0,0,0.7);
                padding: 8px 12px;
                border-radius: 20px;
            }
           
            .dot {
                width: 8px;
                height: 8px;
                border-radius: 50%;
                background: rgba(255,255,255,0.5);
                transition: all 0.3s;
            }
           
            .dot.active {
                background: white;
                transform: scale(1.3);
            }
        }
       
        @media (max-width: 768px) and (orientation: landscape) {
            .layout-4 {
                grid-template-columns: 1fr 1fr !important;
            }
           
            .layout-3 .frame-container:first-child {
                grid-row: 1;
            }
        }
       
        .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;
        }
       
        /* Header avec bouton fullscreen uniquement */
        .frame-header {
            position: absolute;
            top: 5px;
            left: 5px;
            z-index: 10;
        }
       
        /* Bouton fullscreen */
        .fullscreen-btn {
            background: rgba(0,0,0,0.8);
            color: white;
            border: none;
            padding: 8px 10px;
            font-size: 16px;
            border-radius: 3px;
            cursor: pointer;
            transition: background 0.2s;
            min-width: 36px;
            min-height: 36px;
        }
       
        .fullscreen-btn:hover {
            background: rgba(0,0,0,0.95);
        }
       
        .fullscreen-btn.exit {
            background: rgba(220,53,69,0.9);
        }
       
        .fullscreen-btn.exit:hover {
            background: rgba(220,53,69,1);
        }
       
        /* Plein écran */
        .frame-container.fullscreen-active {
            position: fixed !important;
            top: 0 !important;
            left: 0 !important;
            width: 100vw !important;
            height: 100vh !important;
            z-index: 9999 !important;
            display: block !important;
        }
       
        /* Contrôles de zoom draggables */
        .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;
            cursor: move;
            user-select: none;
            transition: box-shadow 0.2s;
        }
       
        .zoom-controls:hover {
            background: rgba(0,0,0,0.95);
            box-shadow: 0 2px 5px rgba(0,0,0,0.5);
        }
       
        .zoom-controls.dragging {
            opacity: 0.8;
            box-shadow: 0 5px 15px rgba(0,0,0,0.5);
            z-index: 1000;
        }
       
        .zoom-btn {
            width: 30px;
            height: 30px;
            border: none;
            background: #444;
            color: white;
            cursor: pointer;
            border-radius: 3px;
            font-size: 18px;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s;
        }
       
        .zoom-btn:hover {
            background: #666;
        }
       
        .zoom-value {
            color: white;
            font-size: 12px;
            min-width: 45px;
            text-align: center;
            display: flex;
            align-items: center;
            justify-content: center;
        }
       
        /* Boutons globaux */
        .global-controls {
            display: none;
            position: fixed;
            bottom: 10px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.9);
            padding: 10px 20px;
            border-radius: 25px;
            z-index: 100;
            backdrop-filter: blur(10px);
        }
       
        .dashboard-mode .global-controls {
            display: block;
        }
       
        .global-btn {
            background: #4CAF50;
            color: white;
            border: none;
            padding: 8px 15px;
            margin: 0 5px;
            border-radius: 20px;
            cursor: pointer;
            font-size: 14px;
            transition: all 0.3s;
        }
       
        .global-btn:hover {
            background: #45a049;
            transform: translateY(-2px);
        }
       
        .config-btn {
            background: #FF9800;
        }
       
        .config-btn:hover {
            background: #F57C00;
        }
       
        /* Mobile optimizations */
        @media (max-width: 768px) {
            .zoom-controls {
                display: none !important;
            }
           
            .frame-header {
                font-size: 12px;
                padding: 6px 10px;
            }
           
            .fullscreen-btn {
                min-width: 44px;
                min-height: 44px;
            }
           
            .global-controls {
                bottom: 20px;
                padding: 12px 16px;
            }
           
            .global-btn {
                padding: 10px 18px;
                font-size: 16px;
            }
           
            /* Auto-zoom mobile */
            .frame-wrapper {
                transform: scale(0.5) !important;
                width: 200% !important;
                height: 200% !important;
            }
           
            .frame-container.fullscreen-active .frame-wrapper {
                transform: scale(1) !important;
                width: 100% !important;
                height: 100% !important;
            }
        }
       
        /* Message d'erreur iframe */
        .iframe-error {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(248,215,218,0.95);
            color: #721c24;
            padding: 20px;
            border-radius: 8px;
            text-align: center;
            max-width: 80%;
            z-index: 20;
            display: none;
            backdrop-filter: blur(5px);
        }
       
        .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: 4px 8px;
            border-radius: 3px;
            font-size: 12px;
            display: inline-block;
            margin: 5px 0;
        }
       
        .iframe-error a {
            color: #0056b3;
            text-decoration: none;
            font-weight: bold;
        }
       
        .iframe-error a:hover {
            text-decoration: underline;
        }
       
        /* Authentification */
        .auth-section {
            background: rgba(76,175,80,0.1);
            border: 1px solid rgba(76,175,80,0.3);
        }
       
        .auth-help {
            font-size: 12px;
            color: #aaa;
            margin-top: 10px;
        }
       
        /* Loading spinner */
        .loading {
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid rgba(255,255,255,0.3);
            border-radius: 50%;
            border-top-color: #4CAF50;
            animation: spin 1s ease-in-out infinite;
        }
       
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
       
        /* Welcome message */
        .welcome-message {
            text-align: center;
            padding: 40px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            border-radius: 10px;
            margin-bottom: 30px;
            color: white;
        }
       
        .welcome-message h2 {
            color: white;
            margin-bottom: 10px;
            font-size: 28px;
        }
       
        .welcome-message p {
            font-size: 16px;
            opacity: 0.9;
        }

        /* URL validation indicator */
        .url-status {
            position: absolute;
            right: 10px;
            top: 50%;
            transform: translateY(-50%);
            font-size: 20px;
        }
       
        .url-valid { color: #4CAF50; }
        .url-invalid { color: #f44336; }
    </style>
</head>
<body class="config-mode">
    <!-- 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 interfaces pour créer votre dashboard personnalisé</p>
            </div>
           
            <div class="section auth-section">
                <h2>? Sécurité du Dashboard (optionnel)</h2>
                <label>Nom d'utilisateur :</label>
                <input type="text" id="authUser" placeholder="admin">
               
                <label>Mot de passe :</label>
                <input type="password" id="authPass" placeholder="Mot de passe">
               
                <p class="auth-help">Laissez vide pour désactiver l'authentification</p>
            </div>
           
            <div class="section">
                <h2>? Configuration des Interfaces</h2>
                <div class="help-text">
                    <strong>Formats acceptés :</strong><br>
                    • <span class="example">192.168.1.100</span> → IP simple<br>
                    • <span class="example">192.168.1.100:8080</span> → IP avec port<br>
                    • <span class="example">monserveur.local</span> → Domaine local<br>
                    • <span class="example">https://app.example.com</span> → URL complète<br>
                    • <span class="example">http://serveur.com:3000/dashboard</span> → URL avec chemin
                </div>
               
                <div id="interfacesList"></div>
               
                <button class="add-btn" onclick="addInterface()">➕ Ajouter une interface</button>
            </div>
           
            <div id="layoutPreview" class="layout-preview"></div>
           
            <button class="save-btn" onclick="showDashboard()">? Afficher le dashboard</button>
            <button class="export-btn" onclick="exportConfig()">? Exporter la configuration</button>
           
            <div id="message" class="message"></div>
        </div>
    </div>
   
    <!-- Mode Dashboard -->
    <div id="dashboardContainer" class="dashboard-container"></div>
   
    <!-- Contrôles globaux -->
    <div class="global-controls">
        <button class="global-btn" onclick="resetAllZoom()">↺ Réinitialiser</button>
        <button class="global-btn" onclick="zoomAll(-0.1)">➖ Réduire tout</button>
        <button class="global-btn" onclick="zoomAll(0.1)">➕ Agrandir tout</button>
        <button class="global-btn config-btn" onclick="showConfig()">⚙️ Configuration</button>
    </div>
   
    <!-- Indicateur mobile -->
    <div class="mobile-indicator" id="mobileIndicator" style="display: none;"></div>
   
    <script>
        // Configuration par défaut
        let config = {
            interfaces: [
                { url: '', name: 'Interface 1' },
                { url: '', name: 'Interface 2' }
            ],
            authUser: '',
            authPass: ''
        };
       
        let zoomLevels = {};
        let currentFullscreen = null;
        let isAuthenticated = false;
       
        // Charger la configuration sauvegardée
        function loadConfig() {
            const saved = localStorage.getItem('dashboardConfig');
            if (saved) {
                try {
                    config = JSON.parse(saved);
                    // Assurer au moins 2 interfaces
                    while (config.interfaces.length < 2) {
                        config.interfaces.push({ url: '', name: `Interface ${config.interfaces.length + 1}` });
                    }
                } catch (e) {
                    console.error('Erreur chargement config:', e);
                }
            }
        }
       
        // Sauvegarder la configuration
        function saveConfig() {
            localStorage.setItem('dashboardConfig', JSON.stringify(config));
        }
       
        // Valider et normaliser une URL
        function normalizeURL(url) {
            if (!url) return '';
           
            url = url.trim();
           
            // Si pas de protocole, ajouter http://
            if (!url.match(/^https?:\/\//)) {
                url = 'http://' + url;
            }
           
            return url;
        }
       
        // Valider une URL
        function validateURL(url) {
            if (!url) return false;
           
            try {
                new URL(normalizeURL(url));
                return true;
            } catch (e) {
                return false;
            }
        }
       
        // Afficher la liste des interfaces
        function renderInterfaces() {
            const container = document.getElementById('interfacesList');
            container.innerHTML = '';
           
            config.interfaces.forEach((interface, index) => {
                const div = document.createElement('div');
                div.className = 'interface-group';
               
                const urlInput = document.createElement('input');
                urlInput.type = 'text';
                urlInput.value = interface.url;
                urlInput.placeholder = `URL Interface ${index + 1} (ex: 192.168.1.100:8080)`;
                urlInput.oninput = (e) => {
                    config.interfaces[index].url = e.target.value;
                    updateLayoutPreview();
                    updateURLStatus(urlInput);
                };
               
                const nameInput = document.createElement('input');
                nameInput.type = 'text';
                nameInput.value = interface.name;
                nameInput.placeholder = `Nom ${index + 1}`;
                nameInput.oninput = (e) => {
                    config.interfaces[index].name = e.target.value || `Interface ${index + 1}`;
                };
               
                div.appendChild(urlInput);
                div.appendChild(nameInput);
               
                // Bouton supprimer (sauf pour les 2 premières)
                if (index >= 2) {
                    const removeBtn = document.createElement('button');
                    removeBtn.className = 'remove-btn';
                    removeBtn.textContent = '✕';
                    removeBtn.onclick = () => removeInterface(index);
                    div.appendChild(removeBtn);
                }
               
                container.appendChild(div);
               
                // Validation initiale
                updateURLStatus(urlInput);
            });
           
            updateLayoutPreview();
        }
       
        // Indicateur de validation d'URL
        function updateURLStatus(input) {
            // Supprimer l'ancien indicateur
            const oldStatus = input.parentElement.querySelector('.url-status');
            if (oldStatus) oldStatus.remove();
           
            if (input.value) {
                const status = document.createElement('span');
                status.className = 'url-status';
               
                if (validateURL(input.value)) {
                    status.className += ' url-valid';
                    status.textContent = '✓';
                } else {
                    status.className += ' url-invalid';
                    status.textContent = '✗';
                }
               
                input.parentElement.style.position = 'relative';
                input.parentElement.appendChild(status);
            }
        }
       
        // Ajouter une interface
        function addInterface() {
            if (config.interfaces.length < 4) {
                config.interfaces.push({
                    url: '',
                    name: `Interface ${config.interfaces.length + 1}`
                });
                renderInterfaces();
            } else {
                showMessage('Maximum 4 interfaces', 'error');
            }
        }
       
        // Supprimer une interface
        function removeInterface(index) {
            config.interfaces.splice(index, 1);
            renderInterfaces();
        }
       
        // Mettre à jour l'aperçu
        function updateLayoutPreview() {
            const preview = document.getElementById('layoutPreview');
            const validInterfaces = config.interfaces.filter(i => i.url).length;
           
            let layout = '';
            switch(validInterfaces) {
                case 1: layout = '?️ Plein écran'; break;
                case 2: layout = '? 2 colonnes côte à côte'; break;
                case 3: layout = '? 1 grande vue + 2 petites'; break;
                case 4: layout = '⚏ Grille 2x2'; break;
                default: layout = '❓ Configurez au moins une interface';
            }
           
            preview.innerHTML = `<strong>Disposition :</strong> ${validInterfaces} interface${validInterfaces > 1 ? 's' : ''} - ${layout}`;
        }
       
        // Afficher un message
        function showMessage(text, type) {
            const messageEl = document.getElementById('message');
            messageEl.textContent = text;
            messageEl.className = 'message ' + type;
            messageEl.style.display = 'block';
           
            setTimeout(() => {
                messageEl.style.display = 'none';
            }, 3000);
        }
       
        // Vérifier l'authentification
        function checkAuth() {
            if (!config.authUser || !config.authPass) {
                return true; // Pas d'auth configurée
            }
           
            if (isAuthenticated) {
                return true;
            }
           
            const user = prompt('Nom d\'utilisateur :');
            const pass = prompt('Mot de passe :');
           
            if (user === config.authUser && pass === config.authPass) {
                isAuthenticated = true;
                return true;
            }
           
            alert('Identifiants incorrects');
            return false;
        }
       
        // Afficher le dashboard
        function showDashboard() {
            // Sauvegarder l'auth
            config.authUser = document.getElementById('authUser').value;
            config.authPass = document.getElementById('authPass').value;
           
            // Valider les interfaces
            const validInterfaces = config.interfaces.filter(i => i.url && validateURL(i.url));
           
            if (validInterfaces.length === 0) {
                showMessage('Configurez au moins une interface valide', 'error');
                return;
            }
           
            // Sauvegarder
            saveConfig();
           
            // Vérifier l'auth
            if (!checkAuth()) {
                return;
            }
           
            // Construire le dashboard
            buildDashboard(validInterfaces);
           
            // Basculer en mode dashboard
            document.body.className = 'dashboard-mode';
            document.getElementById('welcomeMessage').style.display = 'none';
        }
       
        // Construire le dashboard
        function buildDashboard(interfaces) {
            const container = document.getElementById('dashboardContainer');
            container.innerHTML = '';
            container.className = `dashboard-container layout-${interfaces.length}`;
           
            interfaces.forEach((interface, index) => {
                const frameDiv = document.createElement('div');
                frameDiv.className = 'frame-container';
                frameDiv.id = `frame${index + 1}-container`;
               
                frameDiv.innerHTML = `
                    <div class="frame-header">
                        <button class="fullscreen-btn" onclick="toggleFullscreen(${index + 1})" id="fullscreen-btn-${index + 1}">⛶</button>
                    </div>
                    <div class="zoom-controls" id="zoom-controls-${index + 1}">
                        <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>
                    </div>
                    <div class="frame-wrapper" id="wrapper${index + 1}">
                        <iframe
                            id="frame${index + 1}"
                            src="${normalizeURL(interface.url)}"
                            sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-top-navigation"
                            referrerpolicy="no-referrer"
                        ></iframe>
                    </div>
                    <div class="iframe-error" id="error${index + 1}">
                        <h3>⚠️ Impossible de charger cette interface</h3>
                        <p>Cette interface ne peut pas être affichée dans une iframe.</p>
                        <p><small>Certains sites bloquent l'affichage dans des iframes pour des raisons de sécurité.</small></p>
                        <p style="margin-top: 15px;">
                            <a href="${normalizeURL(interface.url)}" target="_blank">Ouvrir dans un nouvel onglet →</a>
                        </p>
                    </div>
                `;
               
                container.appendChild(frameDiv);
               
                // Initialiser le zoom
                zoomLevels[index + 1] = 1;
               
                // Gérer les erreurs
                const iframe = frameDiv.querySelector('iframe');
                iframe.addEventListener('error', () => {
                    document.getElementById(`error${index + 1}`).style.display = 'block';
                });
               
                // Vérifier le chargement après un délai
                setTimeout(() => {
                    try {
                        const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
                        // Si on peut accéder au document, c'est bon
                    } catch (e) {
                        // Erreur de cross-origin, possiblement bloqué
                        console.warn(`L'iframe ${index + 1} pourrait être bloquée`);
                    }
                }, 3000);
            });
           
            // Rendre les contrôles de zoom draggables
            setTimeout(makeZoomControlsDraggable, 100);
           
            // Setup mobile si nécessaire
            if (isMobile()) {
                setupMobileNavigation(interfaces.length);
            }
        }
       
        // Détection mobile
        function isMobile() {
            return window.innerWidth <= 768;
        }
       
        // Navigation mobile
        function setupMobileNavigation(count) {
            const indicator = document.getElementById('mobileIndicator');
            indicator.style.display = 'flex';
            indicator.innerHTML = '';
           
            for (let i = 0; i < count; i++) {
                const dot = document.createElement('div');
                dot.className = 'dot';
                if (i === 0) dot.classList.add('active');
                indicator.appendChild(dot);
            }
           
            const container = document.getElementById('dashboardContainer');
            container.addEventListener('scroll', () => {
                const scrollLeft = container.scrollLeft;
                const width = window.innerWidth;
                const currentIndex = Math.round(scrollLeft / width);
               
                document.querySelectorAll('.dot').forEach((dot, index) => {
                    dot.classList.toggle('active', index === currentIndex);
                });
            });
        }
       
        // Zoom
        function zoom(frameNum, delta) {
            zoomLevels[frameNum] = Math.max(0.2, Math.min(2, (zoomLevels[frameNum] || 1) + delta));
            updateZoom(frameNum);
        }
       
        function updateZoom(frameNum) {
            const wrapper = document.getElementById(`wrapper${frameNum}`);
            const zoomDisplay = document.getElementById(`zoom${frameNum}`);
            if (!wrapper || !zoomDisplay) return;
           
            const scale = zoomLevels[frameNum];
            wrapper.style.transform = `scale(${scale})`;
            wrapper.style.width = `${100 / scale}%`;
            wrapper.style.height = `${100 / scale}%`;
            zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
        }
       
        function resetAllZoom() {
            Object.keys(zoomLevels).forEach(frameNum => {
                zoomLevels[frameNum] = 1;
                updateZoom(frameNum);
            });
        }
       
        function zoomAll(delta) {
            Object.keys(zoomLevels).forEach(frameNum => {
                zoom(parseInt(frameNum), delta);
            });
        }
       
        // Plein écran
        function toggleFullscreen(frameNum) {
            const container = document.getElementById(`frame${frameNum}-container`);
            const btn = document.getElementById(`fullscreen-btn-${frameNum}`);
           
            if (!container) return;
           
            if (currentFullscreen === frameNum) {
                // Sortir du plein écran
                container.classList.remove('fullscreen-active');
                btn.textContent = '⛶';
                btn.classList.remove('exit');
                currentFullscreen = null;
               
                // Réafficher les autres
                document.querySelectorAll('.frame-container').forEach(fc => {
                    fc.style.display = '';
                });
            } else {
                // Sortir du plein écran actuel si existe
                if (currentFullscreen) {
                    toggleFullscreen(currentFullscreen);
                }
               
                // Entrer en plein écran
                container.classList.add('fullscreen-active');
                btn.textContent = '✕';
                btn.classList.add('exit');
                currentFullscreen = frameNum;
               
                // Masquer les autres
                document.querySelectorAll('.frame-container').forEach(fc => {
                    if (fc.id !== `frame${frameNum}-container`) {
                        fc.style.display = 'none';
                    }
                });
            }
        }
       
        // Rendre les contrôles de zoom draggables
        function makeZoomControlsDraggable() {
            document.querySelectorAll('.zoom-controls').forEach(controls => {
                let isDragging = false;
                let currentX;
                let currentY;
                let initialX;
                let initialY;
               
                controls.addEventListener('mousedown', dragStart);
                controls.addEventListener('touchstart', handleTouch);
               
                function handleTouch(e) {
                    const touch = e.touches[0];
                    dragStart({
                        clientX: touch.clientX,
                        clientY: touch.clientY,
                        target: e.target
                    });
                }
               
                function dragStart(e) {
                    if (e.target.tagName === 'BUTTON') return;
                   
                    isDragging = true;
                    initialX = e.clientX - controls.offsetLeft;
                    initialY = e.clientY - controls.offsetTop;
                    controls.classList.add('dragging');
                   
                    document.addEventListener('mousemove', drag);
                    document.addEventListener('mouseup', dragEnd);
                    document.addEventListener('touchmove', handleTouchMove);
                    document.addEventListener('touchend', dragEnd);
                }
               
                function handleTouchMove(e) {
                    const touch = e.touches[0];
                    drag({
                        clientX: touch.clientX,
                        clientY: touch.clientY
                    });
                }
               
                function drag(e) {
                    if (!isDragging) return;
                   
                    e.preventDefault();
                    currentX = e.clientX - initialX;
                    currentY = e.clientY - initialY;
                   
                    const container = controls.closest('.frame-container');
                    const maxX = container.offsetWidth - controls.offsetWidth - 5;
                    const maxY = container.offsetHeight - controls.offsetHeight - 5;
                   
                    currentX = Math.max(5, Math.min(maxX, currentX));
                    currentY = Math.max(5, Math.min(maxY, currentY));
                   
                    controls.style.left = currentX + 'px';
                    controls.style.top = currentY + 'px';
                    controls.style.right = 'auto';
                }
               
                function dragEnd() {
                    isDragging = false;
                    controls.classList.remove('dragging');
                    document.removeEventListener('mousemove', drag);
                    document.removeEventListener('mouseup', dragEnd);
                    document.removeEventListener('touchmove', handleTouchMove);
                    document.removeEventListener('touchend', dragEnd);
                }
            });
        }
       
        // Retour à la configuration
        function showConfig() {
            document.body.className = 'config-mode';
            renderInterfaces();
           
            // Restaurer les valeurs d'auth
            document.getElementById('authUser').value = config.authUser || '';
            document.getElementById('authPass').value = config.authPass || '';
        }
       
        // Exporter la configuration
        function exportConfig() {
            // Sauvegarder d'abord la config actuelle
            config.authUser = document.getElementById('authUser').value;
            config.authPass = document.getElementById('authPass').value;
            saveConfig();
           
            const dataStr = JSON.stringify(config, null, 2);
            const dataBlob = new Blob([dataStr], { type: 'application/json' });
           
            const link = document.createElement('a');
            link.href = URL.createObjectURL(dataBlob);
            link.download = 'dashboard-config.json';
            link.click();
           
            showMessage('Configuration exportée !', 'success');
        }
       
        // Gestion Escape pour sortir du plein écran
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && currentFullscreen) {
                toggleFullscreen(currentFullscreen);
            }
        });
       
        // Gestion du redimensionnement
        window.addEventListener('resize', () => {
            if (isMobile() && document.body.className === 'dashboard-mode') {
                const validInterfaces = config.interfaces.filter(i => i.url);
                setupMobileNavigation(validInterfaces.length);
            }
        });
       
        // Initialisation
        window.onload = () => {
            loadConfig();
            renderInterfaces();
           
            // Si config déjà présente, masquer le message de bienvenue
            if (config.interfaces.some(i => i.url)) {
                document.getElementById('welcomeMessage').style.display = 'none';
            }
        };
    </script>
</body>
</html>

Merci lucky,

Cette nouvelle version scroll droite <>gauche pour tel fonctionne parfaitement.
Très bonnes idées que ces dashboards que j'utilise autant à la maison qu'à l'extérieur.

Très bonne soirée,
Merci


Pièces jointes Miniature(s)
   
Routeur UxIx2 (Maison et CE) - Dimmer Robotdyn avec triac BTA40 - Sonde T° sur CE - 4 PV 400Wc sur 2 PowerStream
Répondre


Atteindre :


Utilisateur(s) parcourant ce sujet : 2 visiteur(s)