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>


ok, on reprends le boulot fait dans 019ab576-2394-7cf1-9bb5-7415220df642
tu repars du fichier todo_logs.md
ok pour toi ?


Summary: auto


**Preparing to read todo_logs.md**


Output:
Exit code: 1
Wall time: 1.3 seconds
Output:
Get-Content : Impossible de trouver le chemin d'accŠs ®ÿC:\wamp64\www\cssp2AI\todo_logs.mdÿ¯, car il n'existe pas.
Au caractŠre Ligne:1 : 1
+ Get-Content -Raw todo_logs.md
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\wamp64\www\cssp2AI\todo_logs.md:String) [Get-Content], ItemNotFoundE
   xception
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand


**Listing files to locate target**


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

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

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
d-----        24/11/2025     14:25                admin
d-----        24/11/2025     14:25                assets
d-----        24/11/2025     14:25                cache
d-----        24/11/2025     16:44                out
d-----        24/11/2025     14:25                outils
-a----        21/11/2025     15:02           9983 AGENTS.md
-a----        24/11/2025     14:25           5050 hyperadmin_integration.php
-a----        24/11/2025     14:25          21592 index.php
-a----        24/11/2025     15:28           2237 notes.md
-a----        24/11/2025     14:25         128840 ratp_debug.log
-a----        24/11/2025     14:25        2097204 ratp_debug.log.1
-a----        24/11/2025     14:25        2097322 ratp_debug.log.2
-a----        24/11/2025     14:25        2097358 ratp_debug.log.3
-a----        24/11/2025     14:25          10482 ratp_proxy.php


**Searching for todo_logs files**


Get-ChildItem -Recurse -Filter todo_logs.md


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

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

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        24/11/2025     16:46           2098 todo_logs.md


**Reading file path**


Output:
Exit code: 0
Wall time: 0.8 seconds
Output:
# TODO Reprise (systÇùme de logs / encodage)

## Ç%tat actuel
- Encodage : plusieurs fichiers en CP-1252/corrompus (glyphes ƒ?o’'ƒ?Ýƒ??, ƒ?oÇ?Yƒ?Ýƒ??). Patchs fragiles.
- Logger central prÇ¸sent dans `out/admin/helpers.php` (`admin_log_action`, `admin_get_logs`, actions.log limitÇ¸ Çÿ 300 lignes).
- `out/admin/auth.php` : log login/logout ajoutÇ¸.
- `out/admin/hyperadmin.php` : log des sauvegardes de section ajoutÇ¸.
- MÇ¸tÇ¸o rainline : flag `weather.display.rainline` prÇ¸sent dans config et formulaire Hyper Admin ; front supprime `.meteo-rainline` si false ; `out/admin/data/hyperadmin.json` contient rainline.
- Risque dƒ?Tinsertions partielles dans `out/admin/index.php` (tentative de log sur toggle : une ligne ¶® admin_log_action ƒ?Ý ['id' => , 'field' => , 'value' => ] ¶¯ peut subsister).

## Ç? faire (proprement, aprÇùs nettoyage/UTF-8)
1. Normaliser lƒ?Tencodage UTF-8 de `out/admin/*.php` et `out/assets/js/front.js` pour Ç¸viter les glyphes corrompus.
2. RÇ¸appliquer les logs de faÇõon propre :
   - `auth.php` : login/logout OK Çÿ vÇ¸rifier.
   - `index.php` (annonces) : log `annonce_save`, `annonce_archive`, `annonce_delete`, `annonce_toggle`, `layout_template` (quick_set_template) ; corriger la ligne partielle.
   - `ui_admin.php` : log sauvegarde UI (template/gap).
   - `themes_admin.php` / `themes_unified_admin.php` : log activation de thÇùme/sauvegardes.
   - `hyperadmin.php` : log save_section dÇ¸jÇÿ prÇ¸sent.
3. Ajouter lƒ?Taffichage des logs (300 derniÇùres entrÇ¸es) dans lƒ?Tonglet SÇ¸curitÇ¸ de lƒ?THyper Admin (lecture via `admin_get_logs()`).
4. VÇ¸rifier rainline cÇïtÇ¸ front aprÇùs conversion UTF-8 : le bandeau doit disparaÇ©tre si `weather.display.rainline` = false.

## Fichiers sensibles Çÿ vÇ¸rifier/corriger
- `out/admin/index.php` (logs partiels), `out/admin/themes_admin.php`, `out/admin/themes_unified_admin.php`, `out/admin/ui_admin.php`, `out/admin/auth.php`, `out/admin/hyperadmin.php`, `out/assets/js/front.js`.
- `out/admin/data/hyperadmin.json` : conserver rainline + sync avec config.

codex resume 019ab576-2394-7cf1-9bb5-7415220df642


**Confirming next steps and environment limits**


Vu. J’ai relu `out/todo_logs.md` : encodage CP-1252 à normaliser, remettre les logs propres (auth ok à vérifier, annonces CRUD/toggle/layout, UI, thèmes, hyperadmin déjà partiel), afficher les 300 derniers logs dans l’onglet Sécurité Hyper Admin, et vérifier le flag rainline côté front après conversion UTF-8.

Je suis prêt à enchaîner à partir de cette liste. Le répertoire est en lecture seule ici ; si tu veux que je applique les corrections, il faudra m’autoriser l’écriture. On commence ?


ok, t'es autorisé à écrire dans /out


**Preparing for escalated write operations**


Output:
Exit code: 0
Wall time: 0.7 seconds
Output:
<?php
/**
 * helpers.php - Fonctions utilitaires communes
 * Contient les fonctions partagÇ¸es entre les diffÇ¸rents modules
 */

/**
 * Ç%chapper les caractÇùres HTML
 * @param string $s
 * @return string
 */
function h($s) {
    return htmlspecialchars((string)$s, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8');
}

/**
 * Lit un fichier JSON et retourne un tableau
 * @param string $path Chemin du fichier
 * @param array $fallback Valeur par dÇ¸faut si le fichier n'existe pas
 * @param bool $merge Si true, fusionne avec fallback. Si false, retourne les donnÇ¸es brutes
 * @return array
 */
function read_json($path, $fallback = [], $merge = true) {
    if (!is_file($path)) {
        return $fallback;
    }

    $content = file_get_contents($path);
    if ($content === false) {
        return $fallback;
    }

    $data = json_decode($content, true);
    if (!is_array($data)) {
        return $fallback;
    }

    // Si pas de fusion, retourner les donnÇ¸es telles quelles
    if (!$merge) {
        return $data;
    }

    // Sinon, fusionner avec le fallback
    return array_replace_recursive($fallback, $data);
}

/**
 * Ç%crit des donnÇ¸es dans un fichier JSON
 * @param string $path Chemin du fichier
 * @param mixed $data DonnÇ¸es Çÿ encoder
 * @param bool $backup CrÇ¸er une sauvegarde avant Ç¸criture
 * @return bool|int
 */
function write_json($path, $data, $backup = true) {
    $dir = dirname($path);

    // CrÇ¸er le rÇ¸pertoire si nÇ¸cessaire
    if (!is_dir($dir)) {
        @mkdir($dir, 0775, true);
    }

    // Sauvegarde si demandÇ¸e et si le fichier existe
    if ($backup && file_exists($path)) {
        @copy($path, $path . '.bak');
    }

    // Ç%criture avec verrouillage
    $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
    return @file_put_contents($path, json_encode($data, $flags), LOCK_EX);
}

/**
 * Convertit une date franÇõaise (jj/mm/aaaa) en timestamp
 * @param string|null $dateStr
 * @return int
 */
function date_fr_to_timestamp($dateStr) {
    $dateStr = trim((string)$dateStr);
    if (!preg_match('~^(\d{2})/(\d{2})/(\d{4})$~', $dateStr, $matches)) {
        return 0;
    }

    list(, $day, $month, $year) = $matches;
    return @mktime(12, 0, 0, (int)$month, (int)$day, (int)$year) ?: 0;
}

/**
 * Convertit une date ISO (yyyy-mm-dd) en format franÇõais (jj/mm/aaaa)
 * @param string $isoDate
 * @return string
 */
function date_iso_to_fr($isoDate) {
    $matches = [];
    if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $isoDate, $matches)) {
        return $matches[3] . '/' . $matches[2] . '/' . $matches[1];
    }
    return '';
}

/**
 * Convertit une date franÇõaise (jj/mm/aaaa) en format ISO (yyyy-mm-dd)
 * @param string $frDate
 * @return string
 */
function date_fr_to_iso($frDate) {
    $matches = [];
    if (preg_match('/^(\d{2})\/(\d{2})\/(\d{4})$/', $frDate, $matches)) {
        return $matches[3] . '-' . $matches[2] . '-' . $matches[1];
    }
    return '';
}

/**
 * Normalise une valeur boolÇ¸enne depuis diffÇ¸rentes reprÇ¸sentations
 * @param mixed $value
 * @return bool
 */
function to_bool($value) {
    return $value === true
        || $value === 1
        || $value === '1'
        || $value === 'true'
        || $value === 'on'
        || $value === 'yes';
}

/**
 * GÇ¸nÇùre un ID unique
 * @param int $length Longueur en octets (sera doublÇ¸e en hexa)
 * @return string
 */
function generate_id($length = 6) {
    return bin2hex(random_bytes($length));
}

/**
 * Nettoie et valide une clÇ¸ alphanumÇ¸rique
 * @param string $key
 * @param int $minLength
 * @param int $maxLength
 * @return string|false
 */
function validate_key($key, $minLength = 3, $maxLength = 24) {
    $key = trim((string)$key);

    if (strlen($key) < $minLength || strlen($key) > $maxLength) {
        return false;
    }

    if (!preg_match('/^[a-zA-Z0-9_-]+$/', $key)) {
        return false;
    }

    return $key;
}

/**
 * Normalise une couleur hexadÇ¸cimale
 * @param string $color
 * @return string Couleur au format #RRGGBB ou chaÇ©ne vide si invalide
 */
function normalize_hex_color($color) {
    $color = strtoupper(trim((string)$color));

    if ($color === '') {
        return '';
    }

    // Ajouter # si manquant
    if ($color[0] !== '#') {
        $color = '#' . $color;
    }

    // Format court (#RGB)
    if (preg_match('/^#[0-9A-F]{3}$/', $color)) {
        return '#' . $color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3];
    }

    // Format long (#RRGGBB)
    if (preg_match('/^#[0-9A-F]{6}$/', $color)) {
        return $color;
    }

    return '';
}

/**
 * Limite une chaÇ©ne Çÿ un nombre de caractÇùres
 * @param string $str
 * @param int $length
 * @param string $suffix
 * @return string
 */
function truncate($str, $length = 100, $suffix = '...') {
    $str = (string)$str;
    if (mb_strlen($str) <= $length) {
        return $str;
    }
    return mb_substr($str, 0, $length - mb_strlen($suffix)) . $suffix;
}

/**
 * Valide et nettoie une date depuis un input HTML5 date
 * @param string $dateValue Valeur de l'input (ISO ou FR)
 * @return string Date au format FR (jj/mm/aaaa) ou chaÇ©ne vide
 */
function validate_date_input($dateValue) {
    $dateValue = trim((string)$dateValue);

    if ($dateValue === '') {
        return '';
    }

    // Si format ISO (yyyy-mm-dd) depuis input type="date"
    if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateValue)) {
        $timestamp = strtotime($dateValue);
        if ($timestamp !== false) {
            return date('d/m/Y', $timestamp);
        }
    }

    // Si dÇ¸jÇÿ au format FR
    if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $dateValue)) {
        return $dateValue;
    }

    return '';
}

/**
 * CrÇ¸e un message flash pour affichage
 * @param string $message
 * @param string $type success|error|warning|info
 * @return array
 */
function create_flash($message, $type = 'success') {
    return [
        'message' => $message,
        'type' => $type,
        'timestamp' => time()
    ];
}

/**
 * Affiche un message flash HTML
 * @param string $message
 * @param string $type
 * @return string
 */
function display_flash($message, $type = 'success') {
    $classes = [
        'success' => 'ok',
        'error' => 'error',
        'warning' => 'warning',
        'info' => 'info'
    ];

    $class = $classes[$type] ?? 'ok';
    $icon = $type === 'success' ? 'ƒo.' : ($type === 'error' ? 'ƒ?O' : 'ƒsÿ‹÷?');

    return sprintf(
        '
%s %s
',
        h($class),
        $icon,
        h($message)
    );
}

/**
 * VÇ¸rifie si une requÇ¦te est AJAX
 * @return bool
 */
function is_ajax_request() {
    return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
        && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
}

/**
 * VÇ¸rifie si une requÇ¦te est en fetch
 * @return bool
 */
function is_fetch_request() {
    return isset($_SERVER['HTTP_X_REQUESTED_WITH'])
        && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'fetch';
}

/**
 * Envoie une rÇ¸ponse JSON et termine le script
 * @param mixed $data
 * @param int $statusCode
 */
function json_response($data, $statusCode = 200) {
    http_response_code($statusCode);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

/**
 * Redirige vers une URL
 * @param string $url
 * @param int $statusCode
 */
function redirect($url, $statusCode = 302) {
    header('Location: ' . $url, true, $statusCode);
    exit;
}

/**
 * Logger centralisÇ¸ pour les actions admin/hyperadmin.
 * Conserve les 300 derniÇùres lignes dans admin/logs/actions.log
 */
function admin_log_action(string $type, string $message, array $context = []): void {
    $logFile = __DIR__ . '/logs/actions.log';
    $dir = dirname($logFile);
    if (!is_dir($dir)) {
        @mkdir($dir, 0775, true);
    }
    $entry = [
        'ts'   => date('c'),
        'user' => $_SESSION['user_login'] ?? 'invitÇ¸',
        'role' => $_SESSION['user_role'] ?? 'guest',
        'ip'   => $_SERVER['REMOTE_ADDR'] ?? '',
        'type' => $type,
        'msg'  => $message,
        'ctx'  => $context,
    ];
    $line = json_encode($entry, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    @file_put_contents($logFile, $line . PHP_EOL, FILE_APPEND | LOCK_EX);

    $lines = @file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
    if (is_array($lines) && count($lines) > 300) {
        $lines = array_slice($lines, -300);
        @file_put_contents($logFile, implode(PHP_EOL, $lines) . PHP_EOL, LOCK_EX);
    }
}

/**
 * RÇ¸cupÇùre les derniÇùres lignes de log (max N)
 */
function admin_get_logs(int $max = 300): array {
    $logFile = __DIR__ . '/logs/actions.log';
    if (!is_file($logFile)) return [];
    $lines = @file($logFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: [];
    $slice = array_slice($lines, -$max);
    return array_reverse(array_map(function ($line) {
        $d = json_decode($line, true);
        return is_array($d) ? $d : ['ts' => null, 'msg' => $line];
    }, $slice));
}


**Planning encoding fixes**


Output:
Exit code: 0
Wall time: 0.8 seconds
Total output lines: 1318
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));
  const showRainline = isEnabled(getConfig('weather.display.rainline', 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}`;
    if (!showRainline) {
      $root.querySelectorAll('.meteo-rainline').forEach(el => el.remove());
    }
  }

  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)}`, { ti…2703 tokens truncated… '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);
})();


**Planning encoding fixes and log updates**

I'm considering how to handle the file encoding issues by converting relevant files to UTF-8, fixing character problems, and reapplying log changes. I want to inspect key admin-related files carefully for structure and log usage, using tools like `Get-Content` and `rg` to find log action references before making edits. Since editing requires permission and the environment might lack some tools, I’ll plan steps cautiously.


Output:
Exit code: 0
Wall time: 1.3 seconds
Output:
out\admin\auth.php:        if (function_exists('admin_log_action')) {
out\admin\auth.php:            admin_log_action('login', 'Connexion rÃ©ussie', ['user' => $user['login'] ?? $loginOrEmail]);
out\admin\auth.php:    if (function_exists('admin_log_action')) {
out\admin\auth.php:        admin_log_action('logout', 'DÃ©connexion utilisateur', ['user' => $_SESSION['user_login'] ?? null]);
out\todo_logs.md:- Logger central présent dans `out/admin/helpers.php` (`admin_log_action`, `admin_get_logs`, actions.log limité à 300 lignes).
out\todo_logs.md:- Risque d’insertions partielles dans `out/admin/index.php` (tentative de log sur toggle : une ligne « admin_log_action … ['id' => , 'field' => , 'value' => ] » peut subsister).
out\admin\helpers.php:function admin_log_action(string $type, string $message, array $context = []): void {
out\admin\hyperadmin.php:                        if (function_exists('admin_log_action')) {
out\admin\hyperadmin.php:                            admin_log_action('hyper_save', "Section {$section} sauvegardÃ©e", ['section' => $section]);
out\admin\index.php:      if (function_exists('admin_log_action')) {
out\admin\index.php:        admin_log_action('layout_template', 'Template UI modifiÃ©', ['template' => $tpl]);
out\admin\index.php:    if (function_exists('admin_log_action')) {
out\admin\index.php:      admin_log_action('annonce_save', 'Annonce enregistrÃ©e', ['id' => $id, 'title' => $item['title']]);
out\admin\index.php:    if (function_exists('admin_log_action')) {
out\admin\index.php:      admin_log_action('annonce_archive', 'Annonce archivÃ©e', ['id' => $id]);
out\admin\index.php:    if (function_exists('admin_log_action')) {
out\admin\index.php:      admin_log_action('annonce_delete', 'Annonce supprimÃ©e', ['id' => $id]);
out\admin\index.php:    if (function_exists('admin_log_action')) {
out\admin\index.php:      admin_log_action('annonce_toggle', 'Annonce mise à jour', ['id' => , 'field' => , 'value' => ]);
out\admin\index.php:    if (function_exists('admin_log_action')) {
out\admin\index.php:      admin_log_action('annonce_toggle', 'Annonce mise Ã  jour', ['id' => $id, 'field' => $field, 'value' => $val]);


**Addressing encoding and logging issues**

I'm examining encoding problems in admin UI files, suspecting CP1252 characters causing weird symbols. I'll consider converting files to UTF-8 and decide whether to replace original icon characters with ASCII or Unicode escapes to maintain readability and functionality, while also checking for missing logging in admin components.


**Investigating encoding issues**


Output:
Exit code: 0
Wall time: 0.8 seconds
Total output lines: 1219
Output:
<?php
/**
 * annonces_admin.php - Administration des annonces
 * Gestion CRUD des annonces avec interface d'administration
 */
require_once __DIR__.'/config_annonces.php';
require_once __DIR__.'/annonces_theme.php';
require_once __DIR__.'/auth.php';
require_once __DIR__.'/helpers.php';
// VÇ¸rification de l'authentification
requireAuth('Admin annonces');
// --- DÇ¸finition des chemins et options ---
$UI_PATH = __DIR__.'/data/ui.json';
$tpl_opts = [
  'grid-2x2'     => '2 Ç- 2',
  'grid-3plus3' => '3 plus 3',
  'grid-1plus2' => '1 plein + 2',
  'grid-2plus1' => '2 + 1 plein',
  'grid-1plus3' => '1 plein + 3',
  'grid-3plus1' => '3 + 1 plein',
  'grid-full'   => 'Plein Ç¸cran',
];
/* ------------------------------
   Fonctions spÇ¸cifiques annonces
--------------------------------*/
/**
 * Lit toutes les annonces
 */
function ann__read_all($path) {
  return read_json($path, []);
}
/**
 * Validation / normalisation d'un tableau d'annonces
 */
function ann__validate_all($arr, array $theme) {
  if (!is_array($arr)) return [];
  $out = [];

  foreach ($arr as $a) {
    if (!is_array($a)) continue;
    $item = [];
    $item['id']           = (string)($a['id'] ?? generate_id());
    $item['title']        = trim((string)($a['title'] ?? ''));
    $item['text']         = (string)($a['text'] ?? '');
    $item['date_visible'] = trim((string)($a['date_visible'] ?? ''));

    if ($item['date_visible'] && !preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $item['date_visible'])) {
      $item['date_visible'] = '';
    }
    $color = $a['color'] ?? 'neutral';
    $allowed = array_keys($theme);
    $item['color'] = in_array($color, $allowed, true) ? $color : 'neutral';
    $item['pinned']    = to_bool($a['pinned'] ?? false);
    $item['active']    = to_bool($a['active'] ?? true);
    $item['show_date'] = to_bool($a['show_date'] ?? true);
    $created = $a['created_at'] ?? null;
    $updated = $a['updated_at'] ?? null;
    $item['created_at'] = $created ?: date('c');
    $item['updated_at'] = $updated ?: $item['created_at'];
    $out[] = $item;
  }
  return $out;
}
/**
 * Ç%crit toutes les annonces
 */
function ann__write_all($path, $arr) {
  global $ANN_THEME;

  $dir = dirname($path);
  if (!is_dir($dir)) @mkdir($dir, 0775, true);
  // Validation minimale + normalisation
  $arr = ann__validate_all($arr, $ANN_THEME);
  // Sauvegarde .bak avant Ç¸criture
  if (file_exists($path)) {
    @copy($path, $path . '.bak');
  }
  // Ç%criture atomique avec lock
  $fp = fopen($path, 'c+');
  if (!$fp) throw new RuntimeException('Impossible d\'ouvrir le fichier.');
  if (!flock($fp, LOCK_EX)) {
    fclose($fp);
    throw new RuntimeException('Lock impossible.');
  }
  ftruncate($fp, 0);
  fwrite($fp, json_encode(array_values($arr), JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE));
  fflush($fp);
  flock($fp, LOCK_UN);
  fclose($fp);
}
// --- Charger les annonces et migration des boolÇ¸ens ---
$all = ann__read_all($ANN_PATH);
$all = ann__validate_all($all, $ANN_THEME);
$dirty = false;
foreach ($all as &$a) {
  $p = $a['pinned'] ?? false;
  $n = to_bool($p);
  if ($p !== $n) { $a['pinned'] = $n; $dirty = true; }
  $ac = $a['active'] ?? true;
  $an = to_bool($ac);
  if ($ac !== $an) { $a['active'] = $an; $dirty = true; }
  $sd = $a['show_date'] ?? true;
  $sn = to_bool($sd);
  if ($sd !== $sn) { $a['show_date'] = $sn; $dirty = true; }
}
unset($a);
if ($dirty) {
  ann__write_all($ANN_PATH, $all);
}
/* ------------------------------
   Actions CRUD + Toggle inline
--------------------------------*/
$action = $_POST['action'] ?? '';
if ($action && verifyAnnCsrf($_POST['csrf'] ?? '')) {
  // Changement rapide de template depuis cette page
  if ($action === 'quick_set_template') {
    $tpl = trim((string)($_POST['template'] ?? 'grid-2x2'));
    $allowed = ['grid-2x2','grid-3plus3','grid-1plus2','grid-2plus1','grid-1plus3','grid-3plus1','grid-full'];

    // DÇ¸tection requÇ¦te AJAX - vÇ¸rification multiple
    $isAjax = false;
    if (isset($_SERVER['HTTP_X_REQUESTED_WITH'])) {
        $xrw = strtolower($_SERVER['HTTP_X_REQUESTED_WITH']);
        if ($xrw === 'xmlhttprequest' || $xrw === 'fetch') {
            $isAjax = true;
        }
    }
    // VÇ¸rification supplÇ¸mentaire via Accept header
    if (!$isAjax && isset($_SERVER['HTTP_ACCEPT'])) {
        if (strpos($_SERVER['HTTP_ACCEPT'], 'application/json') !== false) {
            $isAjax = true;
        }
    }

    if (in_array($tpl, $allowed, true)) {
      // IMPORTANT : Lire le fichier existant SANS fusion avec les defaults
      $ui_data = [];
      if (is_file($UI_PATH)) {
        $raw = file_get_contents($UI_PATH);
        $ui_data = json_decode($raw, true);
        if (!is_array($ui_data)) {
          $ui_data = [];
        }
      }

      // Si le fichier est vide ou invalide, utiliser la structure par dÇ¸faut
      if (empty($ui_data)) {
        $ui_data = [
          'annonces_layout' => [
            'template'    => 'grid-2x2',
            'gap'         => 16,
            'order'       => [],
            'takeover_id' => null,
            'rowfull_id'  => null,
          ],
          'annonces_defaults' => [
            'maxlines' => 5,
            'speed'    => 30,
            'pause'    => 1500
          ],
          'annonces_overrides' => []
        ];
      }

      // S'assurer que la structure annonces_layout existe
      if (!isset($ui_data['annonces_layout'])) {
        $ui_data['annonces_layout'] = [
          'template'    => 'grid-2x2',
          'gap'         => 16,
          'order'       => [],
          'takeover_id' => null,
          'rowfull_id'  => null,
        ];
      }

      // Mettre Çÿ jour SEULEMENT le template
      $ui_data['annonces_layout']['template'] = $tpl;

      // Sauvegarder en prÇ¸servant toute la structure
      $ok = @file_put_contents($UI_PATH, json_encode($ui_data, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES), LOCK_EX);
      if (function_exists('admin_log_action')) {
        admin_log_action('layout_template', 'Template UI modifiÇ¸', ['template' => $tpl]);
      }

      if ($isAjax) {
        // RÇ¸ponse AJAX - pas de redirection !
        header('Content-Type: text/plain; charset=utf-8');
        http_response_code($ok !== false ? 200 : 500);
        echo $ok !== false ? 'OK' : 'ERROR';
        exit;
      } else {
        // Redirection normale (si JavaScript dÇ¸sactivÇ¸)
        if ($ok !== false) {
          redirect($_SERVER['PHP_SELF'] . '?template_changed=1');
        } else {
          redirect($_SERVER['PHP_SELF'] . '?template_error=1');
        }
      }
    } else {
      // Template invalide
      if ($isAjax) {
        header('Content-Type: text/plain; charset=utf-8');
        http_response_code(400);
        echo 'INVALID_TEMPLATE';
        exit;
      } else {
        redirect($_SERVER['PHP_SELF'] . '?template_error=1');
      }
    }
  }
  if ($action === 'save') {
    $id = $_POST['id'] ?: generate_id();
    $item = [
      'id'           => $id,
      'title'        => trim($_POST['title'] ?? ''),
      'text'         => trim($_POST['text'] ?? ''),
      'date_visible' => validate_date_input($_POST['date_visible'] ?? ''),
      'color'        => array_key_exists($_POST['color'] ?? 'neutral', $ANN_THEME)
                          ? ($_POST['color'] ?? 'neutral')
                          : 'neutral',
      'pinned'       => to_bool($_POST['pinned'] ?? false),
      'active'       => to_bool($_POST['active'] ?? true),
      'show_date'    => to_bool($_POST['show_date'] ?? false), // dÇ¸faut false si checkbox non cochÇ¸e
      'updated_at'   => date('c'),
    ];
    // created_at si nouveau
    $found = false;
    foreach ($all as &$a) {
      if (($a['id'] ?? null) === $id) {
        $found = true;
        $item['created_at'] = $a['created_at'] ?? date('c');
        $a = array_merge($a, $item);
        break;
      }
    }
    unset($a);

    if (!$found) {
      $item['created_at'] = date('c');
      $all[] = $item;
    }
    ann__write_all($ANN_PATH, $all);
    if (function_exists('admin_log_action')) {
      admin_log_action('annonce_save', 'Annonce enregistrÇ¸e', ['id' => $id, 'title' => $item['title']]);
    }
    redirect($_SERVER['PHP_SELF'] . '?ok=1#annoncelist');
  }
  if ($action === 'archive') {
    $id = $_POST['id'] ?? '';
    foreach ($all as &$a) {
      if (($a['id'] ?? null) === $id) {
        $a['active'] = false;
        $a['updated_at'] = date('c');
        break;
      }
    }
    unset($a);
    ann__write_all($ANN_PATH, $all);
    if (function_exists('admin_log_action')) {
      admin_log_action('annonce_archive', 'Annonce archivÇ¸e', ['id' => $id]);
    }
    redirect($_SERVER['PHP_SELF'] . '?arch=1');
  }
  if ($action === 'delete') {
    $id = $_POST['id'] ?? '';
    $all = array_values(array_filter($all, fn($a) => ($a['id'] ?? null) !== $id));
    ann__write_all($ANN_PATH, $all);
    if (function_exists('admin_log_action')) {
      admin_log_action('annonce_delete', 'Annonce supprimÇ¸e', ['id' => $id]);
    }
    redirect($_SERVER['PHP_SELF'] . '?del=1');
  }
  // Toggle inline (pinned / active / show_date)
  if ($action === 'toggle') {
    $id    = $_POST['id'] ?? '';
    $field = $_POST['field'] ?? '';
    $val   = to_bool($_POST['val'] ?? '0');
    foreach ($all as &$a) {
      if (($a['id'] ?? null) === $id) {
        if (in_array($field, ['pinned','active','show_date'], true)) {
          $a[$field] = $val;
          $a['updated_at'] = date('c');
        }
        break;
      }
    }
    if (function_exists('admin_log_action')) {
      admin_log_action('annonce_toggle', 'Annonce mise … jour', ['id' => , 'field' => , 'value' => ]);
    }
    unset($a);

    ann__write_all($ANN_PATH, $all);
    if (function_exists('admin_log_action')) {
      admin_log_action('annonce_toggle', 'Annonce mise Çÿ jour', ['id' => $id, 'field' => $field, 'value' => $val]);
    }
    // DÇ¸tection amÇ¸liorÇ¸e des requÇ¦tes AJAX/fetch
    $isAjax =
        (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
         strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') ||
        (isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
         $_SERVER['HTTP_X_REQUESTED_WITH'] === 'fetch');

    if ($isAjax) {
      header('Content-Type: text/plain; charset=utf-8');
      http_response_code(200);
      echo 'OK';
      exit;
    }

    redirect($_SERVER['PHP_SELF']);
  }
}
// --- Lecture du template actuel (aprÇùs les actions pour avoir la valeur Çÿ jour) ---
$tpl_cur = 'grid-2x2';
if (is_file($UI_PATH)) {
  $ui_data = read_json($UI_PATH);
  if (isset($ui_data['annonces_layout']['template'])) {
    $tpl_cur = $ui_data['annonces_layout']['template'];
  }
}
// --- PrÇ¸paration affichage ---
// Tri par dÇ¸faut
$sort = $_GET['sort'] ?? 'date_desc';
$validSorts = ['date_asc','date_desc','alpha_asc','alpha_desc','created_asc','created_desc'];
if (!in_array($sort, $validSorts, true)) $sort = 'date_desc';
// Copie pour tri
$sorted = $all;
// Application du tri
switch($sort) {
  case 'date_asc':
    usort($sorted, function($a,$b){
      $ta = date_fr_to_timestamp($a['date_visible'] ?? '');
      $tb = date_fr_to_timestamp($b['date_visible'] ?? '');
      if ($ta === $tb) return strcasecmp($a['title'] ?? '', $b['title'] ?? '');
      return $ta <=> $tb;
    });
    break;
  case 'date_desc':
    usort($sorted, function($a,$b){
      $ta = date_fr_to_timestamp($a['date_visible'] ?? '');
      $tb = date_fr_to_timestamp($b['date_visible'] ?? '');
      if ($ta === $tb) return strcasecmp($b['title'] ?? '', $a['title'] ?? '');
      return $tb <=> $ta;
    });
    break;
  case 'alpha_asc':
    usort($sorted, fn($a,$b) => strcasecmp($a['title'] ?? '', $b['title'] ?? ''));
    break;
  case 'alpha_desc':
    usort($sorted, fn($a,$b) => strcasecmp($b['title'] ?? '', $a['title'] ?? ''));
    break;
  case 'created_asc':
    usort($sorted, fn($a,$b) => strcmp($a['created_at'] ?? '', $b['created_at'] ?? ''));
    break;
  case 'created_desc':
    usort($sorted, fn($a,$b) => strcmp($b['created_at'] ?? '', $a['created_at'] ?? ''));
    break;
}
// Pinned en premier
$pinned = array_filter($sorted, fn($a) => !empty($a['pinned']));
$notPinned = array_filter($sorted, fn($a) => empty($a['pinned']));
$sorted = array_merge(array_values($pinned), array_values($notPinned));
// Mode Ç¸dition
$edit = null;
if (isset($_GET['edit'])) {
  $editId = $_GET['edit'];
  foreach ($all as $a) {
    if (($a['id'] ?? null) === $editId) {
      $edit = $a;
      break;
    }
  }
}
// Stats
$countActive = count(array_filter($all, fn($a) => !empty($a['active'])));
$countTotal = count($all);
?><!doctype html>

  <meta charset="utf-8">
  <title>Admin ƒ?" Annonces</title>
    <link rel="icon" type="image/svg+xml" href="assets/img/favicon_admin.svg">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link rel="stylesheet" href="assets/css/admin.css">

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/easymde/dist/easymde.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">

<?php
// GÇ¸nÇ¸rer le CSS des couleurs dynamiquement selon le thÇùme actif
if (isset($ANN_THEME) && is_array($ANN_THEME)):
?>

<?php endif; ?>

<?php
require_once __DIR__.'/header.php';
?>

    <?php if (!empty($_GET['ok'])): ?>
      <?= display_flash('EnregistrÇ¸ avec succÇùs.', 'success') ?>
    <?php endif; ?>
    <?php if (!empty($_GET['arch'])): ?>
      <?= display_flash('Annonce archivÇ¸e.', 'success') ?>
    <?php endif; ?>
    <?php if (!empty($_GET['del'])): ?>
      <?= display_flash('Annonce supprimÇ¸e.', 'success') ?>
    <?php endif; ?>
    <?php if (!empty($_GET['template_changed'])): ?>
      <?= display_flash('Template de grille mis Çÿ jour.', 'success') ?>
    <?php endif; ?>
    <?php if (!empty($_GET['template_error'])): ?>
      <?= display_flash('Erreur lors du changement de template.', 'error') ?>
    <?php endif; ?>

<?= $countActive ?>

Actives

<?= $countTotal ?>

Total

<?= $countTotal - $countActive ?>

ArchivÇ¸es

Liste des annonces

        Trier par :
        <select class="annadmin-select annadmin-input-inline" onchange="location.href='?sort='+this.value">
          <?php
          $sortOptions = [
            'date_desc' => 'Date dÇ¸croissante',
            'date_asc' => 'Date croissante',
            'alpha_asc' => 'Titre AƒÅ'Z',
            'alpha_desc' => 'Titre ZƒÅ'A',
            'created_desc' => 'CrÇ¸ation rÇ¸cente',
            'created_asc' => 'CrÇ¸ation ancienne',
          ];
          foreach ($sortOptions as $value => $label): ?>
            <option value="<?= h($value) ?>" <?= $sort === $value ? 'selected' : '' ?>>
              <?= h($label) ?>
            </option>
          <?php endforeach; ?>
        </select>

        <form method="post" action="<?= $_SERVER['PHP_SELF'] ?>" id="template-form" style="margin-left: auto; display: flex; gap: 8px; align-items: center;">
         …2226 tokens truncated…                               <input type="hidden" name="field" value="pinned">
                                <input type="hidden" name="id" value="<?= h($a['id']) ?>">
                                <input type="hidden" name="val" value="0">
                                <label class="toggle <?= !empty($a['pinned']) ? 'on' : '' ?>">
                                    <input type="checkbox" class="quick-toggle" <?= !empty($a['pinned']) ? 'checked' : '' ?>>

                            </form>

                            ÐY'?
                            <form class="inline-toggle">
                                <input type="hidden" name="csrf" value="<?= h($_SESSION['ANN_CSRF']) ?>">
                                <input type="hidden" name="action" value="toggle">
                                <input type="hidden" name="field" value="active">
                                <input type="hidden" name="id" value="<?= h($a['id']) ?>">
                                <input type="hidden" name="val" value="0">
                                <label class="toggle <?= !empty($a['active']) ? 'on' : '' ?>">
                                    <input type="checkbox" class="quick-toggle" <?= !empty($a['active']) ? 'checked' : '' ?>>

                            </form>

                            ÐY".
                            <form class="inline-toggle">
                                <input type="hidden" name="csrf" value="<?= h($_SESSION['ANN_CSRF']) ?>">
                                <input type="hidden" name="action" value="toggle">
                                <input type="hidden" name="field" value="show_date">
                                <input type="hidden" name="id" value="<?= h($a['id']) ?>">
                                <input type="hidden" name="val" value="0">
                                <label class="toggle <?= !empty($a['show_date']) ? 'on' : '' ?>">
                                    <input type="checkbox" class="quick-toggle" <?= !empty($a['show_date']) ? 'checked' : '' ?>>

                            </form>

                        <?= h($a['title'] ?: '[Sans titre]') ?>

        Date visible
        <?= h($a['date_visible'] ?: 'ƒ?"') ?>

            Style
            <span class="color-chip" data-color="<?= h($a['color'] ?? 'neutral') ?>"
                  style="display: inline-block; width: 28px; height: 28px; border-radius: 6px; border: 1px solid var(--color-border);">

            <a href="?edit=<?= h($a['id']) ?>#editform" class="annadmin-btn annadmin-btn-small" title="Modifier">ƒo?‹÷?
            <?php if (!empty($a['active'])): ?>
                <form method="post" style="display:inline">
                    <input type="hidden" name="csrf" value="<?= h($_SESSION['ANN_CSRF']) ?>">
                    <input type="hidden" name="action" value="archive">
                    <input type="hidden" name="id" value="<?= h($a['id']) ?>">
                    <button type="submit" class="annadmin-btn annadmin-btn-small annadmin-btn-secondary"
                            title="Archiver" onclick="return confirm('Archiver cette annonce ?')">ÐY"Ý</button>
                </form>
            <?php endif; ?>
            <form method="post" style="display:inline">
                <input type="hidden" name="csrf" value="<?= h($_SESSION['ANN_CSRF']) ?>">
                <input type="hidden" name="action" value="delete">
                <input type="hidden" name="id" value="<?= h($a['id']) ?>">
                <button type="submit" class="annadmin-btn annadmin-btn-small annadmin-btn-danger"
                        title="Supprimer" onclick="return confirm('Supprimer dÇ¸finitivement cette annonce ?')">ÐY-'</button>
            </form>

            <?php endforeach; ?>
        <?php endif; ?>

<?= $edit ? 'Modifier l\'annonce' : 'Nouvelle annonce' ?>

      <form method="post">
        <input type="hidden" name="csrf" value="<?= h($_SESSION['ANN_CSRF']) ?>">
        <input type="hidden" name="action" value="save">
        <input type="hidden" name="id" value="<?= h($edit['id'] ?? '') ?>">

          Titre
          <input type="text"
                 name="title"
                 class="annadmin-input"
                 value="<?= h($edit['title'] ?? '') ?>"
                 maxlength="100"
                 placeholder="Titre de l'annonce">

            Date visible
            <input type="date"
                   id="date_visible"
                   name="date_visible"
                   class="annadmin-input"
                   data-initial-value="<?= h($edit['date_visible'] ?? date('d/m/Y')) ?>"
                   value="">

            Couleur
            <input type="hidden" name="color" value="<?= h($edit['color'] ?? 'neutral') ?>">

              <?php foreach ($ANN_THEME as $key => $colors): ?>
                <div class="color-chip"
                     data-color="<?= h($key) ?>"
                     style="background: <?= h($colors['bg']) ?>;
                            border-color: <?= h($colors['border']) ?>;"
                     title="<?= h($key) ?>">

              <?php endforeach; ?>

    ÐY". Afficher la date
    <label class="toggle <?= !empty($edit['show_date']) ? 'on' : '' ?>" style="margin-left: 8px;">
      <input type="checkbox" name="show_date" value="1"
             <?= !empty($edit['show_date']) ? 'checked' : '' ?>>

            ÐY"O Ç%pinglÇ¸
            <label class="toggle <?= !empty($edit['pinned']) ? 'on' : '' ?>" style="margin-left: 8px;">
              <input type="checkbox" name="pinned" value="1"
                     <?= !empty($edit['pinned']) ? 'checked' : '' ?>>

            ÐY'? Active
            <label class="toggle <?= !empty($edit['active']) || !$edit ? 'on' : '' ?>" style="margin-left: 8px;">
              <input type="checkbox" name="active" value="1"
                     <?= !empty($edit['active']) || !$edit ? 'checked' : '' ?>>

          Texte
          <textarea id="ann-text" name="text" class="annadmin-textarea" maxlength="1200" placeholder="Texte de l'annonceƒ?Ý"><?= h($edit['text'] ?? '') ?></textarea>

Couleur du texte

                <button data-color="#000000" style="background-color:#000000"></button>
                <button data-color="#e60000" style="background-color:#e60000"></button>
                <button data-color="#008a00" style="background-color:#008a00"></button>
                <button data-color="#0066cc" style="background-color:#0066cc"></button>
                <button data-color="#9933cc" style="background-color:#9933cc"></button>
                <button data-color="#f39c12" style="background-color:#f39c12"></button>
                <button data-color="#e74c3c" style="background-color:#e74c3c"></button>
                <button data-color="#3498db" style="background-color:#3498db"></button>

Taille du texte

                <button data-size="12px">Petit</button>
                <button data-size="16px">Normal</button>
                <button data-size="20px">Grand</button>
                <button data-size="24px">TrÇùs grand</button>
                <button data-size="32px">Ç%norme</button>

            Formatage Markdown :

            **gras** ƒÅ' gras   |
            *italique* ƒÅ' italique   |
            __soulignÇ¸__ ƒÅ' soulignÇ¸   |
            ~~barrÇ¸~~ ƒÅ' <s>barrÇ¸</s>

            Les retours Çÿ la ligne sont conservÇ¸s.

          <button class="annadmin-btn annadmin-btn-primary" type="submit">
            ÐY'ó <?= $edit ? 'Mettre Çÿ jour' : 'Enregistrer' ?>
          </button>
          <?php if ($edit): ?>
            Annuler
          <?php endif; ?>

      </form>


**Fixing log line and reviewing auth.php**