Messages : 42
Sujets : 1
Inscription : Sep 2024
Réputation :
0
(08-06-2025, 07:50 PM) lucky a écrit : Bonsoir,
alors version améliorée pour tel en position verticale scrolle droite gauche pour passer d'une interface à l'autre et horizontal retour mode normal
zoom amélioré avec 2 doigts etc etc
version esp seul
https://mega.nz/file/sJwDVJYZ#L2g0jNebOn...axhBJbsZRg
version esp rms 14.23 avec timezone
https://mega.nz/file/9NAUwRDJ#Z2-yc7zIFV...wrQ5P_NyIo
faite moi un retour merci
Encore une fois merci lucky,
Je viens de tester la version esp seul pour tel.
Cependant, je préfère une version dasboard.html, pas de esp dédié et plus réactive.
Mais une version dasboard.html ne donne pas un aperçu exploitable sur tel, pas étonnant, pas prévu pour :
-fenêtre coupée en deux à la verticale
-zoom difficilement exploitable
Alors j'utilise ce petit bout de code que j'avais trouvé, puis oublié et retrouvé grâce à ton intervention :
- problème les fenêtres empilées toutes petites ne sont pas top
--------------------------------------------------------------------------------------
RMS.mhtml que j'exécute sur le tel y compris depuis l'extérieur
--------------------------------------------------------------------------------------
Code :
<!DOCTYPE html>
<html lang="fr-FR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Test</title>
</head>
<body>
<iframe src="http://xx.xxx.xx.xxx:49999/" width="1024" height="1000"></iframe>
<iframe src="http://xx.xxx.xx.xxx:50000/" width="1024" height="1000"></iframe>
</body>
</html>
ALORS? Et bien ta nouvelle version avec scroll droite<>gauche est beaucoup plus agréable
Une version dashboard.mhtml allégée pour tel est-elle possible ?
Très bonne journée,
Paul
Pièces jointes
Miniature(s)
Routeur UxIx2 (Maison et CE) - Dimmer Robotdyn avec triac BTA40 - Sonde T° sur CE - 4 PV 400Wc sur 2 PowerStream
Messages : 216
Sujets : 15
Inscription : Jun 2024
Réputation :
8
09-06-2025, 11:52 AM
(Modification du message : 09-06-2025, 11:54 AM par lucky .)
slt
voilà version html améliorée
Code :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Multi-Interfaces</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background: #1a1a2e;
overflow: hidden;
}
/* Mode configuration */
.config-mode {
background: #1a1a2e !important;
overflow: auto !important;
}
.config-container {
display: none;
max-width: 600px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
}
.config-mode .config-container {
display: block;
}
.config-panel {
background: rgba(255,255,255,0.1);
padding: 30px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
h1 {
color: #4CAF50;
text-align: center;
margin-bottom: 30px;
}
.section {
background: rgba(0,0,0,0.3);
padding: 20px;
margin: 20px 0;
border-radius: 5px;
}
h2 {
color: #2196F3;
margin-top: 0;
font-size: 18px;
}
.interface-group {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 10px;
margin: 15px 0;
align-items: center;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #555;
border-radius: 5px;
background: #333;
color: white;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s;
}
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: #4CAF50;
background: #444;
}
.remove-btn {
background: #f44336;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
min-height: 44px;
}
.remove-btn:hover {
background: #d32f2f;
}
.add-btn {
background: #2196F3;
color: white;
border: none;
padding: 12px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 20px 0;
width: 100%;
transition: background 0.3s;
}
.add-btn:hover {
background: #1976D2;
}
.save-btn {
background: #4CAF50;
color: white;
border: none;
padding: 14px 30px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin: 10px 0;
}
.save-btn:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76,175,80,0.3);
}
.export-btn {
background: #FF5722;
color: white;
border: none;
padding: 14px 30px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin: 10px 0;
}
.export-btn:hover {
background: #E64A19;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255,87,34,0.3);
}
.help-text {
font-size: 13px;
color: #aaa;
margin: 15px 0;
padding: 15px;
background: rgba(255,255,255,0.05);
border-radius: 5px;
line-height: 1.6;
}
.example {
font-family: 'Courier New', monospace;
background: rgba(255,255,255,0.1);
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.layout-preview {
margin: 20px 0;
padding: 20px;
background: #2196F3;
color: white;
border-radius: 5px;
text-align: center;
font-weight: bold;
}
.message {
margin: 20px 0;
padding: 15px;
border-radius: 5px;
text-align: center;
display: none;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Mode Dashboard */
.dashboard-container {
display: none;
height: 100vh;
gap: 2px;
background: #222;
}
.dashboard-mode .dashboard-container {
display: grid;
}
/* Layouts responsifs */
.layout-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.layout-2 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
.layout-3 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.layout-3 .frame-container:first-child {
grid-row: span 2;
}
.layout-4 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
/* Mobile responsive layouts */
@media (max-width: 768px) and (orientation: portrait) {
.layout-2, .layout-3, .layout-4 {
display: flex !important;
overflow-x: auto !important;
scroll-snap-type: x mandatory !important;
gap: 0 !important;
-webkit-overflow-scrolling: touch;
}
.frame-container {
flex: 0 0 100vw;
scroll-snap-align: start;
min-height: 100vh;
}
.mobile-indicator {
position: fixed;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 100;
background: rgba(0,0,0,0.7);
padding: 8px 12px;
border-radius: 20px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.5);
transition: all 0.3s;
}
.dot.active {
background: white;
transform: scale(1.3);
}
}
@media (max-width: 768px) and (orientation: landscape) {
.layout-4 {
grid-template-columns: 1fr 1fr !important;
}
.layout-3 .frame-container:first-child {
grid-row: 1;
}
}
.frame-container {
position: relative;
overflow: hidden;
background: white;
}
.frame-wrapper {
width: 100%;
height: 100%;
transform-origin: top left;
transition: transform 0.3s ease;
}
iframe {
width: 100%;
height: 100%;
border: none;
background: white;
}
/* Header avec bouton fullscreen uniquement */
.frame-header {
position: absolute;
top: 5px;
left: 5px;
z-index: 10;
}
/* Bouton fullscreen */
.fullscreen-btn {
background: rgba(0,0,0,0.8);
color: white;
border: none;
padding: 8px 10px;
font-size: 16px;
border-radius: 3px;
cursor: pointer;
transition: background 0.2s;
min-width: 36px;
min-height: 36px;
}
.fullscreen-btn:hover {
background: rgba(0,0,0,0.95);
}
.fullscreen-btn.exit {
background: rgba(220,53,69,0.9);
}
.fullscreen-btn.exit:hover {
background: rgba(220,53,69,1);
}
/* Plein écran */
.frame-container.fullscreen-active {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
display: block !important;
}
/* Contrôles de zoom draggables */
.zoom-controls {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.8);
padding: 5px;
border-radius: 3px;
z-index: 10;
display: flex;
gap: 5px;
cursor: move;
user-select: none;
transition: box-shadow 0.2s;
}
.zoom-controls:hover {
background: rgba(0,0,0,0.95);
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
}
.zoom-controls.dragging {
opacity: 0.8;
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
z-index: 1000;
}
.zoom-btn {
width: 30px;
height: 30px;
border: none;
background: #444;
color: white;
cursor: pointer;
border-radius: 3px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.zoom-btn:hover {
background: #666;
}
.zoom-value {
color: white;
font-size: 12px;
min-width: 45px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
/* Boutons globaux */
.global-controls {
display: none;
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.9);
padding: 10px 20px;
border-radius: 25px;
z-index: 100;
backdrop-filter: blur(10px);
}
.dashboard-mode .global-controls {
display: block;
}
.global-btn {
background: #4CAF50;
color: white;
border: none;
padding: 8px 15px;
margin: 0 5px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.global-btn:hover {
background: #45a049;
transform: translateY(-2px);
}
.config-btn {
background: #FF9800;
}
.config-btn:hover {
background: #F57C00;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.zoom-controls {
display: none !important;
}
.frame-header {
font-size: 12px;
padding: 6px 10px;
}
.fullscreen-btn {
min-width: 44px;
min-height: 44px;
}
.global-controls {
bottom: 20px;
padding: 12px 16px;
}
.global-btn {
padding: 10px 18px;
font-size: 16px;
}
/* Auto-zoom mobile */
.frame-wrapper {
transform: scale(0.5) !important;
width: 200% !important;
height: 200% !important;
}
.frame-container.fullscreen-active .frame-wrapper {
transform: scale(1) !important;
width: 100% !important;
height: 100% !important;
}
}
/* Message d'erreur iframe */
.iframe-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(248,215,218,0.95);
color: #721c24;
padding: 20px;
border-radius: 8px;
text-align: center;
max-width: 80%;
z-index: 20;
display: none;
backdrop-filter: blur(5px);
}
.iframe-error h3 {
margin: 0 0 10px 0;
font-size: 16px;
}
.iframe-error p {
margin: 5px 0;
font-size: 14px;
}
.iframe-error code {
background: rgba(0,0,0,0.1);
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
display: inline-block;
margin: 5px 0;
}
.iframe-error a {
color: #0056b3;
text-decoration: none;
font-weight: bold;
}
.iframe-error a:hover {
text-decoration: underline;
}
/* Authentification */
.auth-section {
background: rgba(76,175,80,0.1);
border: 1px solid rgba(76,175,80,0.3);
}
.auth-help {
font-size: 12px;
color: #aaa;
margin-top: 10px;
}
/* Loading spinner */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: #4CAF50;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Welcome message */
.welcome-message {
text-align: center;
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
margin-bottom: 30px;
color: white;
}
.welcome-message h2 {
color: white;
margin-bottom: 10px;
font-size: 28px;
}
.welcome-message p {
font-size: 16px;
opacity: 0.9;
}
/* URL validation indicator */
.url-status {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
}
.url-valid { color: #4CAF50; }
.url-invalid { color: #f44336; }
</style>
</head>
<body class="config-mode">
<!-- Mode Configuration -->
<div class="config-container">
<div class="config-panel">
<h1>?️ Dashboard Multi-Interfaces</h1>
<div id="welcomeMessage" class="welcome-message">
<h2>Bienvenue !</h2>
<p>Configurez vos interfaces pour créer votre dashboard personnalisé</p>
</div>
<div class="section auth-section">
<h2>? Sécurité du Dashboard (optionnel)</h2>
<label>Nom d'utilisateur :</label>
<input type="text" id="authUser" placeholder="admin">
<label>Mot de passe :</label>
<input type="password" id="authPass" placeholder="Mot de passe">
<p class="auth-help">Laissez vide pour désactiver l'authentification</p>
</div>
<div class="section">
<h2>? Configuration des Interfaces</h2>
<div class="help-text">
<strong>Formats acceptés :</strong><br>
• <span class="example">192.168.1.100</span> → IP simple<br>
• <span class="example">192.168.1.100:8080</span> → IP avec port<br>
• <span class="example">monserveur.local</span> → Domaine local<br>
• <span class="example">https://app.example.com</span> → URL complète<br>
• <span class="example">http://serveur.com:3000/dashboard</span> → URL avec chemin
</div>
<div id="interfacesList"></div>
<button class="add-btn" onclick="addInterface()">➕ Ajouter une interface</button>
</div>
<div id="layoutPreview" class="layout-preview"></div>
<button class="save-btn" onclick="showDashboard()">? Afficher le dashboard</button>
<button class="export-btn" onclick="exportConfig()">? Exporter la configuration</button>
<div id="message" class="message"></div>
</div>
</div>
<!-- Mode Dashboard -->
<div id="dashboardContainer" class="dashboard-container"></div>
<!-- Contrôles globaux -->
<div class="global-controls">
<button class="global-btn" onclick="resetAllZoom()">↺ Réinitialiser</button>
<button class="global-btn" onclick="zoomAll(-0.1)">➖ Réduire tout</button>
<button class="global-btn" onclick="zoomAll(0.1)">➕ Agrandir tout</button>
<button class="global-btn config-btn" onclick="showConfig()">⚙️ Configuration</button>
</div>
<!-- Indicateur mobile -->
<div class="mobile-indicator" id="mobileIndicator" style="display: none;"></div>
<script>
// Configuration par défaut
let config = {
interfaces: [
{ url: '', name: 'Interface 1' },
{ url: '', name: 'Interface 2' }
],
authUser: '',
authPass: ''
};
let zoomLevels = {};
let currentFullscreen = null;
let isAuthenticated = false;
// Charger la configuration sauvegardée
function loadConfig() {
const saved = localStorage.getItem('dashboardConfig');
if (saved) {
try {
config = JSON.parse(saved);
// Assurer au moins 2 interfaces
while (config.interfaces.length < 2) {
config.interfaces.push({ url: '', name: `Interface ${config.interfaces.length + 1}` });
}
} catch (e) {
console.error('Erreur chargement config:', e);
}
}
}
// Sauvegarder la configuration
function saveConfig() {
localStorage.setItem('dashboardConfig', JSON.stringify(config));
}
// Valider et normaliser une URL
function normalizeURL(url) {
if (!url) return '';
url = url.trim();
// Si pas de protocole, ajouter http://
if (!url.match(/^https?:\/\//)) {
url = 'http://' + url;
}
return url;
}
// Valider une URL
function validateURL(url) {
if (!url) return false;
try {
new URL(normalizeURL(url));
return true;
} catch (e) {
return false;
}
}
// Afficher la liste des interfaces
function renderInterfaces() {
const container = document.getElementById('interfacesList');
container.innerHTML = '';
config.interfaces.forEach((interface, index) => {
const div = document.createElement('div');
div.className = 'interface-group';
const urlInput = document.createElement('input');
urlInput.type = 'text';
urlInput.value = interface.url;
urlInput.placeholder = `URL Interface ${index + 1} (ex: 192.168.1.100:8080)`;
urlInput.oninput = (e) => {
config.interfaces[index].url = e.target.value;
updateLayoutPreview();
updateURLStatus(urlInput);
};
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = interface.name;
nameInput.placeholder = `Nom ${index + 1}`;
nameInput.oninput = (e) => {
config.interfaces[index].name = e.target.value || `Interface ${index + 1}`;
};
div.appendChild(urlInput);
div.appendChild(nameInput);
// Bouton supprimer (sauf pour les 2 premières)
if (index >= 2) {
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.textContent = '✕';
removeBtn.onclick = () => removeInterface(index);
div.appendChild(removeBtn);
}
container.appendChild(div);
// Validation initiale
updateURLStatus(urlInput);
});
updateLayoutPreview();
}
// Indicateur de validation d'URL
function updateURLStatus(input) {
// Supprimer l'ancien indicateur
const oldStatus = input.parentElement.querySelector('.url-status');
if (oldStatus) oldStatus.remove();
if (input.value) {
const status = document.createElement('span');
status.className = 'url-status';
if (validateURL(input.value)) {
status.className += ' url-valid';
status.textContent = '✓';
} else {
status.className += ' url-invalid';
status.textContent = '✗';
}
input.parentElement.style.position = 'relative';
input.parentElement.appendChild(status);
}
}
// Ajouter une interface
function addInterface() {
if (config.interfaces.length < 4) {
config.interfaces.push({
url: '',
name: `Interface ${config.interfaces.length + 1}`
});
renderInterfaces();
} else {
showMessage('Maximum 4 interfaces', 'error');
}
}
// Supprimer une interface
function removeInterface(index) {
config.interfaces.splice(index, 1);
renderInterfaces();
}
// Mettre à jour l'aperçu
function updateLayoutPreview() {
const preview = document.getElementById('layoutPreview');
const validInterfaces = config.interfaces.filter(i => i.url).length;
let layout = '';
switch(validInterfaces) {
case 1: layout = '?️ Plein écran'; break;
case 2: layout = '? 2 colonnes côte à côte'; break;
case 3: layout = '? 1 grande vue + 2 petites'; break;
case 4: layout = '⚏ Grille 2x2'; break;
default: layout = '❓ Configurez au moins une interface';
}
preview.innerHTML = `<strong>Disposition :</strong> ${validInterfaces} interface${validInterfaces > 1 ? 's' : ''} - ${layout}`;
}
// Afficher un message
function showMessage(text, type) {
const messageEl = document.getElementById('message');
messageEl.textContent = text;
messageEl.className = 'message ' + type;
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
}, 3000);
}
// Vérifier l'authentification
function checkAuth() {
if (!config.authUser || !config.authPass) {
return true; // Pas d'auth configurée
}
if (isAuthenticated) {
return true;
}
const user = prompt('Nom d\'utilisateur :');
const pass = prompt('Mot de passe :');
if (user === config.authUser && pass === config.authPass) {
isAuthenticated = true;
return true;
}
alert('Identifiants incorrects');
return false;
}
// Afficher le dashboard
function showDashboard() {
// Sauvegarder l'auth
config.authUser = document.getElementById('authUser').value;
config.authPass = document.getElementById('authPass').value;
// Valider les interfaces
const validInterfaces = config.interfaces.filter(i => i.url && validateURL(i.url));
if (validInterfaces.length === 0) {
showMessage('Configurez au moins une interface valide', 'error');
return;
}
// Sauvegarder
saveConfig();
// Vérifier l'auth
if (!checkAuth()) {
return;
}
// Construire le dashboard
buildDashboard(validInterfaces);
// Basculer en mode dashboard
document.body.className = 'dashboard-mode';
document.getElementById('welcomeMessage').style.display = 'none';
}
// Construire le dashboard
function buildDashboard(interfaces) {
const container = document.getElementById('dashboardContainer');
container.innerHTML = '';
container.className = `dashboard-container layout-${interfaces.length}`;
interfaces.forEach((interface, index) => {
const frameDiv = document.createElement('div');
frameDiv.className = 'frame-container';
frameDiv.id = `frame${index + 1}-container`;
frameDiv.innerHTML = `
<div class="frame-header">
<button class="fullscreen-btn" onclick="toggleFullscreen(${index + 1})" id="fullscreen-btn-${index + 1}">⛶</button>
</div>
<div class="zoom-controls" id="zoom-controls-${index + 1}">
<button class="zoom-btn" onclick="zoom(${index + 1}, -0.1)">−</button>
<span class="zoom-value" id="zoom${index + 1}">100%</span>
<button class="zoom-btn" onclick="zoom(${index + 1}, 0.1)">+</button>
</div>
<div class="frame-wrapper" id="wrapper${index + 1}">
<iframe
id="frame${index + 1}"
src="${normalizeURL(interface.url)}"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-top-navigation"
referrerpolicy="no-referrer"
></iframe>
</div>
<div class="iframe-error" id="error${index + 1}">
<h3>⚠️ Impossible de charger cette interface</h3>
<p>Cette interface ne peut pas être affichée dans une iframe.</p>
<p><small>Certains sites bloquent l'affichage dans des iframes pour des raisons de sécurité.</small></p>
<p style="margin-top: 15px;">
<a href="${normalizeURL(interface.url)}" target="_blank">Ouvrir dans un nouvel onglet →</a>
</p>
</div>
`;
container.appendChild(frameDiv);
// Initialiser le zoom
zoomLevels[index + 1] = 1;
// Gérer les erreurs
const iframe = frameDiv.querySelector('iframe');
iframe.addEventListener('error', () => {
document.getElementById(`error${index + 1}`).style.display = 'block';
});
// Vérifier le chargement après un délai
setTimeout(() => {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Si on peut accéder au document, c'est bon
} catch (e) {
// Erreur de cross-origin, possiblement bloqué
console.warn(`L'iframe ${index + 1} pourrait être bloquée`);
}
}, 3000);
});
// Rendre les contrôles de zoom draggables
setTimeout(makeZoomControlsDraggable, 100);
// Setup mobile si nécessaire
if (isMobile()) {
setupMobileNavigation(interfaces.length);
}
}
// Détection mobile
function isMobile() {
return window.innerWidth <= 768;
}
// Navigation mobile
function setupMobileNavigation(count) {
const indicator = document.getElementById('mobileIndicator');
indicator.style.display = 'flex';
indicator.innerHTML = '';
for (let i = 0; i < count; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
if (i === 0) dot.classList.add('active');
indicator.appendChild(dot);
}
const container = document.getElementById('dashboardContainer');
container.addEventListener('scroll', () => {
const scrollLeft = container.scrollLeft;
const width = window.innerWidth;
const currentIndex = Math.round(scrollLeft / width);
document.querySelectorAll('.dot').forEach((dot, index) => {
dot.classList.toggle('active', index === currentIndex);
});
});
}
// Zoom
function zoom(frameNum, delta) {
zoomLevels[frameNum] = Math.max(0.2, Math.min(2, (zoomLevels[frameNum] || 1) + delta));
updateZoom(frameNum);
}
function updateZoom(frameNum) {
const wrapper = document.getElementById(`wrapper${frameNum}`);
const zoomDisplay = document.getElementById(`zoom${frameNum}`);
if (!wrapper || !zoomDisplay) return;
const scale = zoomLevels[frameNum];
wrapper.style.transform = `scale(${scale})`;
wrapper.style.width = `${100 / scale}%`;
wrapper.style.height = `${100 / scale}%`;
zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
}
function resetAllZoom() {
Object.keys(zoomLevels).forEach(frameNum => {
zoomLevels[frameNum] = 1;
updateZoom(frameNum);
});
}
function zoomAll(delta) {
Object.keys(zoomLevels).forEach(frameNum => {
zoom(parseInt(frameNum), delta);
});
}
// Plein écran
function toggleFullscreen(frameNum) {
const container = document.getElementById(`frame${frameNum}-container`);
const btn = document.getElementById(`fullscreen-btn-${frameNum}`);
if (!container) return;
if (currentFullscreen === frameNum) {
// Sortir du plein écran
container.classList.remove('fullscreen-active');
btn.textContent = '⛶';
btn.classList.remove('exit');
currentFullscreen = null;
// Réafficher les autres
document.querySelectorAll('.frame-container').forEach(fc => {
fc.style.display = '';
});
} else {
// Sortir du plein écran actuel si existe
if (currentFullscreen) {
toggleFullscreen(currentFullscreen);
}
// Entrer en plein écran
container.classList.add('fullscreen-active');
btn.textContent = '✕';
btn.classList.add('exit');
currentFullscreen = frameNum;
// Masquer les autres
document.querySelectorAll('.frame-container').forEach(fc => {
if (fc.id !== `frame${frameNum}-container`) {
fc.style.display = 'none';
}
});
}
}
// Rendre les contrôles de zoom draggables
function makeZoomControlsDraggable() {
document.querySelectorAll('.zoom-controls').forEach(controls => {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
controls.addEventListener('mousedown', dragStart);
controls.addEventListener('touchstart', handleTouch);
function handleTouch(e) {
const touch = e.touches[0];
dragStart({
clientX: touch.clientX,
clientY: touch.clientY,
target: e.target
});
}
function dragStart(e) {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
initialX = e.clientX - controls.offsetLeft;
initialY = e.clientY - controls.offsetTop;
controls.classList.add('dragging');
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', dragEnd);
}
function handleTouchMove(e) {
const touch = e.touches[0];
drag({
clientX: touch.clientX,
clientY: touch.clientY
});
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
const container = controls.closest('.frame-container');
const maxX = container.offsetWidth - controls.offsetWidth - 5;
const maxY = container.offsetHeight - controls.offsetHeight - 5;
currentX = Math.max(5, Math.min(maxX, currentX));
currentY = Math.max(5, Math.min(maxY, currentY));
controls.style.left = currentX + 'px';
controls.style.top = currentY + 'px';
controls.style.right = 'auto';
}
function dragEnd() {
isDragging = false;
controls.classList.remove('dragging');
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', dragEnd);
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', dragEnd);
}
});
}
// Retour à la configuration
function showConfig() {
document.body.className = 'config-mode';
renderInterfaces();
// Restaurer les valeurs d'auth
document.getElementById('authUser').value = config.authUser || '';
document.getElementById('authPass').value = config.authPass || '';
}
// Exporter la configuration
function exportConfig() {
// Sauvegarder d'abord la config actuelle
config.authUser = document.getElementById('authUser').value;
config.authPass = document.getElementById('authPass').value;
saveConfig();
const dataStr = JSON.stringify(config, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = 'dashboard-config.json';
link.click();
showMessage('Configuration exportée !', 'success');
}
// Gestion Escape pour sortir du plein écran
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && currentFullscreen) {
toggleFullscreen(currentFullscreen);
}
});
// Gestion du redimensionnement
window.addEventListener('resize', () => {
if (isMobile() && document.body.className === 'dashboard-mode') {
const validInterfaces = config.interfaces.filter(i => i.url);
setupMobileNavigation(validInterfaces.length);
}
});
// Initialisation
window.onload = () => {
loadConfig();
renderInterfaces();
// Si config déjà présente, masquer le message de bienvenue
if (config.interfaces.some(i => i.url)) {
document.getElementById('welcomeMessage').style.display = 'none';
}
};
</script>
</body>
</html>
ESP32Wroom, Triac 40A "BTA40", Source UxIx2, Cumulus 300L 3000W.
Sonde temperature sur radiateur triac mise en route ventilateur a 25°
réactivité 30 seuil -50
2 esp32 pour gestion charge batteries
14 panneaux de 410wcc en autoconso micro-onduleur APS DS3
Suivi sur Domoticz
Messages : 1,014
Sujets : 31
Inscription : May 2024
Réputation :
110
La version 14.25 du routeur permet d'afficher en bas de la page d'accueil les autres routeurs déclarés dans la page paramètres.
Mode Expert / Liste des Routeurs en réseau
Cordialement
André
Messages : 216
Sujets : 15
Inscription : Jun 2024
Réputation :
8
10-06-2025, 07:54 PM
(Modification du message : 10-06-2025, 08:14 PM par lucky .)
(10-06-2025, 07:35 PM) F1ATB a écrit : La version 14.25 du routeur permet d'afficher en bas de la page d'accueil les autres routeurs déclarés dans la page paramètres.
Mode Expert / Liste des Routeurs en réseau
Cordialement
André
super ....
l idee etait donc bonne
ESP32Wroom, Triac 40A "BTA40", Source UxIx2, Cumulus 300L 3000W.
Sonde temperature sur radiateur triac mise en route ventilateur a 25°
réactivité 30 seuil -50
2 esp32 pour gestion charge batteries
14 panneaux de 410wcc en autoconso micro-onduleur APS DS3
Suivi sur Domoticz
Messages : 1,014
Sujets : 31
Inscription : May 2024
Réputation :
110
Oui , demandé depuis longtemps.
André
Messages : 42
Sujets : 1
Inscription : Sep 2024
Réputation :
0
(09-06-2025, 11:52 AM) lucky a écrit : slt
voilà version html améliorée
Code :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard Multi-Interfaces</title>
<style>
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
background: #1a1a2e;
overflow: hidden;
}
/* Mode configuration */
.config-mode {
background: #1a1a2e !important;
overflow: auto !important;
}
.config-container {
display: none;
max-width: 600px;
margin: 0 auto;
padding: 20px;
min-height: 100vh;
}
.config-mode .config-container {
display: block;
}
.config-panel {
background: rgba(255,255,255,0.1);
padding: 30px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
h1 {
color: #4CAF50;
text-align: center;
margin-bottom: 30px;
}
.section {
background: rgba(0,0,0,0.3);
padding: 20px;
margin: 20px 0;
border-radius: 5px;
}
h2 {
color: #2196F3;
margin-top: 0;
font-size: 18px;
}
.interface-group {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 10px;
margin: 15px 0;
align-items: center;
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 12px;
border: 1px solid #555;
border-radius: 5px;
background: #333;
color: white;
font-size: 16px;
box-sizing: border-box;
transition: border-color 0.3s;
}
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: #4CAF50;
background: #444;
}
.remove-btn {
background: #f44336;
color: white;
border: none;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: background 0.3s;
min-height: 44px;
}
.remove-btn:hover {
background: #d32f2f;
}
.add-btn {
background: #2196F3;
color: white;
border: none;
padding: 12px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
margin: 20px 0;
width: 100%;
transition: background 0.3s;
}
.add-btn:hover {
background: #1976D2;
}
.save-btn {
background: #4CAF50;
color: white;
border: none;
padding: 14px 30px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin: 10px 0;
}
.save-btn:hover {
background: #45a049;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(76,175,80,0.3);
}
.export-btn {
background: #FF5722;
color: white;
border: none;
padding: 14px 30px;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
width: 100%;
margin: 10px 0;
}
.export-btn:hover {
background: #E64A19;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(255,87,34,0.3);
}
.help-text {
font-size: 13px;
color: #aaa;
margin: 15px 0;
padding: 15px;
background: rgba(255,255,255,0.05);
border-radius: 5px;
line-height: 1.6;
}
.example {
font-family: 'Courier New', monospace;
background: rgba(255,255,255,0.1);
padding: 2px 6px;
border-radius: 3px;
font-size: 12px;
}
.layout-preview {
margin: 20px 0;
padding: 20px;
background: #2196F3;
color: white;
border-radius: 5px;
text-align: center;
font-weight: bold;
}
.message {
margin: 20px 0;
padding: 15px;
border-radius: 5px;
text-align: center;
display: none;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
/* Mode Dashboard */
.dashboard-container {
display: none;
height: 100vh;
gap: 2px;
background: #222;
}
.dashboard-mode .dashboard-container {
display: grid;
}
/* Layouts responsifs */
.layout-1 {
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
.layout-2 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
}
.layout-3 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
.layout-3 .frame-container:first-child {
grid-row: span 2;
}
.layout-4 {
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
}
/* Mobile responsive layouts */
@media (max-width: 768px) and (orientation: portrait) {
.layout-2, .layout-3, .layout-4 {
display: flex !important;
overflow-x: auto !important;
scroll-snap-type: x mandatory !important;
gap: 0 !important;
-webkit-overflow-scrolling: touch;
}
.frame-container {
flex: 0 0 100vw;
scroll-snap-align: start;
min-height: 100vh;
}
.mobile-indicator {
position: fixed;
bottom: 70px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 100;
background: rgba(0,0,0,0.7);
padding: 8px 12px;
border-radius: 20px;
}
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.5);
transition: all 0.3s;
}
.dot.active {
background: white;
transform: scale(1.3);
}
}
@media (max-width: 768px) and (orientation: landscape) {
.layout-4 {
grid-template-columns: 1fr 1fr !important;
}
.layout-3 .frame-container:first-child {
grid-row: 1;
}
}
.frame-container {
position: relative;
overflow: hidden;
background: white;
}
.frame-wrapper {
width: 100%;
height: 100%;
transform-origin: top left;
transition: transform 0.3s ease;
}
iframe {
width: 100%;
height: 100%;
border: none;
background: white;
}
/* Header avec bouton fullscreen uniquement */
.frame-header {
position: absolute;
top: 5px;
left: 5px;
z-index: 10;
}
/* Bouton fullscreen */
.fullscreen-btn {
background: rgba(0,0,0,0.8);
color: white;
border: none;
padding: 8px 10px;
font-size: 16px;
border-radius: 3px;
cursor: pointer;
transition: background 0.2s;
min-width: 36px;
min-height: 36px;
}
.fullscreen-btn:hover {
background: rgba(0,0,0,0.95);
}
.fullscreen-btn.exit {
background: rgba(220,53,69,0.9);
}
.fullscreen-btn.exit:hover {
background: rgba(220,53,69,1);
}
/* Plein écran */
.frame-container.fullscreen-active {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
display: block !important;
}
/* Contrôles de zoom draggables */
.zoom-controls {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0,0,0,0.8);
padding: 5px;
border-radius: 3px;
z-index: 10;
display: flex;
gap: 5px;
cursor: move;
user-select: none;
transition: box-shadow 0.2s;
}
.zoom-controls:hover {
background: rgba(0,0,0,0.95);
box-shadow: 0 2px 5px rgba(0,0,0,0.5);
}
.zoom-controls.dragging {
opacity: 0.8;
box-shadow: 0 5px 15px rgba(0,0,0,0.5);
z-index: 1000;
}
.zoom-btn {
width: 30px;
height: 30px;
border: none;
background: #444;
color: white;
cursor: pointer;
border-radius: 3px;
font-size: 18px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.2s;
}
.zoom-btn:hover {
background: #666;
}
.zoom-value {
color: white;
font-size: 12px;
min-width: 45px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
/* Boutons globaux */
.global-controls {
display: none;
position: fixed;
bottom: 10px;
left: 50%;
transform: translateX(-50%);
background: rgba(0,0,0,0.9);
padding: 10px 20px;
border-radius: 25px;
z-index: 100;
backdrop-filter: blur(10px);
}
.dashboard-mode .global-controls {
display: block;
}
.global-btn {
background: #4CAF50;
color: white;
border: none;
padding: 8px 15px;
margin: 0 5px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s;
}
.global-btn:hover {
background: #45a049;
transform: translateY(-2px);
}
.config-btn {
background: #FF9800;
}
.config-btn:hover {
background: #F57C00;
}
/* Mobile optimizations */
@media (max-width: 768px) {
.zoom-controls {
display: none !important;
}
.frame-header {
font-size: 12px;
padding: 6px 10px;
}
.fullscreen-btn {
min-width: 44px;
min-height: 44px;
}
.global-controls {
bottom: 20px;
padding: 12px 16px;
}
.global-btn {
padding: 10px 18px;
font-size: 16px;
}
/* Auto-zoom mobile */
.frame-wrapper {
transform: scale(0.5) !important;
width: 200% !important;
height: 200% !important;
}
.frame-container.fullscreen-active .frame-wrapper {
transform: scale(1) !important;
width: 100% !important;
height: 100% !important;
}
}
/* Message d'erreur iframe */
.iframe-error {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(248,215,218,0.95);
color: #721c24;
padding: 20px;
border-radius: 8px;
text-align: center;
max-width: 80%;
z-index: 20;
display: none;
backdrop-filter: blur(5px);
}
.iframe-error h3 {
margin: 0 0 10px 0;
font-size: 16px;
}
.iframe-error p {
margin: 5px 0;
font-size: 14px;
}
.iframe-error code {
background: rgba(0,0,0,0.1);
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
display: inline-block;
margin: 5px 0;
}
.iframe-error a {
color: #0056b3;
text-decoration: none;
font-weight: bold;
}
.iframe-error a:hover {
text-decoration: underline;
}
/* Authentification */
.auth-section {
background: rgba(76,175,80,0.1);
border: 1px solid rgba(76,175,80,0.3);
}
.auth-help {
font-size: 12px;
color: #aaa;
margin-top: 10px;
}
/* Loading spinner */
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,0.3);
border-radius: 50%;
border-top-color: #4CAF50;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Welcome message */
.welcome-message {
text-align: center;
padding: 40px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 10px;
margin-bottom: 30px;
color: white;
}
.welcome-message h2 {
color: white;
margin-bottom: 10px;
font-size: 28px;
}
.welcome-message p {
font-size: 16px;
opacity: 0.9;
}
/* URL validation indicator */
.url-status {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 20px;
}
.url-valid { color: #4CAF50; }
.url-invalid { color: #f44336; }
</style>
</head>
<body class="config-mode">
<!-- Mode Configuration -->
<div class="config-container">
<div class="config-panel">
<h1>?️ Dashboard Multi-Interfaces</h1>
<div id="welcomeMessage" class="welcome-message">
<h2>Bienvenue !</h2>
<p>Configurez vos interfaces pour créer votre dashboard personnalisé</p>
</div>
<div class="section auth-section">
<h2>? Sécurité du Dashboard (optionnel)</h2>
<label>Nom d'utilisateur :</label>
<input type="text" id="authUser" placeholder="admin">
<label>Mot de passe :</label>
<input type="password" id="authPass" placeholder="Mot de passe">
<p class="auth-help">Laissez vide pour désactiver l'authentification</p>
</div>
<div class="section">
<h2>? Configuration des Interfaces</h2>
<div class="help-text">
<strong>Formats acceptés :</strong><br>
• <span class="example">192.168.1.100</span> → IP simple<br>
• <span class="example">192.168.1.100:8080</span> → IP avec port<br>
• <span class="example">monserveur.local</span> → Domaine local<br>
• <span class="example">https://app.example.com</span> → URL complète<br>
• <span class="example">http://serveur.com:3000/dashboard</span> → URL avec chemin
</div>
<div id="interfacesList"></div>
<button class="add-btn" onclick="addInterface()">➕ Ajouter une interface</button>
</div>
<div id="layoutPreview" class="layout-preview"></div>
<button class="save-btn" onclick="showDashboard()">? Afficher le dashboard</button>
<button class="export-btn" onclick="exportConfig()">? Exporter la configuration</button>
<div id="message" class="message"></div>
</div>
</div>
<!-- Mode Dashboard -->
<div id="dashboardContainer" class="dashboard-container"></div>
<!-- Contrôles globaux -->
<div class="global-controls">
<button class="global-btn" onclick="resetAllZoom()">↺ Réinitialiser</button>
<button class="global-btn" onclick="zoomAll(-0.1)">➖ Réduire tout</button>
<button class="global-btn" onclick="zoomAll(0.1)">➕ Agrandir tout</button>
<button class="global-btn config-btn" onclick="showConfig()">⚙️ Configuration</button>
</div>
<!-- Indicateur mobile -->
<div class="mobile-indicator" id="mobileIndicator" style="display: none;"></div>
<script>
// Configuration par défaut
let config = {
interfaces: [
{ url: '', name: 'Interface 1' },
{ url: '', name: 'Interface 2' }
],
authUser: '',
authPass: ''
};
let zoomLevels = {};
let currentFullscreen = null;
let isAuthenticated = false;
// Charger la configuration sauvegardée
function loadConfig() {
const saved = localStorage.getItem('dashboardConfig');
if (saved) {
try {
config = JSON.parse(saved);
// Assurer au moins 2 interfaces
while (config.interfaces.length < 2) {
config.interfaces.push({ url: '', name: `Interface ${config.interfaces.length + 1}` });
}
} catch (e) {
console.error('Erreur chargement config:', e);
}
}
}
// Sauvegarder la configuration
function saveConfig() {
localStorage.setItem('dashboardConfig', JSON.stringify(config));
}
// Valider et normaliser une URL
function normalizeURL(url) {
if (!url) return '';
url = url.trim();
// Si pas de protocole, ajouter http://
if (!url.match(/^https?:\/\//)) {
url = 'http://' + url;
}
return url;
}
// Valider une URL
function validateURL(url) {
if (!url) return false;
try {
new URL(normalizeURL(url));
return true;
} catch (e) {
return false;
}
}
// Afficher la liste des interfaces
function renderInterfaces() {
const container = document.getElementById('interfacesList');
container.innerHTML = '';
config.interfaces.forEach((interface, index) => {
const div = document.createElement('div');
div.className = 'interface-group';
const urlInput = document.createElement('input');
urlInput.type = 'text';
urlInput.value = interface.url;
urlInput.placeholder = `URL Interface ${index + 1} (ex: 192.168.1.100:8080)`;
urlInput.oninput = (e) => {
config.interfaces[index].url = e.target.value;
updateLayoutPreview();
updateURLStatus(urlInput);
};
const nameInput = document.createElement('input');
nameInput.type = 'text';
nameInput.value = interface.name;
nameInput.placeholder = `Nom ${index + 1}`;
nameInput.oninput = (e) => {
config.interfaces[index].name = e.target.value || `Interface ${index + 1}`;
};
div.appendChild(urlInput);
div.appendChild(nameInput);
// Bouton supprimer (sauf pour les 2 premières)
if (index >= 2) {
const removeBtn = document.createElement('button');
removeBtn.className = 'remove-btn';
removeBtn.textContent = '✕';
removeBtn.onclick = () => removeInterface(index);
div.appendChild(removeBtn);
}
container.appendChild(div);
// Validation initiale
updateURLStatus(urlInput);
});
updateLayoutPreview();
}
// Indicateur de validation d'URL
function updateURLStatus(input) {
// Supprimer l'ancien indicateur
const oldStatus = input.parentElement.querySelector('.url-status');
if (oldStatus) oldStatus.remove();
if (input.value) {
const status = document.createElement('span');
status.className = 'url-status';
if (validateURL(input.value)) {
status.className += ' url-valid';
status.textContent = '✓';
} else {
status.className += ' url-invalid';
status.textContent = '✗';
}
input.parentElement.style.position = 'relative';
input.parentElement.appendChild(status);
}
}
// Ajouter une interface
function addInterface() {
if (config.interfaces.length < 4) {
config.interfaces.push({
url: '',
name: `Interface ${config.interfaces.length + 1}`
});
renderInterfaces();
} else {
showMessage('Maximum 4 interfaces', 'error');
}
}
// Supprimer une interface
function removeInterface(index) {
config.interfaces.splice(index, 1);
renderInterfaces();
}
// Mettre à jour l'aperçu
function updateLayoutPreview() {
const preview = document.getElementById('layoutPreview');
const validInterfaces = config.interfaces.filter(i => i.url).length;
let layout = '';
switch(validInterfaces) {
case 1: layout = '?️ Plein écran'; break;
case 2: layout = '? 2 colonnes côte à côte'; break;
case 3: layout = '? 1 grande vue + 2 petites'; break;
case 4: layout = '⚏ Grille 2x2'; break;
default: layout = '❓ Configurez au moins une interface';
}
preview.innerHTML = `<strong>Disposition :</strong> ${validInterfaces} interface${validInterfaces > 1 ? 's' : ''} - ${layout}`;
}
// Afficher un message
function showMessage(text, type) {
const messageEl = document.getElementById('message');
messageEl.textContent = text;
messageEl.className = 'message ' + type;
messageEl.style.display = 'block';
setTimeout(() => {
messageEl.style.display = 'none';
}, 3000);
}
// Vérifier l'authentification
function checkAuth() {
if (!config.authUser || !config.authPass) {
return true; // Pas d'auth configurée
}
if (isAuthenticated) {
return true;
}
const user = prompt('Nom d\'utilisateur :');
const pass = prompt('Mot de passe :');
if (user === config.authUser && pass === config.authPass) {
isAuthenticated = true;
return true;
}
alert('Identifiants incorrects');
return false;
}
// Afficher le dashboard
function showDashboard() {
// Sauvegarder l'auth
config.authUser = document.getElementById('authUser').value;
config.authPass = document.getElementById('authPass').value;
// Valider les interfaces
const validInterfaces = config.interfaces.filter(i => i.url && validateURL(i.url));
if (validInterfaces.length === 0) {
showMessage('Configurez au moins une interface valide', 'error');
return;
}
// Sauvegarder
saveConfig();
// Vérifier l'auth
if (!checkAuth()) {
return;
}
// Construire le dashboard
buildDashboard(validInterfaces);
// Basculer en mode dashboard
document.body.className = 'dashboard-mode';
document.getElementById('welcomeMessage').style.display = 'none';
}
// Construire le dashboard
function buildDashboard(interfaces) {
const container = document.getElementById('dashboardContainer');
container.innerHTML = '';
container.className = `dashboard-container layout-${interfaces.length}`;
interfaces.forEach((interface, index) => {
const frameDiv = document.createElement('div');
frameDiv.className = 'frame-container';
frameDiv.id = `frame${index + 1}-container`;
frameDiv.innerHTML = `
<div class="frame-header">
<button class="fullscreen-btn" onclick="toggleFullscreen(${index + 1})" id="fullscreen-btn-${index + 1}">⛶</button>
</div>
<div class="zoom-controls" id="zoom-controls-${index + 1}">
<button class="zoom-btn" onclick="zoom(${index + 1}, -0.1)">−</button>
<span class="zoom-value" id="zoom${index + 1}">100%</span>
<button class="zoom-btn" onclick="zoom(${index + 1}, 0.1)">+</button>
</div>
<div class="frame-wrapper" id="wrapper${index + 1}">
<iframe
id="frame${index + 1}"
src="${normalizeURL(interface.url)}"
sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-modals allow-downloads allow-presentation allow-top-navigation"
referrerpolicy="no-referrer"
></iframe>
</div>
<div class="iframe-error" id="error${index + 1}">
<h3>⚠️ Impossible de charger cette interface</h3>
<p>Cette interface ne peut pas être affichée dans une iframe.</p>
<p><small>Certains sites bloquent l'affichage dans des iframes pour des raisons de sécurité.</small></p>
<p style="margin-top: 15px;">
<a href="${normalizeURL(interface.url)}" target="_blank">Ouvrir dans un nouvel onglet →</a>
</p>
</div>
`;
container.appendChild(frameDiv);
// Initialiser le zoom
zoomLevels[index + 1] = 1;
// Gérer les erreurs
const iframe = frameDiv.querySelector('iframe');
iframe.addEventListener('error', () => {
document.getElementById(`error${index + 1}`).style.display = 'block';
});
// Vérifier le chargement après un délai
setTimeout(() => {
try {
const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
// Si on peut accéder au document, c'est bon
} catch (e) {
// Erreur de cross-origin, possiblement bloqué
console.warn(`L'iframe ${index + 1} pourrait être bloquée`);
}
}, 3000);
});
// Rendre les contrôles de zoom draggables
setTimeout(makeZoomControlsDraggable, 100);
// Setup mobile si nécessaire
if (isMobile()) {
setupMobileNavigation(interfaces.length);
}
}
// Détection mobile
function isMobile() {
return window.innerWidth <= 768;
}
// Navigation mobile
function setupMobileNavigation(count) {
const indicator = document.getElementById('mobileIndicator');
indicator.style.display = 'flex';
indicator.innerHTML = '';
for (let i = 0; i < count; i++) {
const dot = document.createElement('div');
dot.className = 'dot';
if (i === 0) dot.classList.add('active');
indicator.appendChild(dot);
}
const container = document.getElementById('dashboardContainer');
container.addEventListener('scroll', () => {
const scrollLeft = container.scrollLeft;
const width = window.innerWidth;
const currentIndex = Math.round(scrollLeft / width);
document.querySelectorAll('.dot').forEach((dot, index) => {
dot.classList.toggle('active', index === currentIndex);
});
});
}
// Zoom
function zoom(frameNum, delta) {
zoomLevels[frameNum] = Math.max(0.2, Math.min(2, (zoomLevels[frameNum] || 1) + delta));
updateZoom(frameNum);
}
function updateZoom(frameNum) {
const wrapper = document.getElementById(`wrapper${frameNum}`);
const zoomDisplay = document.getElementById(`zoom${frameNum}`);
if (!wrapper || !zoomDisplay) return;
const scale = zoomLevels[frameNum];
wrapper.style.transform = `scale(${scale})`;
wrapper.style.width = `${100 / scale}%`;
wrapper.style.height = `${100 / scale}%`;
zoomDisplay.textContent = `${Math.round(scale * 100)}%`;
}
function resetAllZoom() {
Object.keys(zoomLevels).forEach(frameNum => {
zoomLevels[frameNum] = 1;
updateZoom(frameNum);
});
}
function zoomAll(delta) {
Object.keys(zoomLevels).forEach(frameNum => {
zoom(parseInt(frameNum), delta);
});
}
// Plein écran
function toggleFullscreen(frameNum) {
const container = document.getElementById(`frame${frameNum}-container`);
const btn = document.getElementById(`fullscreen-btn-${frameNum}`);
if (!container) return;
if (currentFullscreen === frameNum) {
// Sortir du plein écran
container.classList.remove('fullscreen-active');
btn.textContent = '⛶';
btn.classList.remove('exit');
currentFullscreen = null;
// Réafficher les autres
document.querySelectorAll('.frame-container').forEach(fc => {
fc.style.display = '';
});
} else {
// Sortir du plein écran actuel si existe
if (currentFullscreen) {
toggleFullscreen(currentFullscreen);
}
// Entrer en plein écran
container.classList.add('fullscreen-active');
btn.textContent = '✕';
btn.classList.add('exit');
currentFullscreen = frameNum;
// Masquer les autres
document.querySelectorAll('.frame-container').forEach(fc => {
if (fc.id !== `frame${frameNum}-container`) {
fc.style.display = 'none';
}
});
}
}
// Rendre les contrôles de zoom draggables
function makeZoomControlsDraggable() {
document.querySelectorAll('.zoom-controls').forEach(controls => {
let isDragging = false;
let currentX;
let currentY;
let initialX;
let initialY;
controls.addEventListener('mousedown', dragStart);
controls.addEventListener('touchstart', handleTouch);
function handleTouch(e) {
const touch = e.touches[0];
dragStart({
clientX: touch.clientX,
clientY: touch.clientY,
target: e.target
});
}
function dragStart(e) {
if (e.target.tagName === 'BUTTON') return;
isDragging = true;
initialX = e.clientX - controls.offsetLeft;
initialY = e.clientY - controls.offsetTop;
controls.classList.add('dragging');
document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', dragEnd);
document.addEventListener('touchmove', handleTouchMove);
document.addEventListener('touchend', dragEnd);
}
function handleTouchMove(e) {
const touch = e.touches[0];
drag({
clientX: touch.clientX,
clientY: touch.clientY
});
}
function drag(e) {
if (!isDragging) return;
e.preventDefault();
currentX = e.clientX - initialX;
currentY = e.clientY - initialY;
const container = controls.closest('.frame-container');
const maxX = container.offsetWidth - controls.offsetWidth - 5;
const maxY = container.offsetHeight - controls.offsetHeight - 5;
currentX = Math.max(5, Math.min(maxX, currentX));
currentY = Math.max(5, Math.min(maxY, currentY));
controls.style.left = currentX + 'px';
controls.style.top = currentY + 'px';
controls.style.right = 'auto';
}
function dragEnd() {
isDragging = false;
controls.classList.remove('dragging');
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', dragEnd);
document.removeEventListener('touchmove', handleTouchMove);
document.removeEventListener('touchend', dragEnd);
}
});
}
// Retour à la configuration
function showConfig() {
document.body.className = 'config-mode';
renderInterfaces();
// Restaurer les valeurs d'auth
document.getElementById('authUser').value = config.authUser || '';
document.getElementById('authPass').value = config.authPass || '';
}
// Exporter la configuration
function exportConfig() {
// Sauvegarder d'abord la config actuelle
config.authUser = document.getElementById('authUser').value;
config.authPass = document.getElementById('authPass').value;
saveConfig();
const dataStr = JSON.stringify(config, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(dataBlob);
link.download = 'dashboard-config.json';
link.click();
showMessage('Configuration exportée !', 'success');
}
// Gestion Escape pour sortir du plein écran
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && currentFullscreen) {
toggleFullscreen(currentFullscreen);
}
});
// Gestion du redimensionnement
window.addEventListener('resize', () => {
if (isMobile() && document.body.className === 'dashboard-mode') {
const validInterfaces = config.interfaces.filter(i => i.url);
setupMobileNavigation(validInterfaces.length);
}
});
// Initialisation
window.onload = () => {
loadConfig();
renderInterfaces();
// Si config déjà présente, masquer le message de bienvenue
if (config.interfaces.some(i => i.url)) {
document.getElementById('welcomeMessage').style.display = 'none';
}
};
</script>
</body>
</html>
Merci lucky,
Cette nouvelle version scroll droite <>gauche pour tel fonctionne parfaitement.
Très bonnes idées que ces dashboards que j'utilise autant à la maison qu'à l'extérieur.
Très bonne soirée,
Merci
Pièces jointes
Miniature(s)
Routeur UxIx2 (Maison et CE) - Dimmer Robotdyn avec triac BTA40 - Sonde T° sur CE - 4 PV 400Wc sur 2 PowerStream