Instruction:
# Projet "Accueil / Hall" – Contexte pour Codex

Ce projet affiche une page d'accueil pour le hall d'immeuble avec :
- Horloge / date
- Horaires RATP (bus 310 + RER A)
- Bloc météo + nowcast + radar
- Bloc annonces / alertes (JSON)
- Une admin classique + une hyper admin de configuration

L'objectif est de **faire évoluer le code sans casser l'existant**, ni la disposition visuelle globale.

---

## 1. Techno & environnement

- Serveur local : WAMP64 sous Windows.
- Serveur cible : Apache/PHP (mutu OVH).
- Langage principal : **PHP 8+** (procédural + quelques helpers).
- Front : HTML / CSS / JavaScript vanilla (pas de framework JS lourd).
- Données : fichiers JSON dans `/data` (pas de base de données).

Ne pas introduire de framework type Laravel / Symfony / React / Vue sans demande explicite.

---

## 2. Architecture globale

### 2.1 Front principal

- `index.php`
  Page principale affichée sur l'écran du hall.
  Contient 4 zones logiques (même si ce n’est pas forcément nommé comme ça dans le code) :

  - **Z1 – Horloge / date**
    - Affiche l’heure en temps réel (Intl, timezone `Europe/Paris`).
    - Format 24h/12h piloté par configuration (Hyper Admin).

  - **Z2 – Transports**
    - **Bus 310 – La Haute Maison** : horaires RATP/IDFM via `ratp_proxy.php`.
    - **RER A – Noisy-Champs** (deux sens).
    - **RER A – Noisy-le-Grand Mont d’Est** (deux sens).
    - Nombre d’horaires affichés paramétrable via config (Hyper Admin).

  - **Z3 – Météo**
    - Données Open-Meteo (prévisions horaires).
    - Nowcast / radar (ex. Windy) intégrés dans un bloc météo.
    - Nombre de “pills” / heures affichées paramétrable via config.

  - **Z4 – Annonces / alertes**
    - Affichées à partir de `data/annonces.json`.
    - Mise en page via `data/ui.json` (template : `grid-2x2`, `grid-1plus2`, `grid-full`, etc.).
    - Thèmes de couleur via `annonces_theme.php` / config d’annonces.

- `front.js`
  JavaScript front principal, avec notamment :
  - Gestion de l’heure / date (Intl, timezone, locale).
  - Récupération et mise à jour des blocs RATP, météo, annonces.
  - Hot reload / rafraîchissement via endpoints d’admin (`admin/data_status.php`, `admin/annonces_widget.php?mode=fragment`, etc.).
  - Utilisation de la config Hyper Admin via `window.hyper` et `getConfig()`.

- `assets/`
  - CSS global (thèmes, layout, couleurs).
  - JS global (front, utilitaires).

### 2.2 Administration classique

- `annonces_admin.php`
  Interface d’administration des annonces :
  - Création / modification / suppression d’annonces.
  - Choix de la couleur (thème) via `annonces_theme.php` + chips couleurs.
  - Pinned / active, date de visibilité, etc.
  - Persistance dans `data/annonces.json`.

- `ui_admin.php`
  Interface d’administration de la mise en page des annonces :
  - Édite `data/ui.json`.
  - Gère :
    - `template` (ex : `grid-2x2`, `grid-1plus2`, `grid-full`, etc.),
    - `gap` (espacement),
    - `order` (ordre des annonces),
    - `takeover_id` (annonce plein écran),
    - `rowfull_id` (annonce full width sur une ligne),
    - `annonces_defaults` (ex : `maxlines`, `speed`, `pause`, etc.).
  - Ne touche pas au contenu des annonces ni aux thèmes de couleur.

- `config_annonces.php`
  Fichier central de config pour les annonces :
  - Définit les chemins (`ANN_PATH` vers `data/annonces.json`, etc.).
  - Configure des options globales (ex : tri, filtres par défaut).
  - Doit rester compatible avec les anciens JSON.

- `annonces_theme.php`
  Définit les thèmes de couleur pour les annonces :
  - `$ANN_THEME = ['neutral' => [...], 'info' => [...], 'warn' => [...], 'emph' => [...], ...]`
  - Utilisé à la fois en front et en admin.
  - Ne pas casser les clés existantes (risque de casser les couleurs des annonces existantes).

- `admin.js`
  JavaScript générique pour les pages d’admin :
  - `initializeToggles()` : switches actifs/inactifs, pinned, etc.
  - `initializeColorChips()` : gestion des thèmes couleurs.
  - `initializeInlineToggles()` : bascule AJAX des statuts.
  - `initializeDateHandling()` : gestion des champs date (format, min/max, etc.).
  - Gestion du changement de template UI, etc.

### 2.3 Hyper Admin / Configuration avancée

- Hyper Admin (fichier PHP dédié, ex: `hyperadmin.php` ou équivalent) :
  - Permet de modifier une **config globale** (HYPER_CONFIG) avec surcharge des valeurs par défaut :
    - Formats d’heure / date.
    - Nombre d’horaires RER / bus affichés.
    - Nombre de “pills” météo.
    - Intervalles de rafraîchissement (RATP, météo, hot reload, etc.).
  - Utilise un JSON de configuration (ex : `data/hyper_config.json` ou similaire).

- Sur le front, `front.js` utilise :
  - `getConfig(path, defaultValue)` pour aller chercher les valeurs dans `window.hyper`.
  - `isEnabled(...)` pour les flags ON/OFF.
  - Toujours **préserver les valeurs par défaut** si la config Hyper Admin est absente ou incomplète.

### 2.4 Proxy et données externes

- `ratp_proxy.php`
  Proxy pour les API RATP / IDFM (PRIM) :
  - Gère plusieurs endpoints (stop-monitoring, general-message, etc.).
  - Utilise une clé PRIM_API_KEY.
  - Retourne du JSON simplifié pour le front.
  - Doit gérer les timeouts, erreurs réseau, et potentiellement un cache.

- Bloc météo :
  - Récupère les données via Open-Meteo (prévisions horaires).
  - Utilise latitude, longitude, et un nom de ville configurés.
  - Doit rester robuste face aux erreurs réseau ou à des réponses partielles.

---

## 3. Données & fichiers JSON

- `data/annonces.json`
  - Contient la liste des annonces (titre, contenu, dates, pinned, active, couleur, etc.).
  - Format existant **à respecter** (ne pas changer la structure sans migration explicite).

- `data/ui.json`
  - Contient la configuration de mise en page des annonces :
    - `annonces_layout.template`
    - `annonces_layout.gap`
    - `annonces_layout.order`
    - `annonces_layout.takeover_id`
    - `annonces_layout.rowfull_id`
    - `annonces_defaults.maxlines`, `speed`, `pause`, etc.
  - Utilisé à la fois par le front et par `ui_admin.php`.

- `data/...` (autres JSON)
  - Config de thème, hyper config, etc.
  - Toujours préserver la compatibilité avec les fichiers déjà générés.

---

## 4. Contraintes fortes (à respecter absolument)

### 4.1 Ne pas casser la baseline

- **Ne pas modifier la structure globale** de la page hall :
  - Toujours conserver les 4 zones principales (Z1/Z2/Z3/Z4).
  - Ne pas changer radicalement la disposition CSS.
- Ne pas supprimer les mécanismes existants :
  - Horloge/date (Intl, timezone).
  - Affichage RATP / RER.
  - Affichage météo.
  - Affichage des annonces.

### 4.2 Fichiers et noms

- Ne pas renommer :
  - `index.php`,
  - `ratp_proxy.php`,
  - `annonces_admin.php`,
  - `ui_admin.php`,
  - `config_annonces.php`,
  - `annonces_theme.php`,
  - `helpers.php` (s’il existe),
  - `admin.js`, `front.js`,
  - ni les fichiers JSON dans `/data`, sauf demande explicite.

- Ne pas déplacer ces fichiers dans une autre arborescence sans préserver les chemins existants.

### 4.3 Style et compatibilité

- Code PHP :
  - Procédural, clair, avec des fonctions utilitaires.
  - Commentaires en **français**.
  - Pas de dépendance à un framework PHP externe.

- JavaScript :
  - Vanilla JS.
  - Garder le code compatible avec les navigateurs ciblés par le projet.
  - Ne pas introduire de bundler lourd (Webpack, Vite…) sans demande.

- CSS :
  - Conserver le thème existant (couleurs, panels, badges “maj”, etc.).
  - Ne pas casser l’apparence ni la disposition globale.

---

## 5. Attentes sur les modifications

### 5.1 Ce que tu peux faire

- Refactoriser pour :
  - améliorer la lisibilité,
  - factoriser du code dupliqué,
  - sécuriser (vérifs sur les entrées, robustesse face aux erreurs API),
  - optimiser légèrement les performances.

- Ajouter de nouvelles fonctionnalités :
  - **en suivant les patterns existants** (helpers, JSON de config, etc.).
  - en exposant les nouveaux paramètres dans Hyper Admin (si logique).

- Améliorer l’admin :
  - ergonomie (sans enlever de fonctionnalités),
  - CSS responsive pour l’admin,
  - validation côté serveur et côté front.

### 5.2 Ce que tu ne dois pas faire

- Ne pas :
  - supprimer des champs dans `annonces.json` ou `ui.json` sans migration.
  - changer la structure JSON sans une gestion explicite des anciens fichiers.
  - introduire une base de données à la place des JSON.
  - ajouter des `var_dump`, `die`, `echo` de debug restants en production.
  - laisser des `console.log` bruyants côté front (sauf logs utiles et maîtrisés).

---

## 6. Debug & outils spécifiques

- Le projet utilise des options de debug front (par ex. `?debug=1` / `?debug=2`) :
  - Conserver ces options.
  - Si tu ajoutes des niveaux de debug, bien les documenter.

- Le hot reload doit continuer à fonctionner :
  - Fonctions JS type `hotReload()` dans `front.js`.
  - Respecter les intervalles de polling configurables (en secondes, clampés en JS).

---

## 7. Résumé pour Codex

Quand tu proposes des changements :

1. **Ne change pas les noms de fichiers ni la structure globale.**
2. **Préserve le comportement actuel** par défaut (même si tu ajoutes des options).
3. **Respecte la config Hyper Admin** : toujours prévoir des valeurs par défaut si la config est absente.
4. **Explique les changements** dans les commentaires quand tu touches à des parties sensibles (annonces, RATP, météo, hot reload).
5. Évite les réécritures massives : favorise les **évolutions incrémentales** et les refactors ciblés.


# AGENTS.md instructions for C:\wamp64\www\cssp2AI

<INSTRUCTIONS>
# Projet "Accueil / Hall" – Contexte pour Codex

Ce projet affiche une page d'accueil pour le hall d'immeuble avec :
- Horloge / date
- Horaires RATP (bus 310 + RER A)
- Bloc météo + nowcast + radar
- Bloc annonces / alertes (JSON)
- Une admin classique + une hyper admin de configuration

L'objectif est de **faire évoluer le code sans casser l'existant**, ni la disposition visuelle globale.

---

## 1. Techno & environnement

- Serveur local : WAMP64 sous Windows.
- Serveur cible : Apache/PHP (mutu OVH).
- Langage principal : **PHP 8+** (procédural + quelques helpers).
- Front : HTML / CSS / JavaScript vanilla (pas de framework JS lourd).
- Données : fichiers JSON dans `/data` (pas de base de données).

Ne pas introduire de framework type Laravel / Symfony / React / Vue sans demande explicite.

---

## 2. Architecture globale

### 2.1 Front principal

- `index.php`
  Page principale affichée sur l'écran du hall.
  Contient 4 zones logiques (même si ce n’est pas forcément nommé comme ça dans le code) :

  - **Z1 – Horloge / date**
    - Affiche l’heure en temps réel (Intl, timezone `Europe/Paris`).
    - Format 24h/12h piloté par configuration (Hyper Admin).

  - **Z2 – Transports**
    - **Bus 310 – La Haute Maison** : horaires RATP/IDFM via `ratp_proxy.php`.
    - **RER A – Noisy-Champs** (deux sens).
    - **RER A – Noisy-le-Grand Mont d’Est** (deux sens).
    - Nombre d’horaires affichés paramétrable via config (Hyper Admin).

  - **Z3 – Météo**
    - Données Open-Meteo (prévisions horaires).
    - Nowcast / radar (ex. Windy) intégrés dans un bloc météo.
    - Nombre de “pills” / heures affichées paramétrable via config.

  - **Z4 – Annonces / alertes**
    - Affichées à partir de `data/annonces.json`.
    - Mise en page via `data/ui.json` (template : `grid-2x2`, `grid-1plus2`, `grid-full`, etc.).
    - Thèmes de couleur via `annonces_theme.php` / config d’annonces.

- `front.js`
  JavaScript front principal, avec notamment :
  - Gestion de l’heure / date (Intl, timezone, locale).
  - Récupération et mise à jour des blocs RATP, météo, annonces.
  - Hot reload / rafraîchissement via endpoints d’admin (`admin/data_status.php`, `admin/annonces_widget.php?mode=fragment`, etc.).
  - Utilisation de la config Hyper Admin via `window.hyper` et `getConfig()`.

- `assets/`
  - CSS global (thèmes, layout, couleurs).
  - JS global (front, utilitaires).

### 2.2 Administration classique

- `annonces_admin.php`
  Interface d’administration des annonces :
  - Création / modification / suppression d’annonces.
  - Choix de la couleur (thème) via `annonces_theme.php` + chips couleurs.
  - Pinned / active, date de visibilité, etc.
  - Persistance dans `data/annonces.json`.

- `ui_admin.php`
  Interface d’administration de la mise en page des annonces :
  - Édite `data/ui.json`.
  - Gère :
    - `template` (ex : `grid-2x2`, `grid-1plus2`, `grid-full`, etc.),
    - `gap` (espacement),
    - `order` (ordre des annonces),
    - `takeover_id` (annonce plein écran),
    - `rowfull_id` (annonce full width sur une ligne),
    - `annonces_defaults` (ex : `maxlines`, `speed`, `pause`, etc.).
  - Ne touche pas au contenu des annonces ni aux thèmes de couleur.

- `config_annonces.php`
  Fichier central de config pour les annonces :
  - Définit les chemins (`ANN_PATH` vers `data/annonces.json`, etc.).
  - Configure des options globales (ex : tri, filtres par défaut).
  - Doit rester compatible avec les anciens JSON.

- `annonces_theme.php`
  Définit les thèmes de couleur pour les annonces :
  - `$ANN_THEME = ['neutral' => [...], 'info' => [...], 'warn' => [...], 'emph' => [...], ...]`
  - Utilisé à la fois en front et en admin.
  - Ne pas casser les clés existantes (risque de casser les couleurs des annonces existantes).

- `admin.js`
  JavaScript générique pour les pages d’admin :
  - `initializeToggles()` : switches actifs/inactifs, pinned, etc.
  - `initializeColorChips()` : gestion des thèmes couleurs.
  - `initializeInlineToggles()` : bascule AJAX des statuts.
  - `initializeDateHandling()` : gestion des champs date (format, min/max, etc.).
  - Gestion du changement de template UI, etc.

### 2.3 Hyper Admin / Configuration avancée

- Hyper Admin (fichier PHP dédié, ex: `hyperadmin.php` ou équivalent) :
  - Permet de modifier une **config globale** (HYPER_CONFIG) avec surcharge des valeurs par défaut :
    - Formats d’heure / date.
    - Nombre d’horaires RER / bus affichés.
    - Nombre de “pills” météo.
    - Intervalles de rafraîchissement (RATP, météo, hot reload, etc.).
  - Utilise un JSON de configuration (ex : `data/hyper_config.json` ou similaire).

- Sur le front, `front.js` utilise :
  - `getConfig(path, defaultValue)` pour aller chercher les valeurs dans `window.hyper`.
  - `isEnabled(...)` pour les flags ON/OFF.
  - Toujours **préserver les valeurs par défaut** si la config Hyper Admin est absente ou incomplète.

### 2.4 Proxy et données externes

- `ratp_proxy.php`
  Proxy pour les API RATP / IDFM (PRIM) :
  - Gère plusieurs endpoints (stop-monitoring, general-message, etc.).
  - Utilise une clé PRIM_API_KEY.
  - Retourne du JSON simplifié pour le front.
  - Doit gérer les timeouts, erreurs réseau, et potentiellement un cache.

- Bloc météo :
  - Récupère les données via Open-Meteo (prévisions horaires).
  - Utilise latitude, longitude, et un nom de ville configurés.
  - Doit rester robuste face aux erreurs réseau ou à des réponses partielles.

---

## 3. Données & fichiers JSON

- `data/annonces.json`
  - Contient la liste des annonces (titre, contenu, dates, pinned, active, couleur, etc.).
  - Format existant **à respecter** (ne pas changer la structure sans migration explicite).

- `data/ui.json`
  - Contient la configuration de mise en page des annonces :
    - `annonces_layout.template`
    - `annonces_layout.gap`
    - `annonces_layout.order`
    - `annonces_layout.takeover_id`
    - `annonces_layout.rowfull_id`
    - `annonces_defaults.maxlines`, `speed`, `pause`, etc.
  - Utilisé à la fois par le front et par `ui_admin.php`.

- `data/...` (autres JSON)
  - Config de thème, hyper config, etc.
  - Toujours préserver la compatibilité avec les fichiers déjà générés.

---

## 4. Contraintes fortes (à respecter absolument)

### 4.1 Ne pas casser la baseline

- **Ne pas modifier la structure globale** de la page hall :
  - Toujours conserver les 4 zones principales (Z1/Z2/Z3/Z4).
  - Ne pas changer radicalement la disposition CSS.
- Ne pas supprimer les mécanismes existants :
  - Horloge/date (Intl, timezone).
  - Affichage RATP / RER.
  - Affichage météo.
  - Affichage des annonces.

### 4.2 Fichiers et noms

- Ne pas renommer :
  - `index.php`,
  - `ratp_proxy.php`,
  - `annonces_admin.php`,
  - `ui_admin.php`,
  - `config_annonces.php`,
  - `annonces_theme.php`,
  - `helpers.php` (s’il existe),
  - `admin.js`, `front.js`,
  - ni les fichiers JSON dans `/data`, sauf demande explicite.

- Ne pas déplacer ces fichiers dans une autre arborescence sans préserver les chemins existants.

### 4.3 Style et compatibilité

- Code PHP :
  - Procédural, clair, avec des fonctions utilitaires.
  - Commentaires en **français**.
  - Pas de dépendance à un framework PHP externe.

- JavaScript :
  - Vanilla JS.
  - Garder le code compatible avec les navigateurs ciblés par le projet.
  - Ne pas introduire de bundler lourd (Webpack, Vite…) sans demande.

- CSS :
  - Conserver le thème existant (couleurs, panels, badges “maj”, etc.).
  - Ne pas casser l’apparence ni la disposition globale.

---

## 5. Attentes sur les modifications

### 5.1 Ce que tu peux faire

- Refactoriser pour :
  - améliorer la lisibilité,
  - factoriser du code dupliqué,
  - sécuriser (vérifs sur les entrées, robustesse face aux erreurs API),
  - optimiser légèrement les performances.

- Ajouter de nouvelles fonctionnalités :
  - **en suivant les patterns existants** (helpers, JSON de config, etc.).
  - en exposant les nouveaux paramètres dans Hyper Admin (si logique).

- Améliorer l’admin :
  - ergonomie (sans enlever de fonctionnalités),
  - CSS responsive pour l’admin,
  - validation côté serveur et côté front.

### 5.2 Ce que tu ne dois pas faire

- Ne pas :
  - supprimer des champs dans `annonces.json` ou `ui.json` sans migration.
  - changer la structure JSON sans une gestion explicite des anciens fichiers.
  - introduire une base de données à la place des JSON.
  - ajouter des `var_dump`, `die`, `echo` de debug restants en production.
  - laisser des `console.log` bruyants côté front (sauf logs utiles et maîtrisés).

---

## 6. Debug & outils spécifiques

- Le projet utilise des options de debug front (par ex. `?debug=1` / `?debug=2`) :
  - Conserver ces options.
  - Si tu ajoutes des niveaux de debug, bien les documenter.

- Le hot reload doit continuer à fonctionner :
  - Fonctions JS type `hotReload()` dans `front.js`.
  - Respecter les intervalles de polling configurables (en secondes, clampés en JS).

---

## 7. Résumé pour Codex

Quand tu proposes des changements :

1. **Ne change pas les noms de fichiers ni la structure globale.**
2. **Préserve le comportement actuel** par défaut (même si tu ajoutes des options).
3. **Respecte la config Hyper Admin** : toujours prévoir des valeurs par défaut si la config est absente.
4. **Explique les changements** dans les commentaires quand tu touches à des parties sensibles (annonces, RATP, météo, hot reload).
5. Évite les réécritures massives : favorise les **évolutions incrémentales** et les refactors ciblés.

</INSTRUCTIONS>


<environment_context>
  <cwd>C:\wamp64\www\cssp2AI</cwd>
  <approval_policy>on-request</approval_policy>
  <sandbox_mode>read-only</sandbox_mode>
  <network_access>restricted</network_access>
  <shell>powershell</shell>
</environment_context>


resume 019ab510-5109-75e2-8e09-e2962bc418f3


Summary: auto


**Searching for resume by ID**


Get-ChildItem -Recurse | Select-String -Pattern "019ab510-5109-75e2-8e09-e2962bc418f3"


Output:
Exit code: 0
Wall time: 1.7 seconds
Output:


**Handling missing resume context**


I couldn’t find any notes or files matching `019ab510-5109-75e2-8e09-e2962bc418f3` in the workspace. Can you share what you’d like me to resume or point me to the relevant file/feature?


je suis qui ?


**Responding to identity query**


Je n’ai pas d’infos sur ton identité dans ce contexte. Dis‑moi ce dont tu as besoin et j’aiderai.


ok, analyses le code des fichiers


**Listing repo files**


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:

    R‚pertoireÿ: C:\wamp64\www\cssp2AI

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        21/11/2025     15:31                admin
d-----        21/11/2025     14:18                assets
d-----        21/11/2025     15:36                cache
d-----        24/11/2025     10:53                out
d-----        21/11/2025     14:18                outils
-a----        21/11/2025     15:02           9983 AGENTS.md
-a----        24/11/2025     10:52           5122 hyperadmin_integration.php
-a----        24/11/2025     10:50          18088 index.php
-a----        24/11/2025     11:30          11482 inline_style_map.json
-a----        21/11/2025     15:36         529270 ratp_debug.log
-a----        21/11/2025     09:13        2097238 ratp_debug.log.1
-a----        21/11/2025     09:03          10482 ratp_proxy.php
-a----        24/11/2025     11:24          12258 style_map.json


**Considering approach to code analysis**


Inspect root PHP entrypoints and config files to understand front rendering and data flow

Review assets (JS/CSS) for front behavior and helpers

Check admin/hyperadmin related files for config handling and constraints


Output: Plan updated


**Opening index.php to read**


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:
<?php
/**
 * FRONT-END PRINCIPAL - VERSION FINALE CORRIGÇ%E
 * IntÇ¸gration complÇùte des thÇùmes avec toutes les variables CSS
 *
 * @version 2.0 - ThÇùmes entiÇùrement fonctionnels
 */

// Chargement des configurations
require_once __DIR__ . '/admin/config_annonces.php';
require_once __DIR__ . '/admin/themes_config_unified.php';
// NOUVEAU - Ajouter Hyper Admin
require_once __DIR__ . '/hyperadmin_integration.php';

// Support pour la prÇ¸visualisation de thÇùme
$previewTheme = null;
if (isset($_GET['preview_theme']) && isset($THEMES_UNIFIED[$_GET['preview_theme']])) {
    $previewTheme = $_GET['preview_theme'];
    $themeColors = $THEMES_UNIFIED[$previewTheme]['panel_colors'];
} else {
    $themeColors = getActivePanelColors();
}

// Variables de debug
$currentTheme = $previewTheme ?? getActiveTheme();
$variableCount = count($themeColors);
?>
<!DOCTYPE html>

  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
  <meta name="robots" content="noindex, nofollow" />
  <title>Hall ƒ?" Infos RÇ¸sidence</title>
  <link rel="icon" type="image/svg+xml" href="assets/img/favicon_front.svg">

  <link rel="stylesheet" href="assets/css/theme-vars.php?ts=<?= time() ?>&preview_theme=<?= htmlspecialchars($previewTheme ?? '') ?>">

  <link rel="stylesheet" href="assets/css/front.css">

  <link rel="stylesheet" href="assets/css/theme.css">

  <?php
  if (function_exists('hyper_render_config_script')) {
      hyper_render_config_script();
  }
  ?>

<body data-theme="<?= htmlspecialchars($currentTheme) ?>">

ƒ?"

ƒ?"

          --:--
          --

                ÐYsO
                310

La Haute Maison

              Mise Çÿ jour...

-- min

-- min

                RER
                A

Noisy-Champs

              maj 13:35:56

Vers Paris / St-Germain

                    Mission
                    Destination
                    Prochain passage

                    ZUPE47
                    Saint-Germain-en-Laye
                    13:41

                    ZUPE89
                    Saint-Germain-en-Laye
                    13:49

                    TROC89
                    Poissy
                    13:53

Vers Marne-la-VallÇ¸e

                    Mission
                    Destination
                    Prochain passage

                    OKLA75
                    Torcy
                    13:37

                    QIWI78
                    Marne-la-VallÇ¸e - Chessy
                    13:41

      A RER

Noisy-le-Grand ƒ?" Mont dƒ?TEst

              maj 13:35:56

Vers Paris / St-Germain

Mission	Destination	Prochain passage

Chargementƒ?Ý

Vers Marne-la-VallÇ¸e

Mission	Destination	Prochain passage

Chargementƒ?Ý

MÇ¸tÇ¸o

                  ÐY"?
                  Noisy-le-Grand

                14¶øC
                Max: 15¶ø ¶ú Min: 9¶ø

HumiditÇ¸

73%

Vent

15 km/h

UV

2

Pas de pluie imminente

                Aujourd'hui
                ¶øC

                <svg viewBox="0 0 300 140">
                  <g class="spark-grid">
                    <line x1="0" y1="35" x2="300" y2="35"/>
                    <line x1="0" y1="70" x2="300" y2="70"/>
                    <line x1="0" y1="105" x2="300" y2="105"/>
                  </g>
                  <g class="spark-grid-vert">
                    <line x1="60" y1="0" x2="60" y2="140"/>
                    <line x1="120" y1="0" x2="120" y2="140"/>
                    <line x1="180" y1="0" x2="180" y2="140"/>
                    <line x1="240" y1="0" x2="240" y2="140"/>
                  </g>
                  <path class="spark-path" d="M0,80 Q75,60 150,70 T300,65"/>
                  <g class="spark-dots">
                    <circle cx="0" cy="80"/>
                    <circle cx="75" cy="60"/>
                    <circle cx="150" cy="70"/>
                    <circle cx="225" cy="65"/>
                    <circle cx="300" cy="65"/>
                  </g>
                  <g class="spark-axis">
                    <text x="0" y="130">6h</text>
                    <text x="75" y="130">12h</text>
                    <text x="150" y="130">18h</text>
                    <text x="225" y="130">24h</text>
                  </g>
                </svg>

Infos

        <?php
        // Inclusion du widget d'annonces avec gestion d'erreur
        if (file_exists(__DIR__ . '/admin/annonces_widget.php')) {
            require_once __DIR__ . '/admin/annonces_widget.php';

            if (function_exists('ann__render_front')) {
                ann__render_front($ANN_PATH, 6, $ANN_THEME);
            } else {
                // Affichage de dÇ¸monstration si le widget n'est pas disponible
                echo '
';
                echo '
';
                echo '
SystÇùme d\'annonces
';
                echo '
Le module d\'annonces est en cours de configuration.
';
                echo '
';
                echo '
';
                echo '
ThÇùme appliquÇ¸
';
                echo '
ThÇùme actuel : ' . htmlspecialchars($THEMES_UNIFIED[$currentTheme]['name']) . '
';
                echo '
';
                echo '
';
            }
        } else {
            // Demo des diffÇ¸rents styles d'annonces
            echo '
';
            foreach ($ANN_THEME as $type => $colors) {
                echo '
';
                echo '
Annonce ' . ucfirst($type) . '
';
                echo '
Exemple d\'annonce avec le style "' . $type . '" appliquÇ¸ au thÇùme actuel.
';
                echo '
';
            }
            echo '
';
        }
        ?>


**Noting encoding issue and checking assets**


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:

    R‚pertoireÿ: C:\wamp64\www\cssp2AI\assets

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        21/11/2025     14:18                css
d-----        21/11/2025     14:18                img
d-----        21/11/2025     14:18                js


**Checking JavaScript front-end files**


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:

    R‚pertoireÿ: C:\wamp64\www\cssp2AI\assets\js

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        18/11/2025     09:46          23990 debug-easter.js
-a----        21/11/2025     12:23          50233 front.js


**Reading front.js for context**


Output:
Exit code: 0
Wall time: 0.8 seconds
Total output lines: 1313
Output:
/**
 * Front-end JavaScript - Version adaptÇ¸e pour Hyper Admin
 * PrÇ¸serve toutes les fonctionnalitÇ¸s existantes
 * Utilise la configuration HYPER_CONFIG quand disponible
 */

/* =======================
   HELPER HYPER ADMIN
   ======================= */
// Fonction pour obtenir une config avec valeur par dÇ¸faut
function getConfig(path, defaultValue) {
    return window.hyper?.get(path) ?? defaultValue;
}

// Fonction pour vÇ¸rifier si activÇ¸ (gÇùre les strings '0' et '1')
function isEnabled(value) {
    return value !== '0' && value !== 0 && value !== false && value !== 'false';
}

/* =======================
   Utils communs (une seule fois)
   ======================= */
const TZ = getConfig('datetime.timezone', 'Europe/Paris');
const LOCALE = getConfig('datetime.locale', 'fr-FR');
const fmt = (opts) => new Intl.DateTimeFormat(LOCALE, {timeZone: TZ, ...opts});
const fmtWeekday = new Intl.DateTimeFormat(LOCALE, {weekday: 'long', timeZone: TZ});
const fmtDate = new Intl.DateTimeFormat(LOCALE, {day: '2-digit', month: 'long', year: 'numeric', timeZone: TZ});
const pad2 = n => String(n).padStart(2, '0');
const toHM = iso => { const d = new Date(iso); return `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; };

function badgeUpdateHTML(d = new Date()) {
  const t = d.toLocaleTimeString(LOCALE, {hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: TZ});
  return `<span class="dot" aria-hidden="true"></span> maj ${t}`;
}

function fetchJSON(url, {timeout = 10000, ...opt} = {}) {
  const ctrl = new AbortController();
  const id = setTimeout(() => ctrl.abort('timeout'), timeout);
  return fetch(url, {...opt, signal: ctrl.signal, cache: 'no-store'})
    .then(r => r.json().catch(() => null))
    .finally(() => clearTimeout(id));
}

/* ===== Horloge ===== */
(function clock() {
  // VÇ¸rifier si l'horloge est activÇ¸e
  if (!isEnabled(getConfig('datetime.enabled', true))) {
    const elem = document.querySelector('.row-date-time');
    if (elem) elem.style.display = 'none';
    return;
  }

  const $weekday = document.getElementById('weekday');
  const $fullDate = document.getElementById('fullDate');
  const $hhmm = document.getElementById('hhmm');
  const $sec = document.getElementById('seconds');

  // Configuration depuis Hyper Admin
  const showSeconds = isEnabled(getConfig('datetime.show_seconds', true));
  const showWeekday = isEnabled(getConfig('datetime.show_weekday', true));
  const format24h = isEnabled(getConfig('datetime.time_format_24h', true));
  const refreshInterval = parseInt(getConfig('datetime.refresh_interval', 1000));

  // Masquer les Ç¸lÇ¸ments selon config
  if (!showSeconds && $sec) {
    $sec.style.display = 'none';
  }
  if (!showWeekday && $weekday) {
    $weekday.style.display = 'none';
  }

  function tick() {
    const now = new Date();

    // Date
    if (showWeekday && $weekday) {
      $weekday.textContent = fmtWeekday.format(now).replace(/^\w/, c => c.toUpperCase());
    }
    if ($fullDate) {
      $fullDate.textContent = fmtDate.format(now);
    }

    // Heure/min/sec en une fois => zÇ¸ros prÇ¸servÇ¸s (17:00, 09)
    const parts = new Intl.DateTimeFormat(LOCALE, {
      timeZone: TZ,
      hour: '2-digit',
      minute: '2-digit',
      second: '2-digit',
      hourCycle: format24h ? 'h23' : 'h12'
    }).formatToParts(now);

    const HH = parts.find(p => p.type === 'hour')?.value ?? pad2(now.getHours());
    const MM = parts.find(p => p.type === 'minute')?.value ?? pad2(now.getMinutes());
    const SS = parts.find(p => p.type === 'second')?.value ?? pad2(now.getSeconds());

    if ($hhmm) {
      $hhmm.textContent = `${HH}:${MM}`;
    }
    if (showSeconds && $sec) {
      $sec.textContent = SS;
    }
  }

  tick();
  let int = setInterval(tick, refreshInterval);
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) { clearInterval(int); }
    else { tick(); int = setInterval(tick, refreshInterval); }
  });
})();

/* =======================
   MÇ¸tÇ¸o (Open-Meteo)
   ======================= */
(function meteo() {
  // VÇ¸rifier si la mÇ¸tÇ¸o est activÇ¸e
  if (!isEnabled(getConfig('weather.enabled', true))) {
    const elem = document.querySelector('.meteo--compact');
    if (elem) elem.style.display = 'none';
    return;
  }

  // Configuration depuis Hyper Admin
  const LAT = parseFloat(getConfig('weather.location.latitude', 48.8326848));
  const LON = parseFloat(getConfig('weather.location.longitude', 2.535359));
  const PLACE = getConfig('weather.location.name', 'Noisy-le-Grand');
  const REFRESH_INTERVAL = parseInt(getConfig('weather.refresh_interval', 600000)); // 10 min par dÇ¸faut
  const REQUEST_TIMEOUT = parseInt(getConfig('weather.request_timeout', 10000));

  // Options d'affichage
  const showCurrentTemp = isEnabled(getConfig('weather.display.current_temp', true));
  const showMinMax = isEnabled(getConfig('weather.display.min_max', true));
  const showHumidity = isEnabled(getConfig('weather.display.humidity', true));
  const showWind = isEnabled(getConfig('weather.display.wind', true));
  const showPrecipitation = isEnabled(getConfig('weather.display.precipitation', true));

  // Graphique
  const graphEnabled = isEnabled(getConfig('weather.graph.enabled', true));
  const graphWidth = parseInt(getConfig('weather.graph.width', 700));
  const graphHeight = parseInt(getConfig('weather.graph.height', 120));
  const graphHoursToShow = parseInt(getConfig('weather.graph.hours_to_show', 10));

  // PrÇ¸visions hebdo
  const weeklyEnabled = isEnabled(getConfig('weather.weekly_forecast.enabled', true));
  const weeklyDaysToShow = parseInt(getConfig('weather.weekly_forecast.days_to_show', 5));

  // PrÇ¸visions horaires (pills)
  const hourlyEnabled = isEnabled(getConfig('weather.hourly_forecast.enabled', true));
  const hourlyHoursToShow = parseInt(getConfig('weather.hourly_forecast.hours_to_show', 8));

  const $meteo = document.getElementById('meteo-card');
  if (!$meteo) return;

  const round = (n, d = 0) => Number(n).toFixed(d);

  function iconFromCode(code) {
    if (code <= 1) return 'ƒ~?‹÷?';
    if (code <= 3) return 'ƒ>.';
    if (code <= 49) return 'ƒ~?‹÷?';
    if (code <= 69 || (code >= 80 && code <= 82)) return 'ÐYOõ‹÷?';
    if (code <= 79 || code >= 85) return 'ƒ?"‹÷?';
    if (code >= 95) return 'ƒ>^‹÷?';
    return 'ƒ?"';
  }

  function svgSpark(points, w = graphWidth, h = graphHeight, pad = 10) {
    if (!graphEnabled) return '';
    if (!points.length) return '';
    const xs = points.map((_, i) => i / (points.length - 1));
    const temps = points.map(p => Number(p.T));
    const rawMin = Math.min(...temps), rawMax = Math.max(...temps);
    let minT = Math.max(0, Math.floor(rawMin) - 2);
    let maxT = Math.min(35, Math.ceil(rawMax) + 2);
    const rng = Math.max(1, maxT - minT);
    const px = i => pad + xs[i] * (w - 2 * pad);
    const py = i => pad + (1 - (temps[i] - minT) / rng) * (h - 2 * pad);
    let d = `M ${px(0)} ${py(0)}`;
    for (let i = 1; i < points.length; i++) d += ` L ${px(i)} ${py(i)}`;
    const fill = `${d} L ${px(points.length - 1)} ${h - pad} L ${px(0)} ${h - pad} Z`;

    return `
      <svg viewBox="0 0 ${w} ${h}" style="width:100%; height:${h}px">
        ${points.map((p, i) => `
          <line x1="${px(i)}" y1="${pad}" x2="${px(i)}" y2="${h - pad}"
                stroke="var(--spark-grid-vert, #eef2f7)" stroke-width="1" opacity=".3"/>
        `).join('')}
        <path d="${fill}" fill="var(--spark-fill, rgba(2, 132, 199, .06))"/>
        <path d="${d}" fill="none" stroke="var(--spark-line, #0284c7)" stroke-width="2"/>
        ${points.map((p, i) => `
          <circle cx="${px(i)}" cy="${py(i)}" r="3" fill="var(--spark-dots, #0284c7)"/>
          <text x="${px(i)}" y="${h - 2}" text-anchor="middle" font-size="10"
                fill="var(--spark-text, #64748b)">${round(p.T, 0)}¶ø</text>
        `).join('')}
      </svg>`;
  }

  function buildUI(data = {}, minutely = {}) {
    const $root = $meteo;
    const now = new Date();

    /* DonnÇ¸es actuelles */
    const curT = data?.current?.temperature_2m;
    const curH = data?.current?.relative_humidity_2m;
    const curW = data?.current?.windspeed_10m;

    /* DonnÇ¸es horaires */
    const hourly = data?.hourly || {};
    const times = (hourly.time || []).map(t => new Date(t));
    const temps = hourly.temperature_2m || [];
    const precs = hourly.precipitation || [];
    const pops = hourly.precipitation_probability || [];
    const codes = hourly.weather_code || [];

    const hourlyData = times.map((t, i) => ({
      t, T: temps[i], mm: precs[i], pop: pops[i], code: codes[i]
    }));

    /* Prochaines heures (pour graph + cartes horaires) */
    const futureOnly   = hourlyData.filter(h => h.t > now);
    const hours        = futureOnly.slice(0, graphHoursToShow);
    const hourCards    = futureOnly.slice(0, hourlyHoursToShow);
    const hoursSmoothed = hours.map(h => ({ ...h, T: h.T }));

    /* DonnÇ¸es journaliÇùres */
    const daily = data?.daily || {};
    const dailyTimes = (daily.time || []).map(t => new Date(t));
    const tmax = daily.temperature_2m_max?.[0];
    const tmin = daily.temperature_2m_min?.[0];

    const dayNames = ['Dim', 'Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam'];
    const windArrow = deg => {
      const dirs = ['ƒÅ'', 'ƒÅ-', 'ƒÅ'', 'ƒÅ~', 'ƒÅ"', 'ƒÅT', 'ƒÅ?', 'ƒÅ-'];
      const i = Math.round((((deg || 0) % 360) + 360) % 360 / 45) % 8;
      return dirs[i];
    };

    /* Header + KV + Pluie imminente */
    let headerHTML = '
';
    headerHTML += `<div class="place-tag"><span class="pin">ÐY"?</span><span>${PLACE}</span></div>`;
    if (showCurrentTemp) {
      headerHTML += `<div class="temp">${curT != null ? `${round(curT, 0)}¶øC` : 'ƒ?"'}</div>`;
    }
    if (showMinMax) {
      headerHTML += `<div class="minmax">Max. ${tmax != null ? `${round(tmax, 0)}¶ø` : 'ƒ?"'} ¶ú Min. ${tmin != null ? `${round(tmin, 0)}¶ø` : 'ƒ?"'}</div>`;
    }
    headerHTML += '
';

    let detailsHTML = '
';
    if (showHumidity) {
      detailsHTML += `<div class="meteo-kv"><div class="k">HumiditÇ¸</div><div class="v">${curH != null ? `${round(curH, 0)}%` : 'ƒ?"'}</div></div>`;
    }
    if (showPrecipitation) {
      const mPrec = minutely?.minutely_15?.precipitation || [];
      detailsHTML += `<div class="meteo-kv"><div class="k">PrÇ¸cip.</div><div class="v">${mPrec?.[0] != null ? `${round(mPrec[0], 1)} mm/15min` : 'ƒ?"'}</div></div>`;
    }
    if (showWind) {
      detailsHTML += `<div class="meteo-kv"><div class="k">Vent</div><div class="v">${curW != null ? `${round(curW, 0)} km/h` : 'ƒ?"'}</div></div>`;
    }
    detailsHTML += '
';

    const mPrec = minutely?.minutely_15?.precipitation || [];
    const rainHTML = mPrec.some(v => v > 0.1)
      ? '
ƒsÿ‹÷? Pluie dans les 15 prochaines minutes
'
      : '
Pas de pluie imminente
';

    const leftBlock = `<div class="meteo-left">${headerHTML}${detailsHTML}${rainHTML}</div>`;

    /* Graphique de tempÇ¸rature */
    let rightBlock = '';
    if (graphEnabled) {
      const unit = `Maint. ${hours.slice(1, 4).map(p => {
        const hl = new Intl.DateTimeFormat(LOCALE, {hour: '2-digit', timeZone: TZ})
          .format(p.t).replace(':00', ' h');
        return `¶ú ${hl}`;
      }).join(' ')}`;

      rightBlock = `

TempÇ¸rature

${unit}

${svgSpark(hoursSmoothed, graphWidth, graphHeight, 10)}

`;
    }

    /* Prochaines heures (cartes horaires) */
    /* Prochaines heures (cartes horaires) */
    let hoursBlock = '';
    if (hourlyEnabled && hourCards.length) {
      const iconHourly = (mm, pop) =>
        mm === 0
          ? (Number(pop) >= 60 ? 'ÐYOÝ‹÷?' : 'ƒ~?‹÷?')
          : (mm < 1 ? 'ÐYOÝ‹÷?' : mm < 4 ? 'ÐYOõ‹÷?' : 'ƒ>^‹÷?');

      const hoursRowHTML = hourCards.map(x => {
        const hl = new Intl.DateTimeFormat(LOCALE, {hour: '2-digit', timeZone: TZ})
          .format(x.t).replace(':00', ' h');
        return `

            ${hl}
            ${iconHourly(Number(x.mm || 0), Number(x.pop || 0))}
            ${x.T != null ? `${round(x.T, 0)}¶ø` : 'ƒ?"'}
            (${x.mm != null ? `${round(x.mm || 0, 1)}mm` : 'ƒ?"'})

`;
      }).join('');

      hoursBlock = `

          ${hoursRowHTML}

`;
    }

    /* PrÇ¸visions hebdo */
    let weekBlock = '';
    if (weeklyEnabled) {
      const daysHTML = dailyTimes.slice(0, weeklyDaysToShow).map((t, i) => `

${dayNames[t.getDay()]}

          ${iconFromCode(Number(daily.weather_code?.[i] || 0))}
          ${Math.round(daily.temperature_2m_max?.[i] ?? NaN)}¶ø/${Math.round(daily.temperature_2m_min?.[i] ?? NaN)}¶ø
          ÐY'õ ${daily.precipitation_probability_max?.[i] != null ? `${daily.precipitation_probability_max[i]}%` : 'ƒ?"'}
          ${windArrow(Number(daily.winddirection_10m_dominant?.[i] || 0))} ${daily.windspeed_10m_max?.[i] != null ? `${Math.round(daily.windspeed_10m_max[i])} km/h` : 'ƒ?"'}

`).join('');

      weekBlock = `

Cette semaine

${daysHTML}

`;
    }

    $root.innerHTML = `${leftBlock}${rightBlock}${hoursBlock}${weekBlock}`;
  }

  async function fetchHourly() {
    const u = new URL('https://api.open-meteo.com/v1/forecast');
    u.searchParams.set('latitude', LAT);
    u.searchParams.set('longitude', LON);
    u.searchParams.set('timezone', TZ);
    u.searchParams.set('current', 'temperature_2m,relative_humidity_2m,windspeed_10m');
    u.searchParams.set('hourly', 'temperature_2m,precipitation,precipitation_probability,weather_code');
    u.searchParams.set('daily', 'temperature_2m_max,temperature_2m_min,weather_code,precipitation_sum,precipitation_probability_max,windspeed_10m_max,windgusts_10m_max,winddirection_10m_dominant,uv_index_max');
    return fetchJSON(u.toString(), {timeout: REQUEST_TIMEOUT});
  }

  async function fetchMinutely15() {
    const u = new URL('https://api.open-meteo.com/v1/dwd-icon');
    u.searchParams.set('latitude', LAT);
    u.searchParams.set('longitude', LON);
    u.searchParams.set('timezone', TZ);
    u.searchParams.set('minutely_15', 'precipitation');
    return fetchJSON(u.toString(), {timeout: REQUEST_TIMEOUT});
  }

  async function run() {
    $meteo.setAttribute('aria-busy', 'true');
    try {
      const [h, m] = await Promise.all([fetchHourly(), fetchMinutely15()]);
      buildUI(h || {}, m || {});
    } catch (e) {
      console.error(e);
      $meteo.innerHTML = `<div style="color:var(--muted)">MÇ¸tÇ¸o indisponible.</div>`;
    } finally {
      $meteo.setAttribute('aria-busy', 'false');
    }
  }

  let timer;
  function start() { run(); timer = setInterval(run, REFRESH_INTERVAL); }
  function stop() { clearInterval(timer); }
  start();
  document.addEventListener('visibilitychange', () => document.hidden ? stop() : start());
})();

/* =======================
   BUS 310 (La Haute Maison) via proxy
   ======================= */

//doc PRIM
//https://prim.iledefrance-mobilites.fr/fr/apis/idfm-ivtr-requete_unitaire
//monitoringRef STIF:StopPoint:Q:36243:
//LineRef STIF:Line::C01278:

(function ratpBus() {
  // VÇ¸rifier si le bus est activÇ¸
  if (!isEnabled(getConfig('bus_310.enabled', true))) {
    const elem = document.querySelector('.t-bus');
    if (elem) elem.style.display = 'none';
    return;
  }

  const PROXY = './ratp_proxy.php';

  // Configuration depuis Hyper Admin
  const REFRESH_MS = parseInt(getConfig('bus_310.refresh_interval', 30000));
  const REQUEST_TIMEOUT = parseInt(getConfig('bus_310.request_timeout', 10000));
  const SHOW_UPDATE_BADGE = isEnabled(getConfig('bus_310.show_update_badge', true));
  const SHOW_RADAR = isEnabled(getConfig('bus_310.show_radar_animation', true));

  // ArrÇ¦ts depuis la config
  const busStops = getConfig('bus_310.stops', [
    { label: 'ƒÅ' Noisy-Champs RER', ref: 'STIF:StopPoint:Q:36245:', enabled: '1', max_departures: 2 },
    { label: 'ƒÅ' Les Yvris RER',    ref: 'STIF:StopPoint:Q:36243:', enabled: '1', max_departures: 2 },
  ]);

  // Filtrer uniquement les arrÇ¦ts activÇ¸s
  const QUAYS = busStops
    .filter(stop => isEnabled(stop.enabled))
    .map(stop => ({
      label: stop.label,
      ref: stop.ref,
      maxDepartures: parseInt(stop.max_departures) || 2
    }));

  if (QUAYS.length === 0) {
    console.log('ÐYsO Aucun arrÇ¦t de bus activÇ¸ dans la configuration');
    const elem = document.querySelector('.t-bus');
    if (elem) elem.style.display = 'none';
    return;
  }

  const $zone = document.getElementById('ratp-bus');
  if (!$zone) return;

  const qs = o => Object.entries(o).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&');
  const toMin = iso => Math.max(0, Math.round((new Date(iso) - Date.now()) / 60000));

  const pill = m => {
    let content = `<span class="chip-text">${m} <span class="min">mn</span></span>`;
    if (SHOW_RADAR) {
      content = `<span class="chip-radar" aria-hidden="true"></span>` + content;
    }
    return `<span class="chip chip--rt">${content}</span>`;
  };

  function extractMinutes(j, take = 2) {
    const visits = j?.Siri?.ServiceDelivery?.StopMonitoringDelivery?.[0]?.MonitoredStopVisit || [];
    return visits.map(v => {
      const mc = v?.MonitoredVehicleJourney?.MonitoredCall || {};
      return mc.ExpectedDepartureTime || mc.ExpectedArrivalTime || mc.AimedDepartureTime || mc.AimedArrivalTime || null;
    }).filter(Boolean).map(toMin).sort((a, b) => a - b).slice(0, take);
  }

  async function getMinutes(ref, maxDepartures) {
    for (const p of [
      {api: 'stop-monitoring', MonitoringRef: ref},
      {api: 'stop-monitoring', MonitoringRef: ref, MaximumStopVisits: 6},
      {api: 'stop-monitoring', MonitoringRef: ref, MaximumStopVisits: 6, PreviewInterval: 'PT60M'},
    ]) {
      const j = await fetchJSON(`${PROXY}?${qs(p)}`, { timeout: REQUEST_TIMEOUT });
      const mins = extractMinutes(j, maxDepartures);
      if (mins.length) return mins;
    }
    return [];
  }

  async function rowsHTML() {
    let ht…2655 tokens truncated…t: 'application/json'}, cache: 'no-store' });
  if (!r.ok) throw new Error('PRIM HTTP ' + r.status);
  return r.json();
}

/* Helpers */
const norm = s => String(s || '')
  .replace(/\u00A0|\u202F|\u2009/g, ' ')
  .replace(/\s+/g, ' ')
  .trim();

function pickText(msg) {
  const arr = msg?.Content?.Message || [];
  const short = arr.find(m => m.MessageType === 'SHORT_MESSAGE');
  const full  = arr.find(m => m.MessageType === 'TEXT_ONLY');
  const any   = arr[0];
  return norm(full?.MessageText?.value || short?.MessageText?.value || any?.MessageText?.value || '');
}

const hasLineRef = (msg, ref) => (msg?.Content?.LineRef || []).some(r => (r?.value || r) === ref);
const ts = iso => { const n = Date.parse(iso); return Number.isFinite(n) ? n : NaN; };
const stillValid = (msg, now = Date.now()) => {
  const until = ts(msg?.ValidUntilTime);
  return !Number.isFinite(until) || until >= now;
};

/* Filtre basÇ¸ sur config */
function matchesTrigger(text) {
  const t = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();

  // Utiliser les mots-clÇ¸s de la config
  if (TICKER_KEYWORDS && TICKER_KEYWORDS.length > 0) {
    return TICKER_KEYWORDS.some(kw =>
      t.includes(kw.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''))
    );
  }

  // Filtre par dÇ¸faut Ç¸largi si pas de config
  return (
    /\b(est|sont|sera|seront)\s+interromp(u|ue|us|ues)\b/.test(t) ||
    /\b(est|sont|sera|seront)\s+suspend(u|ue|us|ues)\b/.test(t) ||
    /\b(est|sont|sera|seront)\s+(arrete|ferme)(e|s)?\b/.test(t) ||
    /\bfermeture\b/.test(t) ||
    /\bsuppression\b/.test(t) ||
    /\b(est|sont|sera|seront)\s+perturbe(e|s)?\b/.test(t) ||
    /\bperturbation\s+(importante|majeure|grave)\b/.test(t) ||
    /\btrafic\s+(tres\s+)?perturbe(e|s)?\b/.test(t) ||
    /\bcirculation\s+(difficile|impossible|perturbee)\b/.test(t) ||
    /\b(est|sont|sera|seront)\s+ralenti(e|s)?\b/.test(t) ||
    /\bretards?\s+(important|majeur|significatif)s?\b/.test(t) ||
    /\ballongement\s+du\s+temps\s+de\s+parcours\b/.test(t) ||
    /\bstationnement\s+(tres\s+)?prolonge?(e|s)?\b/.test(t) ||
    /\bblocage\b/.test(t) ||
    /\bdesserte\s+modifiee\b/.test(t) ||
    /\bservice\s+(limite|reduit|modifie)\b/.test(t) ||
    /\bdeviation\b/.test(t) ||
    /\bincident\s+(technique|grave|majeur)\b/.test(t) ||
    /\bpanne\b/.test(t) ||
    /\bavarie\b/.test(t) ||
    /\bdefaillance\b/.test(t) ||
    /\bbagage\s+(oublie|suspect)\b/.test(t) ||
    /\bcolis\s+suspect\b/.test(t) ||
    /\bpresence\s+(non\s+autorisee|de\s+personnes?)\b/.test(t) ||
    /\btravaux.*interrup/.test(t) ||
    /\btravaux.*modifi/.test(t) ||
    /\bmaintenance.*pertur/.test(t) ||
    /\bmalaise\s+(voyageur|personne)\b/.test(t) ||
    /\bintervention\s+(secours|police|pompiers)\b/.test(t) ||
    /\balerte\s+(securite|attentat)\b/.test(t)
  );
}

/* Niveau de gravitÇ¸ basÇ¸ sur config */
function levelFrom(text) {
  const t = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();

  // Utiliser les niveaux de la config
  if (TICKER_SEVERITY) {
    for (const [keyword, level] of Object.entries(TICKER_SEVERITY)) {
      if (t.includes(keyword.toLowerCase())) {
        return level;
      }
    }
  }

  // Logique par dÇ¸faut
  if (/interromp|suspend|ferme|supprim/.test(t)) return 'red';
  if (/perturb|incident|panne/.test(t)) return 'orange';
  if (/travaux|maintenance/.test(t)) return 'blue';
  return 'yellow';
}

/* TICKER : Affichage + scroll */
function showAlert(text, recordedTs, level) {
  if (!TICKER_ENABLED || !$PRIM_WRAP) {
    if ($PRIM_WRAP) $PRIM_WRAP.classList.add('hidden');
    return;
  }

  $PRIM_WRAP.innerHTML = `

ƒsÿ‹÷?

      ${text}

maj ${new Intl.DateTimeFormat(LOCALE, {
      hour: '2-digit', minute: '2-digit', hour12: false, timeZone: TZ
    }).format(new Date(recordedTs))}

  `;

  $PRIM_WRAP.className = `ticker-simple level-${level || 'yellow'}`;

  // Appliquer le dÇ¸filement
  setTimeout(() => setupSimpleScroll($PRIM_WRAP), 100);
}

function setupSimpleScroll(ticker) {
  const content = ticker.querySelector('.ticker-content');
  const text = ticker.querySelector('.ticker-text');
  if (!content || !text) return;

  // Reset
  text.style.animation = 'none';
  text.style.transform = 'translateX(0)';

  setTimeout(() => {
    const containerWidth = content.offsetWidth;
    const textWidth = text.scrollWidth;

    if (textWidth > containerWidth) {
      let position = 0;
      const distance = textWidth - containerWidth + 30; // marge de sÇ¸curitÇ¸
      const speed = TICKER_SCROLL_SPEED;
      const duration = distance / speed * 1000;

      function animate() {
        const progress = (Date.now() - startTime) % (duration + TICKER_PAUSE_BEFORE_SCROLL);

        if (progress < TICKER_PAUSE_BEFORE_SCROLL) {
          position = 0;
        } else {
          const scrollProgress = (progress - TICKER_PAUSE_BEFORE_SCROLL) / duration;
          position = -distance * Math.min(scrollProgress, 1);
        }

        text.style.transform = `translateX(${position}px)`;
        requestAnimationFrame(animate);
      }

      const startTime = Date.now();
      setTimeout(() => animate(), 100);
    }
  }, 100);
}

function hideAlert() {
  if ($PRIM_WRAP) {
    $PRIM_WRAP.classList.add('hidden');
  }
}

/**
 * DÇ¸tecte si un message est planifiÇ¸/futur plutÇït qu'une alerte temps rÇ¸el
 * Utilise plusieurs critÇùres :
 * 1. Date de dÇ¸but (ValidFromTime) dans le futur
 * 2. Patterns textuels indiquant une planification
 * 3. Mots-clÇ¸s BLUE (travaux, maintenance)
 */
function isPlannedMessage(msg, text) {
  const t = text.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
  const now = Date.now();

  // 1. VÇ¸rifier si ValidFromTime est dans le futur (> 2h)
  const validFrom = ts(msg?.ValidFromTime);
  if (Number.isFinite(validFrom)) {
    const hoursUntilStart = (validFrom - now) / (1000 * 3600);
    if (hoursUntilStart > 2) {
      return true; // Message qui commence dans plus de 2h = planifiÇ¸
    }
  }

  // 2. Patterns textuels indiquant une planification future
  const plannedPatterns = [
    /\ba\s+partir\s+du\s+\d/i,           // "Çÿ partir du 25"
    /\bdu\s+\d+.*au\s+\d+/i,             // "du 20 au 25"
    /\bde\s+\d+.*a\s+\d+/i,              // "de 20h Çÿ 22h" (future)
    /\bprochainement\b/i,                // "prochainement"
    /\bdebut\s+(janvier|fevrier|mars|avril|mai|juin|juillet|aout|septembre|octobre|novembre|decembre)/i,
    /\bfin\s+(janvier|fevrier|mars|avril|mai|juin|juillet|aout|septembre|octobre|novembre|decembre)/i,
    /\b(lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche)\s+\d+/i, // "lundi 25"
    /\ble\s+\d+\s+(janvier|fevrier|mars)/i, // "le 25 janvier"
    /\bsera.*ferme/i,                    // "sera fermÇ¸" (futur)
    /\bseront.*interrompu/i,             // "seront interrompus"
    /\bprogramme/i,                      // "travaux programmÇ¸s"
    /\bprevu/i,                          // "intervention prÇ¸vue"
  ];

  if (plannedPatterns.some(pattern => pattern.test(text))) {
    return true;
  }

  // 3. Mots-clÇ¸s BLUE (travaux, maintenance) - sauf si incident en cours
  const blueKeywords = [];
  if (TICKER_SEVERITY) {
    for (const [keyword, level] of Object.entries(TICKER_SEVERITY)) {
      if (level === 'blue') {
        blueKeywords.push(keyword.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, ''));
      }
    }
  }

  if (blueKeywords.some(kw => t.includes(kw))) {
    // Exception : si "travaux" mais "en cours" ou "actuellement" = temps rÇ¸el
    if (!/\b(en\s+cours|actuellement|ce\s+jour|aujourd.?hui)\b/i.test(text)) {
      return true;
    }
  }

  return false;
}

/* Chargeur principal */
async function loadPrimTicker(lineKey) {
  if (!TICKER_ENABLED) {
    hideAlert();
    return;
  }

  lineKey = lineKey || TICKER_LINE_NAME;

  try {
    const data = await fetchPrimMessages(lineKey);
    const delivery = data?.Siri?.ServiceDelivery?.GeneralMessageDelivery?.[0];
    const infos = delivery?.InfoMessage || [];
    const lineRef = LINE_MAP[lineKey] || TICKER_LINE_REF;
    const now = Date.now();
    const maxAge = primMaxAgeMs();

    const pool = infos
      .filter(im => (!lineRef || hasLineRef(im, lineRef)))
      .filter(im => stillValid(im, now))
 .filter(im => {
        const channel = (im?.InfoChannelRef?.value || '').toLowerCase();
        return channel === 'perturbation' || channel === 'information' || channel === '';
      })

      .map(im => ({
        msg: im,
        text: pickText(im),
        recorded: ts(im?.RecordedAtTime)
      }))
      .filter(x => x.text && Number.isFinite(x.recorded) && (now - x.recorded) <= maxAge)
      .filter(x => matchesTrigger(x.text))
      .filter(x => !isPlannedMessage(x.msg, x.text)) // Exclure les messages planifiÇ¸s
      .sort((a, b) => b.recorded - a.recorded);

    const chosen = pool[0];
    if (!chosen) { hideAlert(); return; }
    showAlert(chosen.text, chosen.recorded, levelFrom(chosen.text));
  } catch (e) {
    console.warn('PRIM load error:', e);
    hideAlert();
  }
}
/* Appel initial + refrech auto si activÇ¸ */
if (TICKER_ENABLED) {
  loadPrimTicker();
  setInterval(() => loadPrimTicker(), TICKER_REFRESH);

  /* Raccourci A pour basculer 24h <-> 1mois */
  window.addEventListener('keydown', e => {
    if ((e.key || '').toLowerCase() === 'a') {
      window.PRIM.maxAgeHours = (window.PRIM.maxAgeHours === TICKER_TIME_WINDOW_EXTENDED ?
        TICKER_TIME_WINDOW : TICKER_TIME_WINDOW_EXTENDED);
      loadPrimTicker();
    }
  });
} else {
  hideAlert();
}

// Expose pour debug
window.loadPrimTicker = loadPrimTicker;

/* ===== Marquee Annonces ===== */
(function annoncesMarquee() {
  // VÇ¸rifier si les annonces sont activÇ¸es
  if (!isEnabled(getConfig('announcements.enabled', true))) {
    const elem = document.querySelector('.annonces-wrap');
    if (elem) elem.style.display = 'none';
    return;
  }

  // Configuration depuis Hyper Admin
  const ANIMATION_ENABLED = isEnabled(getConfig('announcements.animation.enabled', true));
  const DEFAULT_LINES = parseInt(getConfig('announcements.animation.visible_lines', 3));
  const DEFAULT_SPEED = parseInt(getConfig('announcements.animation.scroll_speed', 18));
  const DEFAULT_PAUSE = parseInt(getConfig('announcements.animation.pause_duration', 900));

  function setupAnnMarquee(body) {
    const inner = body.querySelector('.ann-text');
    if (!inner) return;

    // Applique le nombre de lignes (CSS custom prop)
    const lines = parseInt(body.dataset.maxlines || DEFAULT_LINES, 10);
    body.style.setProperty('--ann-lines', lines);

    // Speed pixels/sec & pause ms
    const pxPerSec = parseFloat(body.dataset.speed || DEFAULT_SPEED);
    const pause    = parseInt(body.dataset.pause || DEFAULT_PAUSE, 10);

    // Hauteur visible vs contenu rÇ¸el
    const h = body.clientHeight;
    const H = inner.scrollHeight;
    if (H <= h + 1) return; // pas de dÇ¸passement => pas de dÇ¸filement

    body.classList.add('scrolling');
    const dist = Math.max(0, H - h);
    const dur  = dist / pxPerSec * 1000;

    let anim, stop = false;
    function loop() {
      if (stop) return;
      inner.style.transform = 'translateY(0)';
      anim = inner.animate(
        [{ transform: 'translateY(0)' }, { transform: 'translateY(' + (-dist) + 'px)' }],
        { duration: Math.max(800, dur), easing: 'linear', fill: 'forwards' }
      );
      anim.onfinish = () => {
        setTimeout(() => {
          if (stop) return;
          inner.style.transform = 'translateY(0)';
          setTimeout(loop, pause);
        }, pause);
      };
    }

    if (ANIMATION_ENABLED) {
      loop();
    }

    // Resize => recalculer
    let to;
    const onResize = () => {
      clearTimeout(to);
      to = setTimeout(() => {
        stop = true;
        if (anim) anim.cancel();
        body.classList.remove('scrolling');
        inner.style.transform = 'translateY(0)';
        setupAnnMarquee(body);
      }, 150);
    };
    window.addEventListener('resize', onResize, { passive: true });
    body._annMarqueeCleanup = () => {
      stop = true;
      if (anim) anim.cancel();
      window.removeEventListener('resize', onResize);
      body.classList.remove('scrolling');
      inner.style.transform = 'translateY(0)';
    };
  }

  // Applique marquee Çÿ tous les .ann-body
  const $bodies = document.querySelectorAll('.ann-body');
  $bodies.forEach(setupAnnMarquee);

  // Si contenu change => refaire marquee
  const obs = new MutationObserver(muts => {
    for (const mut of muts) {
      if (mut.type === 'childList' && mut.target.classList?.contains('ann-body')) {
        if (mut.target._annMarqueeCleanup) mut.target._annMarqueeCleanup();
        setupAnnMarquee(mut.target);
      }
    }
  });
  $bodies.forEach(b => obs.observe(b, { childList: true, subtree: true }));
})();

// ============================================
// MODE DEBUG
// ============================================

if (isEnabled(getConfig('display.debug_mode', false))) {
  console.log('ÐY?> MODE DEBUG HYPER ADMIN ACTIVÇ%');
  console.log('Configuration active:', window.HYPER_CONFIG);

  // Ajouter un indicateur visuel
  const debugBadge = document.createElement('div');
  debugBadge.style.cssText = 'position:fixed;bottom:10px;left:10px;background:red;color:white;padding:5px 10px;border-radius:5px;z-index:9999;font-size:12px;';
  debugBadge.textContent = 'DEBUG MODE';
  document.body.appendChild(debugBadge);
}

console.log('ƒo. Front.js Hyper Admin chargÇ¸ avec succÇùs');

/* =======================
   HOT RELOAD Annonces / ThÇùme
   ======================= */
/* =======================
   HOT RELOAD Annonces / ThÇùme
   ======================= */
(function hotReload() {
  // Config depuis Hyper Admin
  const HOT_ENABLED = isEnabled(getConfig('hot_reload.enabled', true));
  const POLL_SEC_RAW = parseInt(getConfig('hot_reload.poll_interval_sec', 15), 10);
  const POLL_SEC = Number.isFinite(POLL_SEC_RAW) ? POLL_SEC_RAW : 15;

  // Clamp simple : entre 1s et 6h
  const POLL_MS = Math.min(6 * 60 * 60 * 1000, Math.max(1000, POLL_SEC * 1000));

  const STATUS_URL   = 'admin/data_status.php';
  const FRAGMENT_URL = 'admin/annonces_widget.php?mode=fragment';

  // Pas de fetch ou hot reload dÇ¸sactivÇ¸ ƒÅ' on sort
  if (!window.fetch) return;
  if (!HOT_ENABLED) {
    console.log('[HOTRELOAD] DÇ¸sactivÇ¸ via Hyper Admin');
    return;
  }

  let annoncesSig = null;
  let themeSig    = null;
  let hyperSig    = null; // pour dÇ¸tecter les modifs de hyperadmin.json

  async function reloadAnnonces() {
    const panel = document.getElementById('annonces-panel');
    if (!panel) return;

    try {
      // On rÇ¸cupÇùre d'abord le HTML Çÿ jour
      const res = await fetch(FRAGMENT_URL + '&ts=' + Date.now(), { cache: 'no-store' });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const html = await res.text();

      // On dÇ¸clenche le fondu de sortie
      panel.classList.remove('ann-fade-in');
      panel.classList.add('ann-fade-out');

      // On attend la fin de la transition (ou fallback timeout)
      const DURATION = 350; // doit correspondre au CSS

      setTimeout(() => {
        // On remplace le contenu
        panel.innerHTML = html;

        // On relance les dÇ¸filements si ta fonction globale existe
        if (window.initAnnMarquees) {
          window.initAnnMarquees();
        }

        // On passe en fondu d'entrÇ¸e
        panel.classList.remove('ann-fade-out');
        panel.classList.add('ann-fade-in');

        // On nettoie la classe aprÇùs lƒ?Tanim
        setTimeout(() => {
          panel.classList.remove('ann-fade-in');
        }, DURATION);
      }, DURATION);

    } catch (e) {
      console.warn('[HOTRELOAD] Erreur lors du rechargement des annonces', e);
    }
  }

  function reloadThemeCss() {
    // On ne recharge que la feuille theme-vars.php via son <link>
    const link = document.querySelector('link[href*="theme-vars.php"]');
    if (!link) return;

    const base = link.getAttribute('href').split('?')[0];
    const newHref = base + '?ts=' + Date.now();

    link.setAttribute('href', newHref);
    // Le navigateur applique les nouvelles variables CSS sans recharger le HTML
  }

  async function tick() {
    try {
      const res = await fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store' });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const data = await res.json();

      // Signature "annonces" = annonces.json + ui.json
      const newAnnSig = JSON.stringify({
        annonces: data.annonces && data.annonces.mtime || null,
        ui:       data.ui && data.ui.mtime || null,
      });

      // Signature "thÇùme" = fichier de thÇùme + thÇùme actif + couleurs annonces
      const newThemeSig = JSON.stringify({
        themes:       data.themes && data.themes.mtime || null,
        activeTheme:  data.active_theme && data.active_theme.mtime || null,
        annColors:    data.annonces_theme && data.annonces_theme.mtime || null,
      });

      // Signature "Hyper Admin" = fichier central hyperadmin.json
      const newHyperSig = JSON.stringify({
        hyperadmin: data.hyperadmin && data.hyperadmin.mtime || null,
      });

      // Si la config Hyper Admin a changÇ¸ ƒÅ' reload complet
      if (hyperSig && newHyperSig !== hyperSig) {
        console.log('[HOTRELOAD] Config Hyper Admin modifiÇ¸e ƒÅ' reload complet');
        window.location.reload();
        return; // on ne va pas plus loin, la page se recharge
      }

      // Si les annonces/UI ont changÇ¸ ƒÅ' on remplace juste la zone annonces
      if (annoncesSig && newAnnSig !== annoncesSig) {
        console.log('[HOTRELOAD] Annonces modifiÇ¸es ƒÅ' rechargement de la grille');
        reloadAnnonces();
      }

      // Si le thÇùme a changÇ¸ ƒÅ' on recharge uniquement le CSS de variables
      if (themeSig && newThemeSig !== themeSig) {
        console.log('[HOTRELOAD] ThÇùme modifiÇ¸ ƒÅ' rechargement des variables CSS');
        reloadThemeCss();
      }

      annoncesSig = newAnnSig;
      themeSig    = newThemeSig;
      hyperSig    = newHyperSig;

    } catch (e) {
      console.warn('[HOTRELOAD] Erreur lors du tick', e);
    } finally {
      setTimeout(tick, POLL_MS);
    }
  }

// ===== Helper pour afficher le dÇ¸lai proprement =====
  function formatDelay(ms) {
    const totalSec = Math.round(ms / 1000);

    if (totalSec < 60) {
      return totalSec + ' sec';
    }

    const minutes = Math.floor(totalSec / 60);
    const seconds = totalSec % 60;

    if (minutes < 60) {
      return minutes + ' min' + (seconds ? ' ' + seconds + ' sec' : '');
    }

    const hours = Math.floor(minutes / 60);
    const remMin = minutes % 60;

    let out = hours + ' h';
    if (remMin) out += ' ' + remMin + ' min';
    if (seconds) out += ' ' + seconds + ' sec';
    return out;
  }

  console.log('[HOTRELOAD] ActivÇ¸, intervalle =',   POLL_MS, 'ms (ƒ%^', formatDelay(POLL_MS), ')'  );
  // On lance le premier check aprÇùs un dÇ¸lai (pour laisser la page se stabiliser)
  setTimeout(tick, POLL_MS);
})();


**Deciding how to handle truncated file**


Get-Content assets/js/front.js -Tail 200


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:
  // Si contenu change => refaire marquee
  const obs = new MutationObserver(muts => {
    for (const mut of muts) {
      if (mut.type === 'childList' && mut.target.classList?.contains('ann-body')) {
        if (mut.target._annMarqueeCleanup) mut.target._annMarqueeCleanup();
        setupAnnMarquee(mut.target);
      }
    }
  });
  $bodies.forEach(b => obs.observe(b, { childList: true, subtree: true }));
})();

// ============================================
// MODE DEBUG
// ============================================

if (isEnabled(getConfig('display.debug_mode', false))) {
  console.log('ÐY?> MODE DEBUG HYPER ADMIN ACTIVÇ%');
  console.log('Configuration active:', window.HYPER_CONFIG);

  // Ajouter un indicateur visuel
  const debugBadge = document.createElement('div');
  debugBadge.style.cssText = 'position:fixed;bottom:10px;left:10px;background:red;color:white;padding:5px 10px;border-radius:5px;z-index:9999;font-size:12px;';
  debugBadge.textContent = 'DEBUG MODE';
  document.body.appendChild(debugBadge);
}

console.log('ƒo. Front.js Hyper Admin chargÇ¸ avec succÇùs');

/* =======================
   HOT RELOAD Annonces / ThÇùme
   ======================= */
/* =======================
   HOT RELOAD Annonces / ThÇùme
   ======================= */
(function hotReload() {
  // Config depuis Hyper Admin
  const HOT_ENABLED = isEnabled(getConfig('hot_reload.enabled', true));
  const POLL_SEC_RAW = parseInt(getConfig('hot_reload.poll_interval_sec', 15), 10);
  const POLL_SEC = Number.isFinite(POLL_SEC_RAW) ? POLL_SEC_RAW : 15;

  // Clamp simple : entre 1s et 6h
  const POLL_MS = Math.min(6 * 60 * 60 * 1000, Math.max(1000, POLL_SEC * 1000));

  const STATUS_URL   = 'admin/data_status.php';
  const FRAGMENT_URL = 'admin/annonces_widget.php?mode=fragment';

  // Pas de fetch ou hot reload dÇ¸sactivÇ¸ ƒÅ' on sort
  if (!window.fetch) return;
  if (!HOT_ENABLED) {
    console.log('[HOTRELOAD] DÇ¸sactivÇ¸ via Hyper Admin');
    return;
  }

  let annoncesSig = null;
  let themeSig    = null;
  let hyperSig    = null; // pour dÇ¸tecter les modifs de hyperadmin.json

  async function reloadAnnonces() {
    const panel = document.getElementById('annonces-panel');
    if (!panel) return;

    try {
      // On rÇ¸cupÇùre d'abord le HTML Çÿ jour
      const res = await fetch(FRAGMENT_URL + '&ts=' + Date.now(), { cache: 'no-store' });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const html = await res.text();

      // On dÇ¸clenche le fondu de sortie
      panel.classList.remove('ann-fade-in');
      panel.classList.add('ann-fade-out');

      // On attend la fin de la transition (ou fallback timeout)
      const DURATION = 350; // doit correspondre au CSS

      setTimeout(() => {
        // On remplace le contenu
        panel.innerHTML = html;

        // On relance les dÇ¸filements si ta fonction globale existe
        if (window.initAnnMarquees) {
          window.initAnnMarquees();
        }

        // On passe en fondu d'entrÇ¸e
        panel.classList.remove('ann-fade-out');
        panel.classList.add('ann-fade-in');

        // On nettoie la classe aprÇùs lƒ?Tanim
        setTimeout(() => {
          panel.classList.remove('ann-fade-in');
        }, DURATION);
      }, DURATION);

    } catch (e) {
      console.warn('[HOTRELOAD] Erreur lors du rechargement des annonces', e);
    }
  }

  function reloadThemeCss() {
    // On ne recharge que la feuille theme-vars.php via son <link>
    const link = document.querySelector('link[href*="theme-vars.php"]');
    if (!link) return;

    const base = link.getAttribute('href').split('?')[0];
    const newHref = base + '?ts=' + Date.now();

    link.setAttribute('href', newHref);
    // Le navigateur applique les nouvelles variables CSS sans recharger le HTML
  }

  async function tick() {
    try {
      const res = await fetch(STATUS_URL + '?t=' + Date.now(), { cache: 'no-store' });
      if (!res.ok) throw new Error('HTTP ' + res.status);
      const data = await res.json();

      // Signature "annonces" = annonces.json + ui.json
      const newAnnSig = JSON.stringify({
        annonces: data.annonces && data.annonces.mtime || null,
        ui:       data.ui && data.ui.mtime || null,
      });

      // Signature "thÇùme" = fichier de thÇùme + thÇùme actif + couleurs annonces
      const newThemeSig = JSON.stringify({
        themes:       data.themes && data.themes.mtime || null,
        activeTheme:  data.active_theme && data.active_theme.mtime || null,
        annColors:    data.annonces_theme && data.annonces_theme.mtime || null,
      });

      // Signature "Hyper Admin" = fichier central hyperadmin.json
      const newHyperSig = JSON.stringify({
        hyperadmin: data.hyperadmin && data.hyperadmin.mtime || null,
      });

      // Si la config Hyper Admin a changÇ¸ ƒÅ' reload complet
      if (hyperSig && newHyperSig !== hyperSig) {
        console.log('[HOTRELOAD] Config Hyper Admin modifiÇ¸e ƒÅ' reload complet');
        window.location.reload();
        return; // on ne va pas plus loin, la page se recharge
      }

      // Si les annonces/UI ont changÇ¸ ƒÅ' on remplace juste la zone annonces
      if (annoncesSig && newAnnSig !== annoncesSig) {
        console.log('[HOTRELOAD] Annonces modifiÇ¸es ƒÅ' rechargement de la grille');
        reloadAnnonces();
      }

      // Si le thÇùme a changÇ¸ ƒÅ' on recharge uniquement le CSS de variables
      if (themeSig && newThemeSig !== themeSig) {
        console.log('[HOTRELOAD] ThÇùme modifiÇ¸ ƒÅ' rechargement des variables CSS');
        reloadThemeCss();
      }

      annoncesSig = newAnnSig;
      themeSig    = newThemeSig;
      hyperSig    = newHyperSig;

    } catch (e) {
      console.warn('[HOTRELOAD] Erreur lors du tick', e);
    } finally {
      setTimeout(tick, POLL_MS);
    }
  }

// ===== Helper pour afficher le dÇ¸lai proprement =====
  function formatDelay(ms) {
    const totalSec = Math.round(ms / 1000);

    if (totalSec < 60) {
      return totalSec + ' sec';
    }

    const minutes = Math.floor(totalSec / 60);
    const seconds = totalSec % 60;

    if (minutes < 60) {
      return minutes + ' min' + (seconds ? ' ' + seconds + ' sec' : '');
    }

    const hours = Math.floor(minutes / 60);
    const remMin = minutes % 60;

    let out = hours + ' h';
    if (remMin) out += ' ' + remMin + ' min';
    if (seconds) out += ' ' + seconds + ' sec';
    return out;
  }

  console.log('[HOTRELOAD] ActivÇ¸, intervalle =',   POLL_MS, 'ms (ƒ%^', formatDelay(POLL_MS), ')'  );
  // On lance le premier check aprÇùs un dÇ¸lai (pour laisser la page se stabiliser)
  setTimeout(tick, POLL_MS);
})();


Summary: auto


**Inspecting admin directory**


Output:
Exit code: 0
Wall time: 0.9 seconds
Output:

    R‚pertoireÿ: C:\wamp64\www\cssp2AI\admin

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        21/11/2025     14:18                assets
d-----        21/11/2025     14:18                data
d-----        21/11/2025     14:18                logs
d-----        21/11/2025     15:31                partials
d-----        21/11/2025     14:18                wiki
-a----        18/11/2025     09:46           1609 annonces_theme.php
-a----        24/11/2025     11:31           9533 annonces_theme_admin.php
-a----        21/11/2025     16:25          15757 annonces_widget.php
-a----        21/11/2025     16:18          10979 auth.php
-a----        20/11/2025     17:36          11691 backups_manager.php
-a----        18/11/2025     16:24           1787 cleanupjson.php
-a----        18/11/2025     09:46            968 config_annonces.php
-a----        18/11/2025     09:54           1105 data_status.php
-a----        24/11/2025     11:31           2006 debug_patch.php
-a----        18/11/2025     16:24           1760 debug_traffic_alerts.php
-a----        21/11/2025     16:10           2255 header.php
-a----        21/11/2025     16:08           9421 helpers.php
-a----        24/11/2025     11:31          49346 hyperadmin.php
-a----        24/11/2025     10:58          19242 hyperadmin_config.php
-a----        24/11/2025     11:31          62684 hyperadmin_forms.php
-a----        24/11/2025     11:31          47259 index.php
-a----        19/11/2025     16:38            199 logout.php
-a----        20/11/2025     14:10           8378 permissions.php
-a----        24/11/2025     11:31          10817 reset_password.php
-a----        19/11/2025     14:33             19 reset_tokens.json
-a----        19/11/2025     16:08           5728 reset_tokens_manager.php
-a----        18/11/2025     14:34           2926 test_logique.php
-a----        24/11/2025     11:31          15828 themes_admin.php
-a----        18/11/2025     09:46           8318 themes_config.php
-a----        18/11/2025     09:46         121441 themes_config_unified.php
-a----        24/11/2025     11:31          24080 themes_unified_admin.php
-a----        24/11/2025     11:31          25888 ui_admin.php
-a----        19/11/2025     10:34          10416 users_manager.php


**Planning code review approach**


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:
<?php
/**
 * HYPER ADMIN - Intゼgration Front-End
 * Expose la configuration centralisゼe au JavaScript du front-end
 *
 * Ce fichier doit オtre inclus dans index.php APRぷS les requires
 */

// Vゼrifier si le fichier de config existe
if (!file_exists(__DIR__ . '/admin/hyperadmin_config.php')) {
    // Si pas de config Hyper Admin, ne rien faire
    return;
}

require_once __DIR__ . '/admin/hyperadmin_config.php';

// Charger la configuration
$HYPER_CONFIG = loadHyperConfig();

// Gゼnゼrer le JavaScript de configuration
function generateHyperConfigJS() {
    global $HYPER_CONFIG;

    // Filtrer les donnゼes sensibles
    $safeConfig = $HYPER_CONFIG;
    unset($safeConfig['security']['admin_password']);
    if (isset($safeConfig['system']['api_keys'])) {
        unset($safeConfig['system']['api_keys']['prim_api_key']);
        unset($safeConfig['system']['api_keys']['proxy_secret']);
    }

    return json_encode($safeConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}

// Variables PHP pour utilisation directe dans index.php si besoin
$HYPER_DATETIME_ENABLED = $HYPER_CONFIG['datetime']['enabled'] ?? true;
$HYPER_TIMEZONE = $HYPER_CONFIG['datetime']['timezone'] ?? 'Europe/Paris';
$HYPER_LOCALE = $HYPER_CONFIG['datetime']['locale'] ?? 'fr-FR';

$HYPER_BUS_ENABLED = $HYPER_CONFIG['bus_310']['enabled'] ?? true;
$HYPER_RER_ENABLED = $HYPER_CONFIG['rer_a']['enabled'] ?? true;
$HYPER_WEATHER_ENABLED = $HYPER_CONFIG['weather']['enabled'] ?? true;
$HYPER_TICKER_ENABLED = $HYPER_CONFIG['traffic_alerts']['enabled'] ?? true;

// Helper pour obtenir une valeur de config
function hyper($path, $default = null) {
    return getHyperConfigValue($path, $default);
}

/**
 * Indique si la configuration Hyper Admin est disponible.
 */
function hyper_has_config(): bool {
    return isset($GLOBALS['HYPER_CONFIG']) && is_array($GLOBALS['HYPER_CONFIG']);
}

/**
 * Affiche le script JS exposant la configuration Hyper Admin.
 * A appeler depuis <head> pour que front.js dispose des helpers.
 */
function hyper_render_config_script(): void {
    if (!hyper_has_config()) {
        return;
    }

    $configJson = generateHyperConfigJS();
    ?>
    <script>
    (function () {
        const config = <?= $configJson ?>;

        const toBool = (value, fallback = false) => {
            if (value === undefined || value === null) return fallback;
            if (typeof value === 'boolean') return value;
            if (typeof value === 'number') return value !== 0;
            if (typeof value === 'string') {
                const normalized = value.trim().toLowerCase();
                if (normalized === '1' || normalized === 'true') return true;
                if (normalized === '0' || normalized === 'false') return false;
            }
            return Boolean(value);
        };

        const readPath = (path, fallback) => {
            if (!path) return fallback;
            const keys = path.split('.');
            let ref = config;
            for (const key of keys) {
                if (ref && Object.prototype.hasOwnProperty.call(ref, key)) {
                    ref = ref[key];
                } else {
                    return fallback;
                }
            }
            return ref;
        };

        window.HYPER_CONFIG = config;
        window.hyper = {
            get: readPath,
            getSection: section => readPath(section, {}),
            isEnabled: (section, fallback = true) => {
                const key = section.includes('.') ? section : section + '.enabled';
                return toBool(readPath(key, fallback), fallback);
            }
        };

        const SECTION_SELECTORS = {
            datetime: '.row-date-time',
            bus_310: '.t-bus',
            rer_a: '.t-rer',
            weather: '.meteo--compact',
            traffic_alerts: '#ticker-prim',
            announcements: '.annonces-wrap'
        };

        const applyVisibility = () => {
            const visibilityOverrides = readPath('display.sections_visibility', {});
            Object.entries(SECTION_SELECTORS).forEach(([key, selector]) => {
                const explicit = Object.prototype.hasOwnProperty.call(visibilityOverrides, key)
                    ? visibilityOverrides[key]
                    : undefined;
                const enabled = explicit !== undefined ? toBool(explicit, true) : window.hyper.isEnabled(key, true);
                document.querySelectorAll(selector).forEach((node) => {
                    node.style.display = enabled ? '' : 'none';
                });
            });

            if (toBool(readPath('display.debug_mode', false))) {
                console.debug('[HyperAdmin] Configuration appliquee', config);
            }
        };

        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', applyVisibility, { once: true });
        } else {
            applyVisibility();
        }
    })();
    </script>
    <?php
}


**Noticing encoding issues**


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:
<?php
/**
 * HYPER ADMIN - Configuration centralisÇ¸e
 * Gestion de TOUS les paramÇùtres du systÇùme depuis une source unique
 *
 * @version 1.0 - Configuration complÇùte
 * @author Claude Assistant
 */

// Chemin vers le fichier de configuration JSON
define('HYPERADMIN_CONFIG_FILE', __DIR__ . '/data/hyperadmin.json');
define('HYPERADMIN_BACKUP_DIR', __DIR__ . '/data/backups/');

/**
 * Structure complÇùte de configuration par dÇ¸faut
 * Tous les paramÇùtres administrables du systÇùme
 */
$HYPERADMIN_DEFAULTS = [

    // ============================================
    // 1. DATE & HEURE
    // ============================================
    'datetime' => [
        'timezone' => 'Europe/Paris',
        'date_format' => 'd MMMM yyyy',  // Format Intl
        'time_format_24h' => true,
        'locale' => 'fr-FR',
        'show_seconds' => true,
        'show_weekday' => true,
        'refresh_interval' => 1000,  // ms
        'enabled' => true
    ],

    // ============================================
    // 2. TRANSPORT - BUS 310
    // ============================================
    'bus_310' => [
        'enabled' => true,
        'api_endpoint' => './ratp_proxy.php',
        'refresh_interval' => 30000,  // ms
        'request_timeout' => 10000,   // ms
        'show_update_badge' => true,
        'show_radar_animation' => true,
        'stops' => [
            [
                'ref' => 'STIF:StopPoint:Q:36245:',
                'label' => 'Noisy-Champs RER',
                'enabled' => true,
                'max_departures' => 2
            ],
            [
                'ref' => 'STIF:StopPoint:Q:36243:',
                'label' => 'Les Yvris RER',
                'enabled' => true,
                'max_departures' => 2
            ]
        ]
    ],

    // ============================================
    // 3. TRANSPORT - RER A
    // ============================================
    'rer_a' => [
        'enabled' => true,
        'refresh_interval' => 60000,  // ms
        'show_missions' => true,
        'show_destinations' => true,
        'time_format' => 'absolute',  // 'absolute' ou 'relative'
        'stations' => [
            [
                'name' => 'Noisy-Champs',
                'ref' => 'stop_point:IDFM:470025',
                'enabled' => true,
                'max_departures_paris' => 3,
                'max_departures_mlv' => 3
            ],
            [
                'name' => 'Noisy-le-Grand ƒ?" Mont d\'Est',
                'ref' => 'stop_point:IDFM:470023',
                'enabled' => true,
                'max_departures_paris' => 3,
                'max_departures_mlv' => 3
            ]
        ]
    ],

    // ============================================
    // 4. ALERTES TRAFIC (TICKER PRIM)
    // ============================================
    'traffic_alerts' => [
        'enabled' => true,
        'line_ref' => 'STIF:Line::C01742:',  // RER A
        'line_name' => 'RER_A',
        'refresh_interval' => 300000,  // 5 minutes en ms
        'time_window_hours' => 24,  // FenÇ¦tre normale
        'time_window_extended_hours' => 720,  // Mode Ç¸tendu (1 mois)
        'trigger_keywords' => [
            'interrompu', 'interrompue', 'interrompus', 'interrompues',
            'suspendu', 'suspendue', 'suspendus', 'suspendues',
            'arrÇ¦tÇ¸', 'arrÇ¦tÇ¸e', 'arrÇ¦tÇ¸s', 'arrÇ¦tÇ¸es',
            'fermÇ¸', 'fermÇ¸e', 'fermÇ¸s', 'fermÇ¸es',
            'fermeture', 'suppression', 'blocage',
            'perturbÇ¸', 'perturbÇ¸e', 'perturbÇ¸s', 'perturbÇ¸es',
            'perturbation importante', 'perturbation majeure', 'perturbation grave',
            'trafic perturbÇ¸', 'trafic trÇùs perturbÇ¸',
            'circulation difficile', 'circulation impossible', 'circulation perturbÇ¸e',
            'ralenti', 'ralentie', 'ralentis', 'ralenties',
            'retard important', 'retard majeur', 'retard significatif',
            'allongement du temps de parcours',
            'stationnement prolongÇ¸', 'stationnement trÇùs prolongÇ¸',
            'desserte modifiÇ¸e', 'service limitÇ¸', 'service rÇ¸duit', 'service modifiÇ¸',
            'dÇ¸viation',
            'incident technique', 'incident grave', 'incident majeur',
            'panne', 'avariÇ¸', 'dÇ¸faillance',
            'bagage oubliÇ¸', 'bagage suspect', 'colis suspect',
            'prÇ¸sence non autorisÇ¸e',
            'travaux', 'maintenance',
            'malaise voyageur', 'malaise personne',
            'intervention secours', 'intervention police', 'intervention pompiers',
            'alerte sÇ¸curitÇ¸', 'alerte attentat'
        ],
        'severity_levels' => [
            // ÐY"ï RED - Interruptions totales
            'interrompu' => 'red',
            'interrompue' => 'red',
            'interrompus' => 'red',
            'interrompues' => 'red',
            'suspendu' => 'red',
            'suspendue' => 'red',
            'suspendus' => 'red',
            'suspendues' => 'red',
            'arrÇ¦tÇ¸' => 'red',
            'arrÇ¦tÇ¸e' => 'red',
            'fermÇ¸' => 'red',
            'fermÇ¸e' => 'red',
            'fermeture' => 'red',
            'suppression' => 'red',
            'blocage' => 'red',
            'circulation impossible' => 'red',
            'alerte attentat' => 'red',
            // ÐYYÿ ORANGE - Perturbations importantes
            'perturbÇ¸' => 'orange',
            'perturbÇ¸e' => 'orange',
            'perturbation importante' => 'orange',
            'perturbation majeure' => 'orange',
            'perturbation grave' => 'orange',
            'trafic trÇùs perturbÇ¸' => 'orange',
            'circulation difficile' => 'orange',
            'incident grave' => 'orange',
            'incident majeur' => 'orange',
            'panne' => 'orange',
            'avariÇ¸' => 'orange',
            'dÇ¸faillance' => 'orange',
            'bagage suspect' => 'orange',
            'colis suspect' => 'orange',
            'alerte sÇ¸curitÇ¸' => 'orange',
            // ÐYY YELLOW - Perturbations modÇ¸rÇ¸es
            'ralenti' => 'yellow',
            'ralentie' => 'yellow',
            'retard important' => 'yellow',
            'stationnement prolongÇ¸' => 'yellow',
            'desserte modifiÇ¸e' => 'yellow',
            'service limitÇ¸' => 'yellow',
            'dÇ¸viation' => 'yellow',
            'incident technique' => 'yellow',
            'circulation perturbÇ¸e' => 'yellow',
            'malaise voyageur' => 'yellow',
            'intervention' => 'yellow'
        ],
        'scroll_speed' => 30,  // px/s
        'pause_before_scroll' => 2000  // ms
    ],

    // ============================================
    // 5. MÇ%TÇ%O
    // ============================================
    'weather' => [
        'enabled' => true,
        'location' => [
            'latitude' => 48.8326848,
            'longitude' => 2.535359,
            'name' => 'Noisy-le-Grand'
        ],
        'refresh_interval' => 600000,  // 10 minutes
        'request_timeout' => 10000,    // ms
        'api_endpoints' => [
            'hourly' => 'https://api.open-meteo.com/v1/forecast',
            'minutely' => 'https://api.open-meteo.com/v1/dwd-icon'
        ],
        'display' => [
            'current_temp' => true,
            'min_max' => true,
            'humidity' => true,
            'wind' => true,
            'precipitation' => true,
            'uv_index' => true
        ],
        'graph' => [
            'enabled' => true,
            'hours_to_show' => 10,
            'height' => 120,  // px
            'width' => 700    // px
        ],
        'hourly_forecast' => [
            'enabled' => true,
            'hours_to_show' => 8,
            'show_icons' => true,
            'show_temp' => true,
            'show_precipitation' => true
        ],
        'weekly_forecast' => [
            'enabled' => true,
            'days_to_show' => 5
        ]
    ],

    // ============================================
    // 6. ANNONCES
    // ============================================
    'announcements' => [
        'enabled' => true,
        'max_visible' => 4,
        'layout' => [
            'template' => 'grid-2x2',  // grid-2x2, grid-3plus3, grid-1plus2, etc.
            'gap' => 10  // px
        ],
        'animation' => [
            'enabled' => true,
            'visible_lines' => 3,
            'scroll_speed' => 18,  // px/s
            'pause_duration' => 900  // ms
        ],
        'colors' => [
            'neutral',
            'info',
            'warn',
            'emph'
        ]
    ],

    // ============================================
    // 7. SYSTÇ^ME & CACHE
    // ============================================
    'system' => [
        'cache' => [
            'ratp_ttl_stop_monitoring' => 15,  // secondes
            'ratp_ttl_messages' => 60,         // secondes
            'cache_directory' => '/cache/ratp/'
        ],
        'logs' => [
            'enabled' => true,
            'max_size_mb' => 2,
            'rotation_count' => 3,
            'directory' => '/logs/'
        ],
        'api_keys' => [
            'prim_api_key' => '',  // Ç? remplir via l'interface
            'proxy_secret' => ''   // Ç? remplir via l'interface
        ],
        'http' => [
            'connect_timeout' => 2,  // secondes
            'total_timeout' => 6,    // secondes
            'user_agent' => 'HallProxy/1.0 (+raspberry.local)',
            'force_ipv4' => true,
            'ssl_verify' => true
        ]
    ],

    // ============================================
    // 8. AFFICHAGE & UI
    // ============================================
    'display' => [
        'viewport' => [
            'min_width' => 1024,
            'breakpoint_tablet' => 768,
            'breakpoint_mobile' => 480
        ],
        'debug_mode' => false,
        'easter_eggs' => true,
        'animations' => [
            'enabled' => true,
            'transition_duration' => 300  // ms
        ],
        'sections_order' => [
            'datetime',
            'transport',
            'weather',
            'announcements'
        ],
        'sections_visibility' => [
            'datetime' => true,
            'bus' => true,
            'rer' => true,
            'weather' => true,
            'announcements' => true
        ]
    ],

     // ============================================
    // 9. SÇ%CURITÇ%
    // ============================================
    'security' => [
        // Authentification (anciens paramÇùtres conservÇ¸s pour compatibilitÇ¸)
        'admin_username' => '',  // ObsolÇùte avec systÇùme users
        'admin_password' => '',  // ObsolÇùte avec systÇùme users
        'session_lifetime' => 3600,  // secondes
        'csrf_enabled' => true,
        'ip_whitelist' => [],  // Optionnel

        // Limites de connexion
        'max_login_attempts' => 5,
        'lockout_duration' => 900,  // 15 minutes en secondes

        // Reset password
        'max_reset_attempts' => 5,  // Nombre max de demandes de reset par user/IP
        'reset_lockout_duration' => 1200,  // 20 minutes en secondes
        'reset_token_validity' => 1200,  // 20 minutes en secondes

        // Email configuration
        'email_method' => 'php',  // 'php' ou 'smtp'
        'smtp_config' => [
            'host' => '',  // ex: smtp.gmail.com
            'port' => 587,  // 587 (TLS) ou 465 (SSL)
            'secure' => 'tls',  // 'tls' ou 'ssl'
            'username' => '',
            'password' => '',
            'from_email' => '',  // Email expÇ¸diteur
            'from_name' => 'Accueil Dashboard'
        ],

        // Logs
        'log_auth_attempts' => true,  // Logger toutes les tentatives de connexion
        'log_file' => '/logs/auth.log'
    ],
        // ============================================
    // 10. HOT RELOAD FRONT
    // ============================================
    'hot_reload' => [
        'enabled'           => true,  // activÇ¸ par dÇ¸faut
        'poll_interval_sec' => 15,    // 15s par dÇ¸faut
    ],
    // ============================================
    // 11. THÇ^MES (rÇ¸fÇ¸rence au systÇùme existant)
    // ============================================
    'theme' => [
        'active_theme' => 'default',
        'preview_mode' => false
    ]
];

// Cache mÇ¸moire pour Ç¸viter de relire le JSON Çÿ chaque appel dans une mÇ¦me requÇ¦te
if (!isset($GLOBALS['HYPERADMIN_CACHE'])) {
    $GLOBALS['HYPERADMIN_CACHE'] = [
        'data' => null,
        'mtime' => null,
    ];
}

/**
 * Met Çÿ jour le cache local de la configuration.
 */
function hyperadmin_refresh_cache(array $config): void {
    $file = HYPERADMIN_CONFIG_FILE;
    clearstatcache(true, $file);
    $GLOBALS['HYPERADMIN_CACHE'] = [
        'data' => $config,
        'mtime' => is_file($file) ? (int)filemtime($file) : time(),
    ];
}

/**
 * Charger la configuration depuis le fichier JSON
 * Merge avec les valeurs par dÇ¸faut pour assurer la complÇ¸tude
 */
function loadHyperConfig(bool $forceReload = false) {
    global $HYPERADMIN_DEFAULTS;

    $file = HYPERADMIN_CONFIG_FILE;
    clearstatcache(true, $file);
    $currentMtime = is_file($file) ? (int)filemtime($file) : null;

    if (
        !$forceReload &&
        isset($GLOBALS['HYPERADMIN_CACHE']['data'], $GLOBALS['HYPERADMIN_CACHE']['mtime']) &&
        $GLOBALS['HYPERADMIN_CACHE']['mtime'] === $currentMtime
    ) {
        return $GLOBALS['HYPERADMIN_CACHE']['data'];
    }

    if (!is_file($file)) {
        $dir = dirname($file);
        if (!is_dir($dir)) {
            @mkdir($dir, 0775, true);
        }

        saveHyperConfig($HYPERADMIN_DEFAULTS, false);
        hyperadmin_refresh_cache($HYPERADMIN_DEFAULTS);
        return $HYPERADMIN_DEFAULTS;
    }

    $json = file_get_contents($file);
    $config = json_decode($json, true);

    if (json_last_error() !== JSON_ERROR_NONE || !is_array($config)) {
        error_log("HYPER ADMIN: Erreur JSON - " . json_last_error_msg());
        hyperadmin_refresh_cache($HYPERADMIN_DEFAULTS);
        return $HYPERADMIN_DEFAULTS;
    }

    $merged = array_replace_recursive($HYPERADMIN_DEFAULTS, $config);

    if (isset($config['traffic_alerts']['trigger_keywords'])) {
        $merged['traffic_alerts']['trigger_keywords'] = $config['traffic_alerts']['trigger_keywords'];
    }

    if (isset($config['traffic_alerts']['severity_levels'])) {
        $merged['traffic_alerts']['severity_levels'] = $config['traffic_alerts']['severity_levels'];
    }

    hyperadmin_refresh_cache($merged);
    return $merged;
}

/**
 * Normalise rÇ¸cursivement toutes les valeurs boolÇ¸ennes
 * Convertit 1, "1", 0, "0", "true", "false" en vrais boolÇ¸ens true/false
 *
 * @param mixed $data DonnÇ¸es Çÿ normaliser
 * @return mixed DonnÇ¸es normalisÇ¸es
 */
function normalizeBooleans($data) {
    if (is_array($data)) {
        foreach ($data as $key => $value) {
            $data[$key] = normalizeBooleans($value);
        }
        return $data;
    }

    // Convertir les valeurs boolÇ¸ennes-like en vrais boolÇ¸ens
    if ($data === 1 || $data === "1" || $data === "true") {
        return true;
    }
    if ($data === 0 || $data === "0" || $data === "false") {
        return false;
    }

    return $data;
}

/**
 * Sauvegarder la configuration
 * Avec backup automatique de l'ancienne version
 */
function saveHyperConfig($config, $createBackup = true) {
    $config = normalizeBooleans($config);

    if ($createBackup && file_exists(HYPERADMIN_CONFIG_FILE)) {
        $backupDir = HYPERADMIN_BACKUP_DIR;
        if (!is_dir($backupDir)) {
            @mkdir($backupDir, 0775, true);
        }

        $backupFile = $backupDir . 'hyperadmin_' . date('Y-m-d_H-i-s') . '.json';
        @copy(HYPERADMIN_CONFIG_FILE, $backupFile);

        $backups = glob($backupDir . 'hyperadmin_*.json');
        if (count($backups) > 10) {
            usort($backups, function($a, $b) {
                return filemtime($a) - filemtime($b);
            });
            $toDelete = array_slice($backups, 0, count($backups) - 10);
            foreach ($toDelete as $file) {
                @unlink($file);
            }
        }
    }

    $json = json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
    $result = file_put_contents(HYPERADMIN_CONFIG_FILE, $json);
    if ($result !== false) {
        hyperadmin_refresh_cache($config);
        return true;
    }
    return false;
}

/**
 * Obtenir une valeur de configuration par son chemin
 * Ex: getHyperConfigValue('weather.location.name')
 */
function getHyperConfigValue($path, $default = null) {
    $config = loadHyperConfig();
    $keys = explode('.', $path);

    $value = $config;
    foreach ($keys as $key) {
        if (isset($value[$key])) {
            $value = $value[$key];
        } else {
            return $default;
        }
    }

    return $value;
}

/**
 * DÇ¸finir une valeur de configuration par son chemin
 */
function setHyperConfigValue($path, $value) {
    $config = loadHyperConfig();
    $keys = explode('.', $path);

    $ref = &$config;
    foreach ($keys as $key) {
        if (!isset($ref[$key])) {
            $ref[$key] = [];
        }
        $ref = &$ref[$key];
    }
    $ref = $value;

    return saveHyperConfig($config);
}

/**
 * Valider une section de configuration
 */
function validateConfigSection($section, $data) {
    // Validation spÇ¸cifique par section
    switch($section) {
        case 'datetime':
            if (!in_array($data['timezone'], timezone_identifiers_list())) {
                return ['error' => 'Fuseau horaire invalide'];
            }
            break;

        case 'weather':
            if (!is_numeric($data['location']['latitude']) ||
                !is_numeric($data['location']['longitude'])) {
                return ['error' => 'CoordonnÇ¸es GPS invalides'];
            }
            break;

        case 'security':
            if (strlen($data['admin_username']) < 3) {
                return ['error' => 'Nom d\'utilisateur trop court (min 3 caractÇùres)'];
            }
            break;
    }

    return ['success' => true];
}

/**
 * Export de la configuration pour le JavaScript
 */
function exportConfigForJS() {
    $config = loadHyperConfig();

    // Filtrer les donnÇ¸es sensibles
    unset($config['security']['admin_password']);
    unset($config['system']['api_keys']);

    return json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
}


**Reviewing key admin files**


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:
<?php
// === Config module annonces ===

// Chemin vers le JSON (assure-toi que /data est inscriptible par PHP)
$ANN_PATH = __DIR__ . '/data/annonces.json';

// 4 couleurs "light" prÇ¸dÇ¸finies (classes CSS)
$ANN_COLORS = [
'neutral', // fond discret par dÇ¸faut
'info', // bleu clair
'warn', // jaune clair
'emph', // orange/rose clair
];

// Nombre dƒ?Tannonces visibles en front
$ANN_FRONT_LIMIT = 4;

// Tri de la liste en admin (true = tri actuel par pinned/date ; false = ordre de crÇ¸ation)
$ANN_SORT_LIST = false; // mets true si tu veux conserver le tri existant dans l'admin

// Identifiants Admin (simple login sans DB)
$ANN_ADMIN_USER = getenv('ANN_ADMIN_USER') ?: 'Nico';
$ANN_ADMIN_PASS = getenv('ANN_ADMIN_PASS') ?: 'bosse';

// Token anti-CSRF (gÇ¸nÇ¸rÇ¸ si absent)
if (session_status() === PHP_SESSION_NONE) session_start();
if (empty($_SESSION['ANN_CSRF'])) {
$_SESSION['ANN_CSRF'] = bin2hex(random_bytes(16));
}


**Identifying encoding and sanitization issues**