Blog

  • L’enfer du HTML statique (et comment je l’ai remplacé par une galerie pilotée par CSV)

    Le mois dernier, sur un portfolio d’artiste (peintures/illustrations), je suis retombé dans le piège classique du site “100% statique” qui devient vite ingérable : à chaque nouvelle toile, il fallait ouvrir index.html, copier/coller un bloc, vérifier les chemins d’images, l’alt, les tags… puis prier pour ne rien casser.

    Le problème n’était pas la “tech”, c’était le workflow : l’artiste devait pouvoir ajouter une œuvre sans toucher au HTML.

    Résultat : j’ai séparé la donnée (liste des œuvres) du rendu (la page) via un simple images.csv chargé en JavaScript.

    Repo de référence (code complet) :
    https://github.com/vladimir-monari/monartistique/blob/main/


    1) L’idée : des données dans images.csv, un rendu automatique dans le DOM

    Structure CSV (la “source de vérité”)

    J’ai standardisé un fichier images.csv avec des en-têtes stables (importants, car le code les référence tels quels) :

    Nom de l'image,Chemin de l'image,Description de l'image,Tags
    Portrait Bleu,/img/bleu.jpg,Huile sur toile,abstrait-portrait
    Coucher de soleil,/img/soleil.jpg,Aquarelle,paysage-nature
    • Nom de l’image : sert de titre affiché
    • Chemin de l’image : URL relative (ex: /images/... ou /img/...)
    • Description de l’image : sert de texte descriptif (et alt)
    • Tags : une liste séparée par - (ex: abstrait-portrait)

    Ce point (séparateur -) est volontaire : c’est simple à saisir dans Google Sheets, et trivial à parser côté JS.


    2) index.html : une page “coquille” (et c’est exactement ce qu’on veut)

    Ce que j’ai gardé dans index.html, c’est :

    • tout ce qui est structurel (header, section “about”, footer)
    • les conteneurs vides destinés à être remplis (#image-container, #tag-cloud)
    • les dépendances (CSS + PapaParse) et le script applicatif scripts.js
    • la modal d’affichage grand format

    Points importants dans ton index.html :

    a) Les conteneurs clés

    <div id="tag-cloud"></div>
    <div id="image-container"></div>

    Ils définissent les “ports d’entrée” où le JS injecte la galerie.

    b) La modal déjà prête

    <div id="modal" class="modal">
    <span class="close">&times;</span>
    <img class="modal-content" id="modal-image">
    <div id="caption"></div>
    </div>

    C’est un bon pattern : la structure est déclarative dans le HTML, et le JS ne fait que manipuler display, src et le texte.

    c) Dépendances : PapaParse OK, D3 probablement inutile

    Tu charges :

    <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
    <script src="https://d3js.org/d3.v5.min.js"></script>

    PapaParse est effectivement utilisé. En revanche, D3 n’est pas utilisé dans scripts.js tel qu’il est actuellement (le nuage de tags est construit au DOM “vanilla”). Donc tu peux supprimer D3 pour alléger la page, sauf si tu prévois une vraie visualisation.

    d) Google Tag Manager / Analytics

    Tu as GTM + gtag. Ça fonctionne, mais garde en tête :

    • c’est du poids JS supplémentaire
    • idéalement, documente dans un README ce qui est mesuré (évite la “boîte noire” côté maintenance)

    3) scripts.js : le pipeline complet (CSV → objets → DOM → tags → filtres)

    a) Chargement du CSV

    function loadCSV(callback) {
    fetch('images.csv')
    .then(response => response.text())
    .then(data => callback(data))
    .catch(error => console.error('Erreur lors du chargement du CSV:', error));
    }

    Ce que ça implique :

    • images.csv doit être dans le même dossier que index.html (ou il faut ajuster le chemin)
    • fetch() ne marchera pas correctement en ouvrant le fichier en file:// (cf. pièges plus bas)

    b) Parsing + génération des cartes image

    Le cœur est ici :

    Papa.parse(data, {
    header: true,
    complete: function (results) {
    results.data.forEach(function (d) {
    if (!d["Nom de l'image"] || !d["Chemin de l'image"]) return;
    var container = document.createElement(‘div’);
    container.classList.add(‘image-wrapper’);
    container.dataset.tags = d[‘Tags’];

    var img = new Image();
    img.src = d[« Chemin de l’image »];
    img.alt = d[« Description de l’image »];
    img.classList.add(« image-responsive »);

    Bon choix : tu fais un early return si les champs essentiels manquent (ça neutralise déjà pas mal de lignes vides / erreurs CSV).

    Ensuite :

    • dataset.tags sert à filtrer
    • alt est alimenté par la description (bon réflexe accessibilité/SEO)
    • l’ouverture de modal se fait au clic

    c) Nuage de tags + filtrage

    Tu comptes les tags :

    d['Tags'].split('-').forEach(function (tag) {
    tags[tag] = (tags[tag] || 0) + 1;
    });

    et tu construis le nuage avec une taille proportionnelle au nombre d’occurrences.


    4) Le flux d’automatisation (celui qui change vraiment la vie)

    Le flux que j’ai mis en place (et qui tient bien dans le temps) :

    1. L’artiste met à jour une feuille (Google Sheets / Excel)
    2. Export en CSV
    3. Dépôt du CSV + des images dans le repo
    4. git push
    5. Déploiement (GitHub Pages ou autre) → la galerie se met à jour automatiquement

    Le gros gain : plus jamais d’édition manuelle du HTML pour une nouvelle œuvre.


    5) Les pièges rencontrés (et les corrections concrètes)

    1) Lignes vides / objets incomplets (PapaParse)

    Tu l’as déjà traité en partie avec :

    if (!d["Nom de l'image"] || !d["Chemin de l'image"]) return;

    Je garde exactement cette logique, et j’ajoute souvent une défense sur Tags :

    const rawTags = (d['Tags'] || '').trim();
    container.dataset.tags = rawTags;

    Sinon, split('-') sur undefined peut casser.


    2) CORS en local (fetch + file://)

    Classique : ouvrir index.html en double-cliquant ne suffit pas.

    Solution : serveur local (Live Server, python -m http.server, etc.).
    Tu l’as bien noté : Live Server sous VS Code règle ça immédiatement.


    3) XSS / injection via CSV (le vrai point sensible)

    Dans ton code actuel, il y a un endroit franchement risqué :

    document.getElementById("caption").innerHTML = this.alt;

    Même si “c’est juste un CSV”, c’est une entrée de données. Si un jour une valeur contient des balises, tu les interprètes.

    Correction simple et robuste : ne jamais utiliser innerHTML pour du texte :

    document.getElementById("caption").textContent = this.alt;

    Ça te rend la fonction sanitize() beaucoup moins critique (et tu peux la réserver aux cas où tu dois vraiment injecter du HTML).


    4) Bug discret : double conteneur #tag-cloud

    Dans index.html, tu as déjà :

    <div id="tag-cloud"></div>

    Mais dans displayTagCloud, tu recrées un nouveau div avec le même id et tu l’insères après le <h3> :

    var tagCloud = document.createElement('div');
    tagCloud.id = 'tag-cloud';
    document.querySelector('#container h3').insertAdjacentElement('afterend', tagCloud);

    Conséquence : tu peux te retrouver avec deux éléments portant le même id (invalid HTML, comportements bizarres).

    Correction : réutiliser le conteneur existant :

    function displayTagCloud(tags) {
    var tagCloud = document.getElementById('tag-cloud');
    tagCloud.innerHTML = ''; // reset
    ...
    }

    6) styles.css : ce qui marche bien (et ce que j’ajusterais)

    Points positifs :

    • layout simple, lisible, cohérent
    • .image-wrapper en inline-block : pratique pour une galerie “mosaïque”
    • modal plein écran avec fond noir : efficace pour de l’art

    Deux remarques terrain :

    a) max-width: 100vh sur .modal-content

    Tu as :

    .modal-content {
    max-width: 100vh;
    max-height: 90vh;
    }

    max-width en vh (hauteur viewport) est inhabituel : sur écran large, ça peut limiter la largeur de façon contre-intuitive. En général, je préfère :

    .modal-content {
    max-width: 95vw;
    max-height: 90vh;
    }

    b) Performance visuelle

    Si la galerie grandit, pense à :

    • loading="lazy" côté image
    • éventuellement des miniatures dédiées (évite de charger un 4000px pour une vignette)

    7) Améliorations futures (SEO, UX, perf) — plan d’évolution réaliste

    UX / Accessibilité

    • fermer la modal au clic hors image + touche Escape
    • focus management (quand la modal s’ouvre, focus sur le bouton close)
    • ajouter aria-label au bouton close

    Performance

    • img.loading = "lazy";
    • pagination / chargement par lots (12 par 12)
    • miniatures + images HD séparées (ex: thumb_path et full_path dans le CSV)

    SEO

    • alt déjà OK, mais tu peux enrichir :
      • titres d’œuvres en <h4> (ou au moins structure sémantique)
      • JSON-LD (type ImageObject ou CreativeWork) si tu veux pousser le référencement

    Sécurité

    • bannir innerHTML pour tout texte issu du CSV (caption, descriptions, titres)
    • valider les chemins d’images (optionnel, mais utile si le CSV est édité par plusieurs personnes)

    Conclusion

    Le vrai bénéfice de cette approche CSV + rendu JS, c’est qu’elle transforme un site statique fragile en mini “CMS maison”, sans serveur, sans base de données, sans back-office. Pour un portfolio d’artiste, c’est exactement le bon compromis : simple à maintenir, rapide à charger, et évolutif.

    Code complet (structure, index.html, images.csv, scripts.js, styles.css) :
    https://github.com/vladimir-monari/monartistique/blob/main/

  • How to Smart-Control a Garage Door with Shelly Plus 1 and Real State Feedback

    The Danger of Blind Control: A Real-Life Experience

    A few months ago, I almost locked my cat inside the garage. As I was driving away, I pressed my old Somfy RTS remote control without looking back. That was the exact moment I realized I needed to automate my garage door opener, but above all, get a 100% reliable open/closed status in Home Assistant. I couldn’t rely on simple time-based estimates anymore.

    The ultimate solution? The Shelly Plus 1 micro-relay. Thanks to its dry contact interface, it is the safest option to trigger an electronic garage motor board without risking sending 230V into it.

    Why the Shelly Plus 1 is Perfect for this Project

    • Dry Contact: The I (Input) and O (Output) terminals are completely isolated from the module’s power supply (L/N).
    • Configurable SW Input: You can connect a magnetic limit switch sensor to detect the exact physical position of the door.
    • Local Reliability: No cloud needed; it communicates locally via MQTT or the native Shelly Home Assistant integration.

    Step 1: Secure Wiring Diagram

    Before any wiring, turn off your main circuit breaker. For my setup, I powered the Shelly Plus 1 with 230V AC (L and N terminals). If you prefer low voltage, the Shelly can also be powered by 12V or 24-48V DC by adjusting the hardware jumper.

    Connect the I and O terminals of the Shelly in parallel with the « wall button » trigger terminals (usually marked as Start or Cyc) on your garage door motor. For the magnetic sensor, mount the wired part on the door frame and the magnet on the moving door panel (positioned so they meet when the door is fully closed). Connect the sensor’s two wires between the SW terminal and the L terminal (if using 230V AC, or GND if using DC).

    Step 2: Configuring the Shelly Plus 1

    Once powered back on, connect to the Shelly’s temporary AP to open its web interface. Apply the following settings:

    • Auto-Off Timer: Set an automatic off timer of 1 second. This simulates a momentary push button switch.
    • Input Mode: Set the SW input to « Detached » mode. This ensures that the sensor state changes do not trigger the relay mechanically.

    Step 3: Integration with Home Assistant

    Using the native Shelly integration, the module is auto-discovered. To get a clean cover component, I configured a template cover in my configuration.yaml that links the relay trigger to the state of the SW input. Now, I control my garage door safely, with visual assurance of its true state.

  • Securing your Proxmox Home Lab: Advanced Network Segmentation Guide with OPNsense

    The day my test server almost compromised my entire home network

    As an IT governance trainer, I often repeat this fundamental security axiom: « Never trust the default local network ». Yet, I fell into the classic beginner’s trap in my own Home Lab. During a routine audit of my Proxmox VE host, I realized that a simple Remote Code Execution (RCE) vulnerability on an LXD container hosting a public-facing test server would have allowed an attacker to pivot directly onto my backup NAS and personal devices.

    To align my infrastructure with ISO 27001 standards and best practices, I had to completely rethink my network governance. The solution? Strict VLAN segmentation orchestrated by a virtualized OPNsense firewall running on Proxmox, while bypassing the typical performance bottlenecks associated with network virtualization.

    Architecture & Governance: The Blueprint

    Effective governance requires mapping and segmenting traffic based on criticality. We will structure our network into 4 isolated zones:

    • VLAN 10 (Management): Access to Proxmox VE, OPNsense, switches, and PDU interfaces.
    • VLAN 20 (Trusted LAN): Personal computers, trusted mobile devices.
    • VLAN 30 (DMZ / Public): Reverse proxy, exposed Nextcloud or Vaultwarden instances.
    • VLAN 40 (IoT / Untrusted): Smart home devices and untrusted appliances.

    Step 1: Configuring a VLAN-Aware Linux Bridge in Proxmox

    To avoid needing multiple physical network interface cards (NICs), we configure Proxmox’s default Linux bridge (vmbr0) to natively handle VLAN tags. In the Proxmox Web UI:

    1. Navigate to System > Network.
    2. Edit the main bridge interface (typically vmbr0).
    3. Check the VLAN Aware box.
    4. Apply the configuration (or reboot the host).

    Step 2: Deploying OPNsense and Optimizing VirtIO Performance

    When provisioning the OPNsense VM, assign network interfaces using the VirtIO driver to achieve 10 Gbps speeds. However, virtualizing network adapters introduces a critical issue: hardware checksum offloading can corrupt packets routed through virtualized OPNsense instances.

    Crucial Action: Once OPNsense is installed, navigate to Interfaces > Settings and check the box to Disable Hardware Checksum Offload (as well as TSO and LRO). Reboot the firewall for changes to take effect.

    Step 3: Implementing Zero-Trust Firewall Rules

    The golden rule of governance is least privilege (Default Deny). By default, create an alias blocking all private address space (RFC 1918) for your DMZ and IoT zones.

    In the OPNsense firewall rules tab for your DMZ interface (VLAN 30), allow only:

    • DNS queries to the OPNsense gateway IP (UDP port 53).
    • Outbound traffic to the WAN (Internet) for package updates.
    • Explicitly block any inbound traffic targeting VLAN 10 (Admin) and VLAN 20 (Trusted).

    The Result: Enterprise-Grade Security at Home

    Thanks to this architecture, if my public-facing web server in the DMZ gets compromised, the attacker is isolated within a digital sandbox. They cannot scan my local LAN or access the Proxmox hypervisor. This setup proves that with the right open-source tools and a structured methodology, you can apply corporate-level security governance to any Home Lab.

  • Sécuriser son Home Lab Proxmox : Guide de segmentation réseau avancée avec OPNsense

    Le jour où mon serveur de test a failli compromettre tout mon réseau domestique

    En tant que formateur en gouvernance des SI, je répète souvent cette maxime de l’ANSSI : « Ne faites jamais confiance au réseau local par défaut ». Pourtant, j’ai moi-même commis l’erreur classique du débutant dans mon propre Home Lab. Lors d’un audit de routine de mon serveur Proxmox VE, j’ai réalisé qu’une simple vulnérabilité RCE (Remote Code Execution) sur un conteneur LXD hébergeant un serveur de test exposé aurait permis à un attaquant de pivoter directement vers mon NAS de sauvegarde et mes équipements personnels.

    Pour aligner mon infrastructure sur les exigences de la norme ISO 27001 et du guide d’hygiène informatique de l’ANSSI, j’ai dû repenser intégralement ma gouvernance réseau. La solution ? Une segmentation stricte par VLANs orchestrée par un pare-feu virtualisé OPNsense sous Proxmox, en évitant les pièges classiques de performances liés à la virtualisation réseau.

    Architecture technique et gouvernance : Le plan d’attaque

    Une bonne gouvernance impose de cartographier et de cloisonner les flux selon leur criticité. Nous allons structurer notre infrastructure autour de 4 zones étanches :

    • VLAN 10 (Administration) : Accès aux interfaces Proxmox VE, OPNsense, switches et PDU.
    • VLAN 20 (Trusted LAN) : PC personnels, smartphones de confiance.
    • VLAN 30 (DMZ / Public) : Reverse proxy, instances Nextcloud ou Vaultwarden exposées.
    • VLAN 40 (IoT / Untrusted) : Domotique et objets connectés.

    Étape 1 : Configuration du pont réseau (Bridge) VLAN-Aware sur Proxmox

    Pour éviter d’utiliser plusieurs cartes réseau physiques, nous configurons le pont Linux par défaut de Proxmox (vmbr0) pour qu’il gère les tags VLAN. Dans l’interface Web de Proxmox :

    1. Allez dans System > Network.
    2. Éditez le pont principal (souvent vmbr0).
    3. Cochez la case VLAN Aware.
    4. Appliquez les modifications (ou redémarrez l’hôte).

    Étape 2 : Déploiement d’OPNsense et optimisation des performances VirtIO

    Lors de la création de la machine virtuelle OPNsense, attribuez-lui des interfaces réseau basées sur le pilote VirtIO pour des performances maximales à 10 Gbps. Cependant, la virtualisation des cartes réseau introduit un problème majeur : le déchargement matériel des sommes de contrôle (Hardware Checksum Offloading) peut corrompre les paquets réseau passant par OPNsense.

    Action indispensable : Une fois OPNsense installé, rendez-vous dans Interfaces > Settings et cochez obligatoirement l’option Disable Hardware Checksum Offload (ainsi que TSO et LRO). Redémarrez ensuite le pare-feu.

    Étape 3 : Configuration des règles de filtrage étanches

    La règle d’or en gouvernance est le moindre privilège (Default Deny). Par défaut, créez une règle de blocage globale vers toutes les adresses privées (RFC 1918) sur votre zone DMZ et IoT.

    Dans l’onglet de configuration des règles de l’interface DMZ (VLAN 30) sur OPNsense, autorisez uniquement :

    • Les flux DNS vers l’IP d’OPNsense (port 53 UDP).
    • Le trafic sortant vers le WAN (Internet) pour les mises à jour.
    • Bloquez explicitement tout accès vers le VLAN 10 (Admin) et le VLAN 20 (Trusted).

    Résultat : Une posture de sécurité professionnelle à la maison

    Grâce à cette architecture, si mon serveur web en DMZ est compromis, l’attaquant se retrouve piégé dans une boîte de conserve numérique. Il lui est impossible de scanner mon réseau local ou d’accéder à l’hyperviseur Proxmox. Ce déploiement prouve qu’avec les bons outils open-source et une méthodologie rigoureuse, on peut appliquer une gouvernance de niveau entreprise au sein de son Home Lab.

  • Static HTML Hell (and How I Replaced It with a CSV‑Driven Gallery)

    Last month, while working on an artist’s portfolio (paintings/illustrations), I fell into the classic “100% static” website trap that quickly becomes unmanageable: every time a new piece was added, I had to open index.html, copy/paste a block, double-check image paths, alt text, tags… and then hope I didn’t break anything.

    The real problem wasn’t the “tech”—it was the workflow. The artist needed to be able to add a new artwork without touching HTML.

    So I separated the data (the list of artworks) from the rendering (the page) using a simple images.csv file loaded via JavaScript.

    Reference repo (full code):
    https://github.com/vladimir-monari/monartistique/blob/main/


    1) The idea: data in images.csv, automatic rendering in the DOM

    CSV structure (the “single source of truth”)

    I standardized an images.csv file with stable headers (important, because the code references them exactly as written):

    Nom de l'image,Chemin de l'image,Description de l'image,Tags
    Portrait Bleu,/img/bleu.jpg,Huile sur toile,abstrait-portrait
    Coucher de soleil,/img/soleil.jpg,Aquarelle,paysage-nature

    What each column is used for:

    • Nom de l’image: becomes the displayed title
    • Chemin de l’image: relative URL (e.g., /images/... or /img/...)
    • Description de l’image: descriptive text (also used as the image alt)
    • Tags: a list of tags separated by - (e.g., abstrait-portrait)

    That hyphen separator is intentional: it’s easy to type in Google Sheets, and trivial to parse in JS.


    2) index.html: a “shell” page (and that’s exactly what we want)

    In index.html, I kept:

    • everything structural (header, “about” section, footer)
    • the empty containers to be filled dynamically (#image-container, #tag-cloud)
    • dependencies (CSS + PapaParse) and the application script scripts.js
    • the fullscreen modal for large image display

    Key parts in your index.html:

    a) The critical containers

    <div id="tag-cloud"></div>
    <div id="image-container"></div>

    These are the “ports” where JavaScript injects the tag cloud and the gallery.

    b) The modal is already in place

    <div id="modal" class="modal">
    <span class="close">&times;</span>
    <img class="modal-content" id="modal-image">
    <div id="caption"></div>
    </div>

    This is a solid pattern: the structure is declarative in HTML, and JS only manipulates display, src, and text content.

    c) Dependencies: PapaParse is used, D3 is probably dead weight

    You currently load:

    <script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/5.3.0/papaparse.min.js"></script>
    <script src="https://d3js.org/d3.v5.min.js"></script>

    PapaParse is indeed used. But D3 isn’t used in the current scripts.js (your tag cloud is built with vanilla DOM). So you can remove D3 to lighten the page—unless you plan to build a real visualization with it later.

    d) Google Tag Manager / Analytics

    You have both GTM and gtag. It works, but keep in mind:

    • it’s extra JavaScript weight
    • ideally document what’s being tracked in a README (avoids “black box” maintenance)

    3) scripts.js: the full pipeline (CSV → objects → DOM → tags → filters)

    a) Loading the CSV

    function loadCSV(callback) {
    fetch('images.csv')
    .then(response => response.text())
    .then(data => callback(data))
    .catch(error => console.error('Erreur lors du chargement du CSV:', error));
    }

    Implications:

    • images.csv must be in the same folder as index.html (or you need to adjust the path)
    • fetch() won’t work properly if you open the page via file:// (more on that in pitfalls)

    b) Parsing + generating image cards

    The core part:

    Papa.parse(data, {
    header: true,
    complete: function (results) {
    results.data.forEach(function (d) {
    if (!d["Nom de l'image"] || !d["Chemin de l'image"]) return;

    var container = document.createElement('div');
    container.classList.add('image-wrapper');
    container.dataset.tags = d['Tags'];

    var img = new Image();
    img.src = d["Chemin de l'image"];
    img.alt = d["Description de l'image"];
    img.classList.add("image-responsive");
    ...

    Good call: the early return prevents most empty rows / malformed CSV entries from causing issues.

    Then:

    • dataset.tags powers filtering
    • alt comes from the description (good accessibility/SEO reflex)
    • the modal opens on click

    c) Tag cloud + filtering

    You count tags with:

    d['Tags'].split('-').forEach(function (tag) {
    tags[tag] = (tags[tag] || 0) + 1;
    });

    …and build the tag cloud with font sizes proportional to frequency.


    4) The automation flow (the part that truly changes everything)

    The workflow I implemented (and that holds up over time):

    1. The artist updates a sheet (Google Sheets / Excel)
    2. Export to CSV
    3. Commit the CSV + images into the repo
    4. git push
    5. Deploy (GitHub Pages or similar) → the gallery updates automatically

    The big win: no more manual HTML editing for each new artwork.


    5) Pitfalls I hit (and the concrete fixes)

    1) Empty rows / incomplete objects (PapaParse)

    You already handle this with:

    if (!d["Nom de l'image"] || !d["Chemin de l'image"]) return;

    I’d keep that logic and also defensively handle missing tags:

    const rawTags = (d['Tags'] || '').trim();
    container.dataset.tags = rawTags;

    Otherwise, split('-') on undefined can crash.


    2) Local CORS issues (fetch + file://)

    Classic: double-clicking index.html isn’t enough.

    Fix: run a local server (VS Code Live Server, python -m http.server, etc.).
    In practice, Live Server solves it instantly during development.


    3) XSS / injection via CSV (the real sensitive one)

    In your current code, this line is risky:

    document.getElementById("caption").innerHTML = this.alt;

    Even if “it’s just a CSV”, it’s still input data. If one day a value contains HTML, it will be interpreted.

    Simple and robust fix: never use innerHTML for plain text:

    document.getElementById("caption").textContent = this.alt;

    This makes a custom sanitize() function far less critical (and you can reserve it for the rare cases where you truly need to inject HTML).


    4) Subtle bug: duplicate #tag-cloud container

    In index.html, you already have:

    <div id="tag-cloud"></div>

    But in displayTagCloud(), you create a new div with the same id and insert it after the <h3>:

    var tagCloud = document.createElement('div');
    tagCloud.id = 'tag-cloud';
    document.querySelector('#container h3').insertAdjacentElement('afterend', tagCloud);

    Result: you can end up with two elements with the same id (invalid HTML, weird behavior).

    Fix: reuse the existing container:

    function displayTagCloud(tags) {
    var tagCloud = document.getElementById('tag-cloud');
    tagCloud.innerHTML = ''; // reset
    ...
    }

    6) styles.css: what works (and what I’d tweak)

    What’s good:

    • simple, readable, consistent layout
    • .image-wrapper as inline-block: great for a “mosaic” gallery
    • fullscreen modal with dark overlay: perfect for artwork viewing

    Two practical notes:

    a) max-width: 100vh on .modal-content

    You currently have:

    .modal-content {
    max-width: 100vh;
    max-height: 90vh;
    }

    Using vh for width is unusual: on wide screens it can limit the image width in a counter-intuitive way. I generally prefer:

    .modal-content {
    max-width: 95vw;
    max-height: 90vh;
    }

    b) Visual performance

    As the gallery grows, consider:

    • loading="lazy" on images
    • dedicated thumbnails (avoid loading a 4000px image just to display a small preview)

    7) Future improvements (SEO, UX, performance) — a realistic roadmap

    UX / Accessibility

    • close the modal by clicking outside the image + pressing Escape
    • focus management (when the modal opens, focus the close button)
    • add aria-label to the close button

    Performance

    • img.loading = "lazy";
    • pagination / batch loading (12 at a time)
    • separate thumbnails + full-res images (e.g., thumb_path and full_path columns in the CSV)

    SEO

    • alt is already good, but you can go further:
      • artwork titles as <h4> (or at least better semantic structure)
      • JSON-LD (ImageObject or CreativeWork) if you want stronger indexing

    Security

    • avoid innerHTML for anything coming from the CSV (caption, descriptions, titles)
    • validate image paths (optional, but useful if multiple people edit the CSV)

    Conclusion

    The real benefit of a CSV + JS rendering approach is that it turns a fragile static site into a lightweight “DIY CMS”—no server, no database, no back office. For an artist portfolio, it’s the right compromise: easy to maintain, fast to load, and scalable.

    Full code (structure, index.html, images.csv, scripts.js, styles.css):
    https://github.com/vladimir-monari/monartistique/blob/main/

  • Tuto : Domotiser une porte de garage avec un Shelly Plus 1 et retour d’état réel

    Le problème du contrôle à l’aveugle : mon retour d’expérience

    Il y a quelques mois, j’ai failli enfermer le chat dans le garage. En quittant la maison en voiture, j’ai actionné ma vieille télécommande Somfy RTS sans vérifier si la voie était libre. C’est à ce moment précis que j’ai compris qu’il me fallait une solution intelligente pour domotiser ma motorisation de garage, mais surtout, obtenir un retour d’état 100 % fiable (ouvert ou fermé) dans Home Assistant. Pas question de me baser sur une simple estimation temporelle !

    La solution idéale ? Le micro-module Shelly Plus 1. Grâce à son contact sec isolé, il est parfait pour piloter une carte électronique de motorisation sans risquer d’y injecter du 230V.

    Pourquoi le Shelly Plus 1 est parfait pour cette tâche

    • Contact sec (Dry Contact) : Les bornes I (Input) et O (Output) sont totalement isolées de l’alimentation électrique du module (L/N).
    • Entrée SW configurable : On peut y brancher un capteur de fin de course magnétique pour connaître la position physique exacte de la porte.
    • Fiabilité locale : Pas besoin de cloud, il communique en WiFi directement via MQTT ou le protocole Shelly de Home Assistant.

    Étape 1 : Le schéma de câblage sécurisé

    Avant toute manipulation, coupez le disjoncteur général. Pour mon installation, j’ai alimenté le Shelly Plus 1 en 230V AC (bornes L et N). Si vous préférez la très basse tension, le Shelly accepte aussi le 12V ou 24-48V DC via un cavalier de configuration.

    Raccordez ensuite les bornes I et O du Shelly en parallèle sur l’entrée « bouton poussoir » (généralement nommée Start ou Cyc) de votre moteur de garage. Pour le capteur magnétique, fixez la partie filaire sur le châssis fixe de la porte de garage et l’aimant sur la section mobile de la porte (en position fermée). Raccordez les deux fils du capteur entre la borne SW et la borne L (si alimenté en 230V, ou GND si alimenté en DC).

    Étape 2 : Configuration du Shelly Plus 1

    Une fois le module sous tension, connectez-vous à son point d’accès Wi-Fi temporaire pour accéder à l’interface d’administration. Voici les réglages indispensables :

    • Timer Auto-Off : Activez un arrêt automatique (Auto-off) après 1 seconde. Cela permet de simuler un appui bref sur un bouton poussoir.
    • Input Mode : Configurez l’entrée SW en mode « Detached » (détachée). Cela évite que l’ouverture physique du capteur ne déclenche le relais par inadvertance.

    Étape 3 : Intégration Home Assistant

    Grâce à l’intégration native Shelly, le module remonte immédiatement. Pour obtenir une entité de type cover propre, j’ai créé un template dans mon fichier configuration.yaml qui combine l’action du relais et l’état de l’entrée SW. Désormais, je contrôle ma porte de garage de manière sécurisée, avec l’assurance visuelle qu’elle est bien fermée.

  • Measuring Real-World ITE Thermal Phase Shift: An ESPHome and DS18B20 Tutorial

    Introduction: Why Measure Real-World Phase Shift?

    During an external thermal insulation (ITE) retrofit, the choice of insulation material (such as wood fiber) is often driven by theoretical thermal phase shift. However, there is often a gap between theoretical calculations and real-world performance on historical walls. This tutorial explains how to accurately measure this phase shift by embedding DS18B20 sensors within the wall layers and integrating them into Home Assistant using ESPHome.

    Hardware Setup: Multi-Layer Integration

    For this measurement, we use an ESP32 microcontroller flashed with ESPHome and three waterproof DS18B20 temperature sensors wired on a OneWire bus:

    1. Outdoor Sensor (T_ext): Placed on the exterior facade, sheltered from direct sunlight.
    2. Interface Sensor (T_intf): Placed precisely between the old brick/stone wall and the new ITE insulation.
    3. Indoor Sensor (T_int): Placed on the inner surface of the wall.

    ESPHome Configuration for the OneWire Bus

    Here is the YAML configuration to integrate into your ESPHome node to report the three temperatures with ultra-high 12-bit resolution:

    dallas:
      - pin: GPIO23

    sensor:
      - platform: dallas
        address: 0x2c00000b12a34f28
        name: "ITE Outdoor Temperature"
      - platform: dallas
        address: 0x3c00000b12b45e12
        name: "Insulation Interface Temperature"
      - platform: dallas
        address: 0x4c00000b12c56f23
        name: "Indoor Wall Temperature"

    Analyzing Phase Shift in Home Assistant

    Thermal phase shift is the time delay between the peak outdoor temperature and the peak interface/indoor temperature. By exporting your Home Assistant data to InfluxDB and Grafana, you can easily calculate the time delta (t2 – t1) of daily maximums. High-performance wood fiber should yield a measured shift of 10 to 12 hours, successfully flattening the summer heat spikes.

  • Mesurer le déphasage thermique réel d’une ITE : Tuto ESPHome et DS18B20

    Introduction : Pourquoi mesurer le déphasage réel ?

    Lors d’une rénovation thermique par l’extérieur (ITE), le choix de l’isolant (comme la fibre de bois) est souvent guidé par le déphasage thermique théorique. Cependant, entre les calculs théoriques et la réalité physique du bâti ancien, il y a souvent un fossé. Ce tutoriel explique comment mesurer précisément ce déphasage en installant des sondes DS18B20 au cœur des couches de votre mur et en les intégrant dans Home Assistant via ESPHome.

    Le montage matériel : Intégration multi-couches

    Pour cette mesure, nous utilisons un microcontrôleur ESP32 flashé sous ESPHome et trois sondes de température étanches DS18B20 câblées en bus OneWire :

    1. Sonde Extérieure (T_ext) : Placée sur la façade extérieure, protégée du rayonnement direct du soleil.
    2. Sonde Interface (T_intf) : Placée précisément entre l’ancien mur en brique/pierre et le nouvel isolant ITE.
    3. Sonde Intérieure (T_int) : Placée sur la surface intérieure du mur.

    Configuration ESPHome pour le bus OneWire

    Voici la configuration YAML à intégrer dans votre nœud ESPHome pour remonter les trois températures de manière ultra-précise avec une résolution de 12 bits :

    dallas:
      - pin: GPIO23

    sensor:
      - platform: dallas
        address: 0x2c00000b12a34f28
        name: "Température Extérieure ITE"
      - platform: dallas
        address: 0x3c00000b12b45e12
        name: "Température Interface Isolant"
      - platform: dallas
        address: 0x4c00000b12c56f23
        name: "Température Intérieure Mur"

    Analyse du déphasage sous Home Assistant

    Le déphasage thermique correspond au retard temporel entre le pic de température extérieure et le pic de température à l’interface ou à l’intérieur. En exportant vos données de Home Assistant vers InfluxDB et Grafana, vous pouvez facilement calculer le delta temporel (t2 – t1) des maximums journaliers. Une fibre de bois performante doit révéler un déphasage mesuré d’environ 10 à 12 heures, lissant efficacement les pics de chaleur estivaux.

  • Sécuriser le LAN familial : Double filtrage DNS et isolation par VLANs avec OPNsense et AdGuard Home

    Le défi de la gouvernance numérique au sein du foyer

    En tant qu’architectes de nos propres infrastructures domestiques, nous appliquons souvent des règles de sécurité strictes au bureau, mais laissons le réseau familial vulnérable. Entre les objets connectés (IoT) bavards, les consoles des enfants et les ordinateurs portables professionnels des parents, la surface d’attaque est immense. Pour sécuriser la vie numérique de la famille sans impacter l’expérience utilisateur, une simple clé WPA2 globale ne suffit plus. La solution ? L’isolation par VLAN combinée à un filtrage DNS granulaire et dynamique.

    Étape 1 : Segmentation réseau (VLAN) sous OPNsense

    La première règle d’or de la sécurité est le cloisonnement. Nous allons segmenter notre réseau en trois zones distinctes à l’aide de VLANs configurés sur notre routeur/pare-feu OPNsense :

    • VLAN 10 (Parents/Admin) : Accès total à l’interface d’administration et à internet.
    • VLAN 20 (Kids) : Accès internet filtré, aucune communication possible avec le VLAN 10 ou l’IoT.
    • VLAN 30 (IoT) : Accès internet restreint au strict minimum, isolation complète du reste du réseau.

    Pour cela, rendez-vous dans Interfaces > Other Types > VLAN sur OPNsense. Créez vos interfaces, puis définissez des règles de pare-feu strictes dans Firewall > Rules pour interdire le trafic inter-VLAN (notamment du VLAN 20/30 vers le VLAN 10).

    Étape 2 : Configuration d’AdGuard Home pour un filtrage par profil

    Plutôt que de déployer plusieurs serveurs de filtrage, nous allons utiliser les fonctionnalités avancées d’AdGuard Home pour appliquer des politiques de sécurité et de contrôle parental différenciées selon les clients.

    Dans l’interface d’AdGuard Home, accédez à la section Paramètres du client. Vous pouvez y ajouter des appareils spécifiques ou des sous-réseaux entiers (par exemple, la plage IP du VLAN 20 des enfants). Pour ce profil « Kids », activez :

    • Le blocage des services spécifiques (TikTok, YouTube restreint, Discord selon l’âge).
    • La recherche sécurisée (SafeSearch) obligatoire sur Google, Bing et YouTube.
    • L’utilisation des serveurs DNS amont de type « Family Protection » (comme Cloudflare 1.1.1.3).

    Étape 3 : Verrouiller les échappatoires (Contrecarrer le contournement du DNS)

    Les adolescents sont astucieux. Si un enfant configure manuellement le DNS 8.8.8.8 sur sa console ou son smartphone, il contournera AdGuard Home. Pour empêcher cela, nous devons mettre en place une règle de NAT Port Forward sur OPNsense.

    Créez une règle de redirection de port (DNAT) pour intercepter toutes les requêtes sortantes sur le port 53 (UDP/TCP) qui ne sont pas destinées à l’IP de votre serveur AdGuard Home, et redirigez-les de force vers ce dernier. Faites de même pour le port 853 (DNS over TLS) pour bloquer ou rediriger le DNS sécurisé tiers.

    Conclusion

    En combinant la puissance de routage d’OPNsense et la flexibilité d’AdGuard Home, vous obtenez une infrastructure domestique digne d’une entreprise. Vos données personnelles sont isolées des objets connectés potentiellement vulnérables, et la navigation de vos enfants est protégée de manière transparente et centralisée.

  • Securing the Family LAN: Dual DNS Filtering and VLAN Isolation with OPNsense and AdGuard Home

    The Challenge of Digital Governance in the Smart Home

    As architects of our own home labs, we often apply strict security rules at work but leave our family network vulnerable. Between chatty IoT devices, children’s consoles, and parents’ work laptops, the attack surface is massive. To secure the family’s digital life without degrading the user experience, a simple global WPA2 key is no longer enough. The solution? VLAN isolation combined with granular, dynamic DNS filtering.

    Step 1: Network Segmentation (VLAN) on OPNsense

    The first golden rule of security is compartmentalization. We will segment our network into three distinct zones using VLANs configured on our OPNsense router/firewall:

    • VLAN 10 (Parents/Admin): Full access to the admin interface and the internet.
    • VLAN 20 (Kids): Filtered internet access, zero communication allowed with VLAN 10 or the IoT network.
    • VLAN 30 (IoT): Internet access restricted to the bare minimum, completely isolated from the rest of the network.

    To do this, navigate to Interfaces > Other Types > VLAN on OPNsense. Create your interfaces, then define strict firewall rules in Firewall > Rules to block inter-VLAN traffic (especially from VLAN 20/30 to VLAN 10).

    Step 2: Configuring AdGuard Home for Profile-Based Filtering

    Instead of deploying multiple filtering servers, we will use the advanced features of AdGuard Home to apply customized security and parental control policies based on the client.

    In the AdGuard Home UI, go to the Client Settings section. Here, you can add specific devices or entire subnets (such as the IP range of the Kids’ VLAN 20). For this « Kids » profile, enable:

    • Block specific services (TikTok, YouTube restricted, Discord depending on age).
    • Enforced SafeSearch on Google, Bing, and YouTube.
    • Upstream DNS servers set to « Family Protection » (like Cloudflare 1.1.1.3).

    Step 3: Blocking the Bypasses (Thwarting DNS Circumvention)

    Kids are tech-savvy. If a child manually sets their DNS to 8.8.8.8 on their console or smartphone, they will bypass AdGuard Home. To prevent this, we must set up a NAT Port Forward rule on OPNsense.

    Create a port redirection rule (DNAT) to intercept all outgoing queries on port 53 (UDP/TCP) that are not destined for your AdGuard Home IP, and forcibly redirect them to it. Do the same for port 853 (DNS over TLS) to block or redirect third-party secure DNS.

    Conclusion

    By combining the routing power of OPNsense and the flexibility of AdGuard Home, you achieve an enterprise-grade home network. Your personal data is isolated from potentially vulnerable smart devices, and your children’s browsing is protected seamlessly and centrally.