Projet TODD
Description
TODD avant
Le projet TODD (pour TIRIA Organisation Digital Display) a été conçu il y a 7 ou 8 ans comme un système d'affichage dynamique (Digital Signage). Il consiste en une flotte de boîtiers, basés sur une carte de développement Raspberry Pi, connectés à des moniteurs. Ils sont tous recensés sur une plateforme d'outils de communication concue par TIRIA : MyComBox. Depuis une interface spécifique, le client MyComBox peut administrer les boitiers dont il dispose. Il peut notamment attribuer du contenu qu'il aura préalablement conçu : images, vidéos ou liens.

Le boitier TODD embarque une distribution classique pour carte Raspberry Pi mais aussi un fork de Screenly (maintenant Anthias), un système d'affichage dynamique open source, basé sur Python, dédié aux Raspberry Pi, qui va gérer la diffusion des médias et le processus de communication avec MyComBox. Une fois configuré, le boîtier TODD est capable de mettre à jour le matériel à diffuser en interrogeant à intervalle régulier la plateforme MyComBox pour récupérer la liste des contenus à diffuser. Chaque contenu a un ordre de passage, une URL, un type (image, vidéo ou HTML), une date de début et de fin, un flag de mise en service et une action. Ce dernier paramètre sert au boitier à comprendre ce qu'il doit faire d'un contenu :
NULL, il ne doit rien faire ;ADD, le contenu est nouveau il doit l'ajouter dans sa liste ;DELETE, il doit le supprimer de sa liste.
Ainsi s'effectue la synchronisation entre ce qui est diffusé sur un boitier et ce qui est présent sur MyComBox.
Un contenu peut exister sur le boîtier mais ne pas être activé. La liste de contenus est donc divisée en deux parties : les contenus actifs et inactifs. Dans la liste des contenus inactifs, il y a tous les contenus avec le flag de mise en service égal à FALSE ainsi que les contenus dont la date de fin de diffusion a expiré.
Évolution du projet
Dernièrement, TIRIA a procédé à une mise à jour majeure de son framework maison MMI dans le but de fonctionner avec un système de plugins. Ce framework est le vaisseau amiral de la flotte TIRIA. Déployé dans un entreprise, il lui permet tout à la fois de suivre et manager un chaîne de production comme éditer des factures. Dans ce cadre là, il devenait évident de proposer un plugin TODD qui permettrait de gérer dans le même écosystème des affichages dynamiques au sein d'une entreprise.

Dans cette perspective, le plugin TODD devra non seulement permettre de gérer des boitiers (c'est-à-dire leur attribuer des contenus à diffuser) mais aussi proposer des modules de conception de contenu avec des thématiques ciblées. En addition, un contenu pourra prendre en charge de l'interactivité sachant qu'il pouvait être diffusé sur un écran tactile.
Missions
L'objectif de mon stage a donc été de m'occuper d'intégrer TODD dans le framework MMI. Partant d'un prototype réalisé par une IA pour une ancienne version de MMI, j'ai dû répartir mon travail en deux phases :
- adaptation de l'extension TODD pour MMI et création de modules notamment interactifs ;
- gestion des contenus créés dans l'extension et attribution de ces contenus aux boitiers, directement depuis l'extension (sans passer par MyComBox).
Travail réalisé
Planning
| Semaines | Mission |
|---|---|
| 1 | Observation des services dans l'entreprise Exploration du système TODD Propositions d'améliorations (UI et UX) |
| 2 | Mise en place de la station d e travail (WSL, Docker, TODD) Débogage |
| 3 | Interface de sélection Création d'un nouveau module Vrai/Faux |
| 4 | Nouvelles interface TODD Création du nouveau module Votes |
| 5 | Nouveau dashboard (design + refactorisation) Conceptualisation et développement de ToddModule (fluent API) Synchronisation des aperçus |
| 6 | Veille sur Hidden API ESPN Nouveau module Pronostics sportifs basé sur ToddModule Nouveau module Mosaïque basé sur ToddModule |
| 7 | Veille API Open-meteo et geo.api.gouv.fr Nouveau module Météo basé sur ToddModule Dashboard des boitiers Découplage SQL |
| 8 | Conceptualisation et développement de ToddDevice API MyComBox Gestion contenus/boitiers |
| 9 | Débogage synchronisation MyComBox Conception réseau local avec boitiers Raspberry Pi |
Analyse détaillée
Nous proposons dans cette partie de présenter quelques points ou jalons essentiels dans le développement de l'extension TODD. Il s'agit pas de présenter tout le projet mais de mentionner des étapes cruciales explicitant certains fonctionnements ou certains partis pris dans la programmation.
Modèle MVC
Lors de la prise en main du prototype de l'extension TODD pour le framwork MMI, je me suis vite aperçu que, si chaque module était fonctionnel, il y avait des défauts dans le code. Particulièrement, j'ai noté que dans un même fichier, on popuvait retrouver :
- des requêtes SQL ;
- des parties HTML construites par concaténation de chaînes (je veux dire des parties entières du DOM fabriquées ainsi) ;
- du style CSS déclaré en dur, intégré dans le code HTML avec des attribut
style.
Voici un exemple d'un fichier view.php (des parties du code ont été masquées pour gagner de la place)
<?php
// plugins/todd/modules/todd_polls/view.php
// ...
$votes = $db->getAssocArray("SELECT option_id, count(*) as cnt FROM {DBPFX}todd_polls_votes WHERE poll_id = $poll_id GROUP BY option_id", 'option_id');
$totalVotes = array_sum(array_column($votes, 'cnt'));
?>
<?php ob_start(); ?>
<style>
.poll-container {
max-width: 600px;
margin: 20px auto;
padding: 20px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
/* ... */
</style>
<?php $module_head_content = ob_get_clean(); ?>
<div class="poll-container" id="poll_container_<?php echo $poll_id; ?>">
<div class="poll-question"><?php echo htmlspecialchars($poll['title']); ?></div>
<?php if ($poll['question']): ?>
<p class="text-center lead"><?php echo htmlspecialchars($poll['question']); ?></p>
<?php
endif; ?>
// ...
</div>
<script>
$(document).ready(function() {
// Vote Handler
$('.btn-vote').click(function() {
var btn = $(this);
var pollId = btn.data('poll');
var optionId = btn.data('option');
// Disable all buttons
$('.btn-vote').prop('disabled', true).css('opacity', 0.6);
// Send Vote
$.ajax({
url: 'plugins/todd/scripts/todd.ws.php',
type: 'POST',
data: {
action: 'moduleAction',
module: 'todd_polls',
module_action: 'vote', // Need to implement this in handler.php!
poll_id: pollId,
option_id: optionId
},
dataType: 'json',
success: function(res) {
// ...
},
error: function() {
// ...
}
});
});
});
</script>Ce code m'a semblé tout de suite extrêment compliqué à maintenir. Non seulement, c'était très difficile de retrouver certaines fonctionnalités dans ce code où tout était mélangé mais aussi chaque module développait son comportement propre, sans même faire appel à des routines du framework MMI, ce qui n'était pas aisé à comprendre.
Donc, le premier chantier que je me suis mis à entreprendre, a été d'adopter un modèle Modèle-Vue-Contrôleur afin que chaque partie du code s'occupe d'une tâche bien définie. Ce modèle, je l'ai pensé reproductible et j'ai tenté de déboguer certains modules en les passant dans ce même moule.
Dorénavant, une arborescence d'un module TODD ressemblera à ceci :
todd_poll
├── admin.php
├── assets
│ ├── css
│ └── js
├── controllers
│ ├── admin.inc.php
│ ├── payload.inc.php
│ └── view.inc.php
├── handler.php
├── install.php
├── modal_add_voter.php
├── modal_import_csv.php
├── models
│ └── ToddPollModel.php
├── script.js
└── view.phpDans cette arborescence, on peut distinguer :
- Des points d'entrée, c'est à dire des fichiers au nom générique qui vont être appelés par le routeur de l'extension TODD dans différents contextes :
admin.php: c'est le point d'entrée de l'interface de cŕeation/édition du module (le back-office). C'est généralement un formulaire avec des champs appropriés au paramétrage ainsi qu'un ifram d'aperçu synchronisé.view.php: c'est le point d'entrée de l'interface publique (le front-office), c'est-à-dire le contenu affiché par les boitiers TODD.handler.php: c'est le gestionnaire de requêtes (les requêtes asynchrones AJAX et surtout la gestion des formulaires POST). IL intercepte l'action de l'utilisateur et la redirige vers la bonne logique.install.php: ce script n'est exécuté qu'une seule fois à l'installation de l'extension TODD dans le framework MMI. Il contient essentiellement les créations des tables dans la base de données MySQL propre à MMI, des données par défaut, des droits/permissions liés au module, etc.
- Le modèle qui s'occupe de la gestion des données. Ici par exemple,
models/ToddPollModel.phpva contenir toutes les requêtes SQL (opérations CRUD) liées aux modules. C'est le seul fichier qui communique directement avec la base de données. Ainsi, l'accès aux données est bien circonscrit. - Les contrôleurs qui font le pont entre les données et l'affichage. Ils contiennent la logique métier.
controllers/admin.inc.php: gère la logique spécifique à l'interface d'administration du module (préparation du payload principalement, fait appel au contrôleurpayload.inc.phpsi édition ou définit par défaut des valeurs si création).controllers/view.inc.php: gère la logique spécifique à l'interface publique (vérification que l'utilsateur a le droite de voter, récupération des questions du sondage, etc.).controllers/payload.inc.php: gère la logique du traitement des données (vérification/transformation de format, conversion en JSON pour l'utilisation côté client,...)
- Des vues partielles qui représentent des morceaux de l'interface complexes dans des fichiers séparés afin de garder le code plus lisible. Ici, ce sont deux fichiers utilisé dans la partie création/édition qui vont créer des modales pour des tâches spécifiques : ajouter un votant, importer une liste de votant depuis un fichier CSV.
- Des ressources statiques :
assets/cssetassets/jscontient respectivement des feuilles de styles (design spécique au module) et les scripts JavaScript modulaires.script.js: ce fichier JavaScript à la racine du module n'est présent que parce qu'il est requis par l'extension TODD qui prend en compte d'anciens comportements mais en réalité, il est vide.
Affichage d'un contenu
À la différence des autres plugins qui fonctionnent dans l'écosystème de la plateforme, TODD doit pouvoir avoir un rendu autonome, la plateforme devant assurer la constitution des éléments HTML à afficher à l'extérieur de son écosystème.
L'objectif est donc de donner cette indépendance au plugin TODD et pouvoir afficher une vue correspondant à un contenu TODD. Un contenu TODD est associé à une URL publique, cette URL permettant au dispositif de diffusion de récupérer le contenu à afficher. Ce contenu correspond à du HTML, qui est généré par le serveur hébergeant l'application.
Les URLs des contenus à diffuser sont stockées sous cette forme :
http://127.0.0.1:8081/project_todd/todd_view/21/1Dans un premier temps, cette URL est traduite par un fichier .htaccess contenu dans le répertoire todd_view à la racine du framework :
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Capture l'ID ($1) et de façon optionnelle le Mode ($2)
RewriteRule ^([0-9]+)(?:/([0-9]+))?/?$ index.php?id=$1&mode_id=$2 [L,QSA]
</IfModule>Ce fichier intercepte une URL propre demandée par le client et la réécrit en interne vers un contrôleur frontal (index.php). À ce stade, le début de l'URL est déjà consommée et il ne reste que 21/1.
La règle ^([0-9]+)(?:/([0-9]+))?/?$ capture 21 et le met dans la variable $1 et capture 1 et le met dans la variable $2. La règle RewriteRule convertit donc ce segment de l'URL en paramètre dynamique GET, sans modifier l'URL affichée dans le navigateur de l'utilisateur et l'URL devient :
http://127.0.0.1:8081/project_todd/todd_view/index.php?id=21&mode_id=1La page index.php récupère les paramète GET :
$id = isset($_GET['id']) ? intval($_GET['id']) : 0;
if (!$id) {
die("ID manquant.");
}et vérifie dans la base de donnée (via le modèle approprié) si le contenu existe :
$content = ToddContentModel::getContentById($id);
if (!$content) {
die("Contenu introuvable.");
}Le nom du module et son payload est récupéré dans les données :
$module_code = $content['module_code'];
$payload = json_decode($content['payload_json'], true);
if (!is_array($payload)) {
$payload = [];
}Le mode est traduit de la même façon :
$mode_id = isset($_GET['mode_id']) ? intval($_GET['mode_id']) : 1;
$render_mode = ($mode_id === 2) ? 'partial' : 'complete';On construit ensuite le chemin vers la vue publique à afficher avec le bon mode :
$viewFile = BASE_PATH . "plugins/todd/modules/" . $module_code . "/view.php";
if (!file_exists($viewFile)) {
die("Vue manquante pour le module " . htmlspecialchars($module_code) . ".");
}Il sera inclus par la suite dans une squelette HTML de base mais passé en mémoire préalablement.
ob_start();
include $viewFile;
$module_html_content = ob_get_clean();On utilise ce principe de mémoire tampon car le fichier view.php (défini ailleurs) aura directement accès aux variables $render_mode et le $payload par exemple qui elles sont définies dans le fichier index.php.
Classe ToddModule
Ayant codé de nouveaux modules pour l'extension TODD, je me suis vu me répéter dans certaines parties du code. J'ai donc fait évoluer le modèle donné ci-dessus vers un modèle à la fois plus robuste et plus reproductible.
Intentions
J'ai imaginé et créé une classe ToddModule qui permet de définir un module à partir de ses éléments constitutifs. Un module consiste en :
- une interface de création/édition qui permet de définir des données à afficher ;
- une vue publique qui peut être complète ou partielle suivant le contexte d'affichage.
Ayant dans un autre projet utilisé la librairie GSAP pour animer des éléments dans une page HTML en utilisant du Javascript, j'ai aimé la manière de l'utiliser : on créer un objet et on y ajouter des propriétés qui correspondent à des parties de l'animation, en chainant les méthodes associées. Ici, lorsqu'on crée un module, il s'agit de définir un élément de type formulaire auquel on va ajouter des champs de différents types qui vont se référer à autant d'éléments dans le contenu à afficher. L'ordre dans lequel ces champs seront déclarés les positionneront dans l'interface. Ce mode de conception logicielle est appelé fluent interface.
Organisation
La classe ToddModule est conçue comme une classe abstraite car elle ne fait que définir un cadre de fonctionnement :
- des méthodes sont définies explicitement ;
- d'autres méthodes sont obligatoires mais on laisse le développeur du module les implémenter comme il le souhaite (ou presque).
Dans le même esprit, la classe ToddModule intègre des propriétés protégées (au sens de la programmation orientée objet) afin de parfaire l'encapsulation de la manipulation des données.
Principes de fonctionnement
On ne peut pas instancier la classe directement mais le développeur d'un module va définir une nouvelle classe qui va étendre celle-ci. Le fonctionnement repose sur trois piliers.
- Hydratation : lorsque cette nouvelle classe définissant le module sera appelée, elle va interroger la base via le constructeur et une ID passée en paramètre afin de retrouver si des éléments liés à cette instance existe dans la base (autrement dit si un contenu utilisant le module a déjà été créé). Cela permet d'hydrater le module avec les données stockées dans le payload correspondant.
- Construction du formulaire : le développeur va concevoir l'interface d'administration en utilisant très simplement une série de méthodes :
addTab()pour ajouter un onglet ;addRow()pour ajouter une ligne de champs ;addField()pour ajouter un champs (plusieurs types possibles)
L'interface sera construite grâce à la méthode renderAdmin() qui va router vers deux fichiers core_admin_shell.js et core_admin_shell.php qui vontg être chargés de decoder l'objet à afficher et construire la page relative à cet objet.
- Affichage : le module est responsable de son affichage. Une méthode
display()s'occupe de router en fonction du mode passé en paramètre (complet ou partiel) vers le fichierpublic_complete.phpoupublic_partial.php. On peut envisager d'autres modes en fonction des modules. D'autre part, un système de hook permet d'incorporer les librairies nécessaires aux vues admin et publiques via les méthodesenqueueAdminAssets()etenqueuePublicAssets()respectivement. Si le fichier permettant de générer la vue n'est pas trouvé, une vue de secours est produite afin de ne pas casser le fonctionnement de l'application.
Module mosaïque
Une de mes tâches les plus complexes aura été de concevoir un affichage modulaire combinant plusieurs contenus sous la forme d'une mosaïque. J'ai développé pour cela un module spécifique todd_mosaic dont j'expose ici le concept dans les grandes lignes, l'architecture adoptée ainsi que quelques détails significatifs de son implémentation.
Design
J'ai conçu le module Mosaïque comme un espace de composition libre, avec une utilisation la plus simple possible, devant permettre :
- une agrégration de contenus déjà existants au sein d'une unique fenêtre de diffusion ;
- une système de grille de type bento permettant un positionnement dynamique par le biais d'un glisser-déposer et de redimensionnement symétriques ;
- la garantie que les processus agrégés soient isolés les uns des autres et n'entrent pas en collision, notamment lors des phases d'interaction.


Architecture
Le cœur du module todd_mosaic doit strictement hériter de la classe abstraite ToddModule. Cependant, comme je l'ai exposé plus haut, l'utilisation de la Fluent API devient inopérant ici. En effet, il ne s'agit pas de proposer une interface avec des champs mais bien un tout autre modèle. J'ai donc simplement surchargé la fonction renderAdmin() de ToddModule et défini mosaic_admin_shell.php et mosaic_admin_shell.js qui viendront remplacer les fichiers originels core_admin_shell. Ces fichiers seront chargés via la fonction de hook enqueueAdminAssets().
L'éditeur fournit une scène, adossée à une grille CSS, et un liste de contenus dans un tableau. L'utilisateur glisse-dépose du contenu de la liste dans cette grille et doit pourvoir le redimensionner. Voici le mockup imaginé en ce sens.

J'ai souhaité que toute la logique de l'agencement ne soit pas déléguée au client mais assuré côté serveur. J'ai défini deux classes :
ToddMosaicContainerqui va définir la structure de la grille avec une gestion abstraite mais contrôlé du contenu. Cet objet aura comme propriété le nombre de colonnesgrid_colset de lignegrid_rowsde la grille et la listecellsdes tuiles à agencer.ToddMosaicCellqui va désigner une tuile dans la mosaïque. Cette tuile aura la pleine connaissance de sa position dans la grille (variablexetydésignant le coin supérieur gauche) et de ses dimensions (hetwdésignant le nombre de cellules de la grille en hauteur et en largeur). Elle aura aussi comme propriété l'ID du contenu à affiché et le type de vue. Enfin, elle connaîtra aussi l'instance du container parentToddMosaicContainer
Ainsi, la classe ToddMosaic sera conçue sur un payload très simple n'indiquant que les dimensions de la grille.
Dans la vue publique d'une mosaïque, les contenus seront chargés dans des iFrames. C'est la seule solution qui permette d'isoler chaque module TODD dans son écosystème et de cloisonner les styles CSS propres, les exécutions JavaScript et les variables PHP.
Voici le rendu final :
Vérification anti-collision
Afin de détecter la collision entre les cellules de la vue en mosaïque (et pouvoir ainsi interdire tout chevauchement), nous utilisons un algorithme pensé pour les jeux vidéos dit de la théorie AABB pour Aligned Axis Bounding Box.
Voici une implémentation de cet algorithme dans la fonction add() de la classe ToddMosaicContainer afin de ne pas ajouter une cellule qui chevaucherat les autres.
public function add(ToddMosaicCell $newCell): void
{
// 1. Détection de chevauchement algorithmique AABB (Axis-Aligned Bounding Box)
foreach ($this->cells as $existingCell) {
// Conditions de collision exactes pour la CSS Grid (bornes limitrophes incluses logiciellement)
$overlap = (
$newCell->getX() < ($existingCell->getX() + $existingCell->getWidth()) &&
($newCell->getX() + $newCell->getWidth()) > $existingCell->getX() &&
$newCell->getY() < ($existingCell->getY() + $existingCell->getHeight()) &&
($newCell->getY() + $newCell->getHeight()) > $existingCell->getY()
);
if ($overlap) {
throw new Exception("Overlap conflict: Impossible d'ajouter la cellule (Content ID: {$newCell->getContentId()}) : elle chevauche la zone d'une cellule existante.");
}
}
// 2. Ajout validé, on l'intègre au tableau
$this->cells[] = $newCell;
}Positionnement CSS déléguée à la tuile
Afin de respecter le contexte de programmation orienté objet, le conteneur global n'a pas à connaitre la syntaxe stylistique de son enfant. La classe ToddMosaicCell résout donc elle-même ses coordonnées et sa taille en déclarations exploitables par une grille CSS. Une fonction getGridAreaCss() va définir l'attribut CSS grid-area pour l'instance d'une tuile permettant ainsi de la positionner correctement dans la grille.
public function getGridAreaCss(): string
{
// En CSS Grid, end-line est la ligne située APRÈS la dernière remplie.
$rowEnd = $this->y + $this->height;
$colEnd = $this->x + $this->width;
return "grid-area: {$this->y} / {$this->x} / {$rowEnd} / {$colEnd};";
}Isolation par contexte
Dans la chaîne de rendu terminal views/public_complete.php, la grille est dessinée. L'attribut src de l'iFrame fait directement appel au routeur noyau du CMS (todd_view) permettant à ce dernier de re-dérouler l'intégralité du cycle de vie du sous-module indépendamment du moteur parent.
La fonction display() de la classe ToddMosaicContainer va donc construire une grille et y injecter autant d'iFrames que de tuiles en parcourant la liste cells.
public function display(): string
{
$html = '';
// Initialisation de CSS Grid
$html .= '<div class="todd-mosaic-grid" style="display: grid; grid-template-columns: repeat(' . $this->grid_cols . ', 1fr); grid-template-rows: repeat(' . $this->grid_rows . ', 1fr);">';
foreach ($this->cells as $cell) {
$html .= ' <div class="mosaic-cell-wrapper" style="' . $cell->getGridAreaCss() . '">';
$html .= ' <div class="mosaic-cell-overlay"></div>';
$html .= ' <iframe src="' . $cell->getIframeUrl() . '" class="mosaic-cell-iframe" style="width:100%; height:100%; border:none;"></iframe>';
$html .= ' </div>';
}
$html .= '</div>';
return $html;
}Gestion des boitiers
La dernière tâche a réaliser dans ma mission aura été de pouvoir gérer l'attribution de contenus dans les boitiers et ce directement depuis l'extension TODD.
Principe
Les possesseurs de boîtiers TODD conçoivent normalement des contenus qu'ils ont créés en autonomie via des logiciels tiers et les asssignent depuis une interface dans MyComBox leurs boîtiers. Un boîtier TODD est conçu pour s'authentifier sur MyComBox et aller chercher la liste des contenus qui lui est attribuée directement depuis ce site.
Maintenant, les utilisateurs de boîtiers TODD peuvent concevoir des contenus directement dans l'extension TODD. Comment va-t-on procéder pour affecter les contenus directement depuis cette interface ? On va utiliser une fonctionnalité cachée (c'est-à-dire inconnue des utilisateurs) : on peut changer les contenus à diffuser directement depuis un boîtier en se connectant à son adresse IP. Le script de mise à jour est prévu pour indiquer que son contenu a changé localement et que celui-ci est prioritaire sur le contenu de MyComBox. L'idée est donc d'actualiser le contennu dédié au boîtier sur MyComBox en se faisant passer pour lui.
Fonctionnement
On a créé un modèle ToddDeviceModel qui va agir comme une passerelle entre MyComBox et l'extension TODD. L'extension va interroger grâce à ce modèle l'API de MyComBox afin de pouvoir synchroniser les données concernant les boitiers sur MyComBox avec la base locale. On utilise deux tables :
screen_ddqui stocke les données relatives aux boitiers (on ne récupèrera que celles dont l'utilisateur de l'extension TODD est propriétaire) ;assetsqui stocke les données relatives aux contenus (URLs et ID des boitiers attribués notamment).
Lors de toute modification d'un actif sur un boitier (ajout, suppression, réordonnancement), le système effectue un appel API puis efface les enregistrements locaux du boîtier pour les réinsérer à partir de la réponse de l'API.
Voici par exemple à quoi ressemble une fonction de ToddDeviceModel faisant appel à cette API pour ajouter un asset à un boîtier.
public static function addLocalAsset(string $hostNameId, array $data): bool
{
$mcb_api_url = App::getOption('todd_settings')['mcb_api_url'] ?? '';
if (empty($mcb_api_url)) throw new \Exception("URL API MCB non configurée.");
$targetUrl = $mcb_api_url . '/api/screen_dd_api.php?action=assets_update&hostNameId=' . urlencode($hostNameId);
global $db;
$sql = "SELECT MAX(play_order) as max_order FROM `" . DBPFX . "assets` WHERE hostNameId = :host";
$row = $db->getRow($sql, [':host' => $hostNameId]);
$nextOrder = isset($row['max_order']) ? ((int)$row['max_order'] + 1) : 0;
$payload = [
'name' => $data['name'] ?? 'Nouveau contenu',
'start_date' => $data['start_date'] ?? '0000-00-00 00:00:00',
'end_date' => $data['end_date'] ?? '0000-00-00 00:00:00',
'duration' => $data['duration'] ?? 10,
'is_enabled' => 1,
'uri' => $data['uri'] ?? '',
'mimetype' => $data['mimetype'] ?? '',
'nocache' => 0,
'play_order' => $nextOrder
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $targetUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(['model' => json_encode($payload)]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
throw new \Exception("Erreur CURL : " . curl_error($ch));
}
if ($http_code !== 200) {
throw new \Exception("Code HTTP " . $http_code . " : " . strip_tags((string)$response));
}
$json = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception("Réponse JSON invalide en retour Add : " . strip_tags((string)$response));
}
// L'API MCB renvoie soit {"status":1} soit directement l'objet {"asset_id":27, ...} lors d'un ajout
if ((!isset($json['status']) || $json['status'] !== 1) && !isset($json['asset_id'])) {
$err = isset($json['msg']) ? $json['msg'] : "Erreur logicielle API Add. Payload : " . strip_tags((string)$response);
throw new \Exception("API Add Info: " . $err);
}
return self::syncDeviceAssets($hostNameId);
}La dernière ligne de cette fonction fait appel à la fonction syncDeviceAssets() qui va permettre la synchronisation avec la base locale.
Sécurité
Lors de l'appel API, on transmet dans la requête la variable hostNameId qui établit la relation entre le boitier (table screen_dd) et ses contenus à diffuser (table assets) : se faisant, l'extension TODD se fait réellement passer pour le boitier.
On peut voir par exemple cette variable dans la construction de l'URL de la requête vers l'API screen_dd_api.php de MyComBox dans la fonction précédente :
$targetUrl = $mcb_api_url . '/api/screen_dd_api.php?action=assets_update&hostNameId=' . urlencode($hostNameId);Côté MyComBox, des adresses IP sont rescencées dans une liste d'hôtes autorisés afin d'assurer la sécurité et d'interdire l'accès à des requêtes venant de machines inconnues.
Expérience utilisateur
J'ai ajouté un onglet Boitiers au tableau de bord TODD. Dans cet onglet, j'ai conçu deux états :
- État 1 : Vue Liste : Les boitiers rescencés dans la base locale sont affichés sous la forme d'une grille de cartes. Pour optimiser le chargement de la page, le HTML complet de chaque boitier est préchargé côté serveur en référence à la base locale et caché dans des balises
<template id="device-complete-tpl-...">.
- État 2 : Vue Détaillée : Lorsque l'utilisateur clique sur une carte, la vue bascule pour afficher le composant détaillé (injecté depuis le
<template>). En dessous, une interface sur deux colonne permet de gérer la playlist par glisser-déposer :- Colonne de gauche : la playlist actuelle affiche les contenus rattachés au boîtier (récupéré en AJAX localement puis mis à jour en tâche de fond). Cette liste sert ` la fois de drop zone pour les dépôt de nouveaux contenus mais aussi elle permet de réorganiser les contenus au sein de la playlist (ordre ou suppression).
- Colonne de droite : le catalogue TODD affiche la liste des modules crées, filtrable par type. Les utilisateurs peuvent glisser-déposer du contenu de cette liste vers la playlist pour l'y ajouter.

Lors de l'affichage de la liste des boitiers dans l'état 1, ou de l'affichage de la playlist dans l'état 2, on utilise en premier la base locale. Pendant ce temps, en arrière plan, la requête concernant la liste des boitiers ou le changement de contenu dans la playlist part sur MyCombox pour mise à jour. Puis, au retour de la requête, la base locale est effacée et réinitialisée avec les nouvelles données. Un message discret est diffusé à l'utilisateur pour l'informer de la procédure et de son status. On a ainsi un affichage très fluide, sans attente pour l'utilisateur et surtout non bloquant pour la navigation.
C'est la classe ToddDevice qui va gérer son rendu dans les différents état via sa fonction render() : le contexte est connu du fichier parent donc on utilise un cache pour insérer le fichier adéquat.
public function render(string $context = 'card'): string
{
// On prépare l'instance pour la vue
$device = $this;
// On définit le chemin vers le dossier des vues du module
$viewPath = PLUGIN_PATH . 'todd/templates/devices/';
ob_start();
switch ($context) {
case 'complete':
include $viewPath . 'device_complete.php';
break;
case 'card':
default:
include $viewPath . 'device_card.php';
break;
}
return ob_get_clean();
}Dans le fichier todd_dashboard_devices.php composant l'interface de l'onglet Boîtiers, on appellera la fonction render() avec le contexte approprié (on retrouve l'élément <template> dont on a parléplus haut) :
<?php if (!empty($devices)): ?>
<?php foreach ($devices as $device): ?>
<div class="device-card-wrapper" data-screen-id="<?= $device->screen_id ?>">
<!-- PRE-LOAD DU COMPONENT COMPLET (OPTIMISATION AJAX) -->
<template id="device-complete-tpl-<?= $device->screen_id ?>">
<?= $device->render('complete') ?>
</template>
<?= $device->render('card') ?>
</div>
<?php endforeach; ?>
<?php else: ?>Feedback
Points forts
Architecture globale
Le point de départ de ce projet était un prototype construit par une IA après plusieurs itérations, adossé à un framework maison qui a connu une mise à jour majeure entre temps. Pour l'utilisateur, on avait une appllication qui fonctionnait mais de l'autre côté du miroir, le code n'avait pas du tout les standards attendus en terme de séparation des fonctions, de réutilisation. De plus, c'était un patchwork de parties redondantes, difficilement lisible.
Il me semble que j'ai apporté plus de robusteste de ce côté. J'ai essayé d'avoir toujours une approche respectueuse de deux principes :
- respect strict du principe MVC (Modèle — Vue — Contrôleur) afin de bien circonscrire les tâches de chaque partie du code et assurer la séparation des préoccupations ;
- réutilisabilité et mutualisation du code afin de se répéter le moins possible.
La création de classes utilitaires en CSS a permis la mutualisation des styles. Une approche par composants a amélioré grandement la modularité de l'interface. Et surtout, l'utilisation de la programmation orientée objet pour la conception de ces composants a rendu le code beaucoup plus lisible et compréhensible. J'ai conçu notamment une classe abstraite pour guider la fabrication de futurs modules TODD et à chaque fois que c'était possible des classes permettant une encapsulation des contraintes techniques pour expliciter les fonctionnalités plutôt que de les cacher derrière des lignes entières de code.
Qualité UI/UX
Le prototype définissait des styles en dur dans des bouts de code concaténés. Non seulement, cela était difficilement maintenable mais cela apportait un manque d'unité entre les modules et pour leur intégration dans le framqework lui-même. L'appel à des classes génériques crées pour toutes les situations dans chaque modules et l'utilisation systématiques des couleurs définies dans le framework MMI a grandement amélioré la tonalité et l'intégration de l'ensemble.
Que ce soit pour la création des modules ou pour l'interface du tableau de bord TODD, j'ai toujours pensé l'UX en première intention, avant le design et bien sûr avant le code. De fait, cette aspect du développement a été mis en premier et, même si parfois cela a été compliqué d'arriver à mes fins, l'expérience utilisateur est fluide et intuitive. L'utilisation du glisser-déposer pour la constitution des listes ou la configuration de la mosaïque dans le module éponyme, l'utilisation d'onglets, la prévisualisation lors de la cŕeation et l'édition d'un contenu ont apporté une simplicité dans les gestes utilisateurs pour prendre en main la composition et la gestion des contenus.
De plus, j'ai systématiquement utilisé les mêmes standards que dans les parties natives du framework MMI :
- composants de la librairie Limitless pour les sélecteurs de dates (calendrier interactif) ou les sélecteur de couleurs notamment ;
- utilisation de la librairie JavaScript Datatables pour la constitution de tableau.
L'intégration au reste du framework MMI aura été ainsi améliorée.
Points d'amélioration
Migration
Mon process d'appropriation du projet s'est déroulé en plusieurs phases :
- j'ai tenté de créer un module en copiant les autres ;
- je me suis aperçu que l'existant ne respectait pas le pattern MVC donc j'ai créé des modules (ou refactorisation des modules existants) afin de les rendre coonformes ;
- me rendant compte que je me répétais dans certaines tâches, j'ai créé une classe abstraite pour guider la formulation d'un nouverau module en créant par ailleurs une fluent interface permattant de concevoir simplement les interfaces d'administration des modules.
Cette dernière phase m'a permis d'obtenir un cadre de travail propre et robuste pour la création de module. Cependant, je n'ai pas voulu (à tord je pense rétrospectivement) repartir de zéro donc les modules dans plusieurs versions ont dû coexister dans l'extension TODD ce qui a complexifié le bon fonctionnement de l'ensemble.
Si j'avais disposé de plus de temps, j'aurais refactoriser tous les modules en les pensant comme des extensions de la classe abstraite ToddModule.
Code mort
Lors du développement des modules et de fonctionnalités supérieures coordonnant l'ensemble, je suis passé par différentes directions et approches. On imagine quelquefois une méthode ou un paramètre qui nous semble indispensable et/ou qu'on a véritablement utilisé et qui n'est plus approprié dans la nouvelle version. Par oubli ou ignorance que ce code existe, nous le laissons la où il est alors qu'il aurait fallu le retirer. Sa présence peut perturber de futur développeurs qui n'ont pas assister à toute les phases d'élaboration du projet et qui pourront se demander par la suite ce que ce code fait là, pourquoi une méthode n'est jamais appelée, etc.
Dette technique
Parfois, on a tendance à patcher une méthode pour lui résoudre un bug. La solution employée n'est pas adéquate mais ça fait le job comme on dit. On a beau savoir que ce n'est pas satisfaisant, on ne laisse pour plus tard car on est pressé d'avancer dans le projet. Et puis on oubli ce patch... Si j'avais eu plus de temps, j'aurais dû corriger de manière plus rigoureuse et robuste ces patches.
Tests
Je n'ai jamais fonctionné avec des tests durant ce projet. J'ai interrogé l'équipe des développeurs et le gérant à ce sujet et je n'ai jamais obtenu de réponse vraiment claires sur le sujet. Je n'ai pas vu ni expérimenté de techniques de test en L2 mais je sais qu'il en existe. Je sais aussi qu'il existe même des techniques de développement dites de test-driven development où les tests sont écrits avant le code proprement dit, comme autant de fonctionnalités à implémenter. Sans en arriver à de telles extrémités qui je pense ne sont pas appropriées à tous les projets, il aurait été bon de concevoir pour le projet TODD une batterie de tests de bases que doivent passer chaque module afin de proposer un code sans bug grossier.
Bilan personnel
Prise en main IA
Lorsque je suis arrivé dans l'entreprise et, une fois qu'on m'a présenté le projet et que je me suis retrouvé devant mon poste de travail, Fabrice le gérant m'a dit : " le code, on s'en moque ! " J'avoue que j'ai été un peu déstabilisé dans un premier temps. J'ai fini par comprendre ce qu'il m'avait lancé là comme une boutade.
J'ai débuté mon stage le 1er mars. Fabrice utilisait l'IA pour son travail depuis début janvier, et graduellement toute l'équipe s'y est mise. L'entreprise utilise les solutions de Google : elle a des abonnements pour Gemini et utilise l'éditeur Antigravity, intégrant des agents utilisables avec Gemini. Les développeurs utilisent ces agents au quotidien et finalement, saisissent plus de prompts qu'ils ne codent véritablement.
J'ai donc dû m'adapter à ce mode de travail. Tout au long de mon stage, j'ai comme le reste de l'équipe appris à dompter la bête. D'un pauvre chat au début, j'ai compris l'utilité du Specs-Driven Development permettant de rédiger des fichiers de spécifications relatives au projet, afin de contraindre le modèle utilisé à proposer des solutions confromes aux librairies utilisées et aux diverses syntaxes et normes de développement qu'utilise l'équipe pour tous les projets qu'elle développe.
J'ai terminé ma période de stage avec un système multi-agent avec cinq rôles bien référencés : un développeur, un contrôleur de qualité du code, un certificateur MVC, un agent responsable du style, un testeur et un chef d'orchestre au quel on adresse tous les prompts. Chaque rôle a une mission décrite très précisément et des contraintes liées au projet, aux librairies et frameworks utilisés et répertoriées rigoureusement. Ainsi, on arrive à focaliser le travail de l'IA sur une tâches bien précises, en la cadrant un maximum et en l'obligeant à adopter certaines solutions plutôt que d'autres pour le développement du projet. J'ai décrit le fonctionnement d'un système multi-agent dans une chronique sur mon blog ainsi que les fichiers décrivant les spécifications de ces agents.
Cependant, l'utilisation de l'IA n'est pas du tout efficace sans :
- une expertise globale sur le code produit et à produire : on ne peut pas élaborer de prompt efficace sans expliquer techniquement à l'agent ce qu'il doit faire. Si on reste trop vague, le résultat est très souvent, si ce n'est systématiquement, décevant, voire incorrect ou inapproprié.
- une expertise spécifique sur le projet : celui-ci reposant sur un framework maison complexe, constitué de centaines de fichiers, des milliers de lignes de codes, avec des fonctionnements typiques et originaux, même en étant rigoureux sur les prompts, le résultat attendu n'insère pas exactement dans le projet, voire présente des défectuosités. Sans l'aide des développeurs de l'équipe qui connaisse leur framework par cœur, je n'aurais pas pu résoudre certains bugs, même avec toute l'artillerie IA dont je disposais. Il suffit pqrfois d'être directif à l'agent pour qu'il corrige le problème mais encore faut-il savoir comment le contraindre à certaines spécificités du framewor.
L'utilisation de l'IA s'avère donc impuissante dans le cadre d'un projet s'intégrant à un système existant complexe si on ne maîtrise pas ce système en profondeur.
Vie entreprise
Pour ma part, je découvrais pour la première fois le fonctionnement d'une entreprise privée de l'intérieur. J'ai pu ainsi mesure le fossée avec la théorie apprise tout au long du cursus universitaire et la réalité du quoitidien d'une petite structure (quatre développeurs à plein temps et une assistante de direction).
J'ai pu assister à plusieurs manières de fonctionner qui contrevenaient à des principes que j'avais étudié en licence cette année et l'année passée. En voici quelques unes :
- l'absence de cahier des charges rigoureux dans certains projets. Des visios hebdomadaires avec un cliant qui semble construire le projet au fur à mesure des appels, indiquant de nouvelles fonctionnalités, oubliant l'existence de certaines qu'il avait demandé auparavant. Et les problématiques qui en découlent : difficultés à facturer le travail effectué, mauvaise traçabilité des échanges, projet qui s'éternise...
- la multiplication des versions de développement du framework maison MMI, versions qui doivent coexister plutôt que d'être mises à jour chez tous les clients. L'équipe fait avancer le développement de ce framework (ce qui est une bonne chose car elle le modernise et le rend plus souple)
- la course aux nouvelles fonctionnalités proposées aux client, au détriment quelquefois de la bonne tenue du code et de la cohérence d'un projet. J'ai pu constaté un peu de dispersion il m'a semblé chez Fabrice le gérant qui avait beaucoup d'idées à proposer et souvent, l'équipe avait du mal à suivre et aurait préférer finaliser une étape plutôt que d'en démarre d'autres.
J'ai aussi constaté les difficultés à intégrer les demandes des clients utilisant des progiciels, dont le bon fonctionnement d'une chaîne de production est lié à celui du logiciel. Lorsqu'il y a un bug, la chaîne s'arrête, tout comme l'équipe dans son travail qui doit régler le bug et s'interrompre dans ce qu'elle était en train de faire.
Une des multiples leçons que j'ai retiré de tout cela, c'est qu'il faut être bien rigoureux dans sa manière de travailler (gestions des projets, des dépôts, des versions, traçabilité des échanges avec les clients, travail sur le cahier des charges précis et détaillé à effectuer conjointement avec le client en amont d'un projet, finalisation des étapes de productions, etc.) au moins autant qu'on doit l'être dans l'écriture du code pour les applications qu'on développe.