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()">×</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> 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()">×</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> 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()">×</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> 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()">×</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> 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
|