> For the complete documentation index, see [llms.txt](https://docs.ipcheck.ing/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ipcheck.ing/developer/fr/architecture/frontend.md).

# Frontend

Tout ce qui se trouve sous `frontend/` est une SPA Vue 3 utilisant `<script setup>`, Pinia, vue-router en mode historique HTML5, vue-i18n et Tailwind CSS v4 au-dessus de primitives shadcn-vue copiées. Pas de TypeScript.

`App.vue` est une coquille légère — fournisseur d’infobulles, hôte des notifications toast, invite d’installation PWA, thème, et `<router-view>`. C’est aussi là que les deux pipelines pilotés par événements sont initialisés exactement une seule fois.

## Routage

`frontend/router/index.js` déclare quatre vraies routes et une route fourre-tout :

| Chemin             | Composant               | Notes                                                   |
| ------------------ | ----------------------- | ------------------------------------------------------- |
| `/`                | `Home.vue`              | importé de façon eager — la page d’accueil par défaut   |
| `/tools/:slug`     | `StandaloneTool.vue`    | Page complète pour un outil, partageable et indexable   |
| `/privacy`         | `PrivacyPolicy.vue`     |                                                         |
| `/r/:id`           | `report/ReportPage.vue` | Rapport de diagnostic partagé en lecture seule, noindex |
| `/:pathMatch(.*)*` | —                       | Redirige vers `/`                                       |

Tout sauf `Accueil` est importé en lazy afin de rester hors du bundle de la page d’accueil. `scrollBehavior` n’effectue délibérément **pas** aucun défilement lorsque seule la requête change sur le même chemin — c’est ce que font l’ouverture et la fermeture du tiroir des outils.

Les outils avancés ont un second point d’entrée : sur la page d’accueil, `?tool=<slug>` ouvre le même composant dans un tiroir en bas. Les deux points d’entrée rendent le même `.vue` fichier ; seul l’enveloppe diffère.

## Le registre des outils

`frontend/data/tools.js` est la source unique de vérité pour les outils avancés. Il exporte un tableau ordonné et une carte de recherche :

```js
export const ADVANCED_TOOLS = [
  { slug: 'whois', emoji: '📓', titleKey: 'whois.Title',
    noteKey: 'advancedtools.Whois',
    component: () => import('@/components/advanced-tools/Whois.vue') },
  // …
];

export const TOOL_BY_SLUG = new Map(ADVANCED_TOOLS.map((t) => [t.slug, t]));
```

Champs d’entrée :

| Champ                  | Signification                                                            |
| ---------------------- | ------------------------------------------------------------------------ |
| `slug`                 | Identifiant stable utilisé par les deux `?tool=<slug>` et `/tools/:slug` |
| `emoji`                | Glyphe sur la carte et dans l’en-tête du tiroir                          |
| `titleKey` / `noteKey` | clés i18n pour le titre et la description en une ligne                   |
| `composant`            | Chargement différé `import()` du `.vue` fichier                          |
| `requiresOriginalSite` | Garde facultative ; si elle est omise, l’outil est public                |

Trois consommateurs en dérivent, c’est pourquoi ajouter une entrée est généralement tout le travail :

* **`Advanced.vue`** mappe le tableau dans la grille de cartes (`cartes`), le filtre selon `configs.originalSite` (`enabledCards`), et résout l’outil actif du tiroir avec `TOOL_BY_SLUG.get(route.query.tool)`. Le composant lazy résolu est mis en cache dans un `Map` afin que les re-rendus ne remontent pas un outil déjà en cours d’exécution. Un simple clic gauche sur une carte appelle `router.push({ path: '/', query: { tool } })`; les clics avec modificateur et le clic du milieu passent à travers vers le `<a href>` afin que la page autonome s’ouvre dans un nouvel onglet.
* **`StandaloneTool.vue`** résout `TOOL_BY_SLUG.get(route.params.slug)`, l’enveloppe dans `defineAsyncComponent`, définit pour chaque outil un titre localisé, une description et une URL canonique via `use-document-meta.js`, et redirige vers `/` en cas de slug inconnu. Notez que le routeur lui-même n’a qu’une seule route dynamique — c’est le registre qui effectue la résolution, pas une table de routes générée.
* **`Nav.vue`** liste les mêmes outils dans la navigation, en appliquant le même `requiresOriginalSite` filtre.

{% hint style="info" %}
`requiresOriginalSite: true` masque un outil sur les instances auto-hébergées, car il a besoin de l’API privée IPCheck.ing et d’un utilisateur connecté. Le drapeau est évalué par rapport à `store.configs.originalSite`, que le backend déduit du referer de la requête — voir [Fonctionnalités liées à IPCheck.ing](/developer/fr/configuration/features-tied-to-ipcheck-ing.md).
{% endhint %}

D’autres registres vivent à côté dans `data/`: `sections.js` (les identifiants des sections de la page d’accueil qui pilotent la navigation, le suivi du défilement et l’état de chargement), `ip-databases.js` (les sources de géolocalisation entre lesquelles les utilisateurs peuvent basculer), `achievements.js` et `achievement-rules.js`, `changelog.json`, et `default-preferences.js`.

## store Pinia

`frontend/store.js` définit un seul store, `main`. Il contient un état partagé entre composants plutôt que des détails propres à chaque composant :

* **Session / authentification** — `utilisateur`, `isSignedIn`, `isFireBaseSet`, plus les actions de connexion, déconnexion et écouteur d’authentification.
* **Drapeaux de fonctionnalités du backend** — `configs`, renseigné en fire-and-forget par `fetchConfigs()` depuis `/api/configs`. Les composants le lisent de manière réactive, donc le premier rendu n’attend jamais cet aller-retour. À l’arrivée, les drapeaux déduisent aussi la disponibilité de chaque source de géolocalisation et, si la préférence de source enregistrée n’est plus configurée, la migrent vers la plus proche disponible.
* **Préférences utilisateur** — `userPreferences`, chargé depuis et réécrit dans `localStorage`, avec migration depuis les clés héritées.
* **État de la page** — `mountingStatus` et `loadingStatus` (un drapeau par section depuis `data/sections.js`), `currentSection`, `isMobile`, `isDarkMode`, `openSheet`, le toast `alerte` slot.
* **IP collectées** — `allIPs`, un tableau de `{ ip, country, location, asn, org }` fusionné à partir de plusieurs composants via `updateAllIPs()`; les sources ultérieures complètent les champs laissés vides par les précédentes. Les sélecteurs Globalping et l’enregistreur d’historique IP le lisent.
* **Sources de géolocalisation** — `ipDBs` (depuis `data/ip-databases.js`) avec le `activeSources` getter ; `enabled` est dérivé uniquement de la configuration, jamais basculé par des échecs à l’exécution.
* **Récompenses** — `userAchievements` plus un pipeline de mise à jour à une seule place (`triggerUpdateAchievements` / `achievementToUpdate`) qui `User.vue` observe et signale au backend.

Les objets d’état qui ne doivent pas être partagés entre instances de store sont produits par des fabriques (`createInitialIpDBs()`, `createMountingStatus()`, …) plutôt que par des littéraux au niveau du module.

## Le bus app-events

`frontend/utils/app-events.js` fait environ 30 lignes : une `Map` de nom d’événement vers un `Set` de gestionnaires, `onAppEvent(event, handler)` renvoyant une fonction de désabonnement, et `emitAppEvent(event, payload)`. L’émission est en fire-and-forget et un gestionnaire qui lance une exception est intercepté, donc un abonné défaillant ne peut pas casser l’émetteur. Il n’importe rien de Vue, donc les utilitaires et les modules simples peuvent aussi l’utiliser.

Les composants émettent des événements métier **sans condition** — « le test de vitesse est terminé », « la requête whois a été exécutée » — et restent ignorants de qui écoute. Deux pipelines empruntent le bus, tous deux initialisés une seule fois dans `App.vue`.

```mermaid
flowchart TD
    C["Composants (SpeedTest, Whois, IpInfos, …)"]
    BUS["utils/app-events.js"]
    AE["use-achievement-engine.js"]
    RC["use-report-collector.js"]
    SE["sentry-init.js"]
    ST["Emplacement du store Pinia → User.vue → backend"]
    SN["Instantanés du rapport → boîte de dialogue de partage + /r/:id"]

    C -->|"emitAppEvent('speedtest:finished', {...})"| BUS
    BUS --> AE --> ST
    BUS --> RC --> SN
    BUS --> SE
```

### Moteur des récompenses

`data/achievement-rules.js` mappe les événements vers des slugs de récompense. Chaque règle est `{ event, slug, when? }`, où `when` est un prédicat pur sur la charge utile :

```js
{ event: 'speedtest:finished', slug: 'RapidPace', when: (p) => p.downloadSpeed >= 500 },
```

`composables/use-achievement-engine.js` abonne un écouteur par règle et prend en charge chaque garde transversale : il ignore les événements lorsque le visiteur n’est pas connecté, saute les récompenses déjà débloquées, évalue `when`, puis met le slug en file d’attente. Le store ne gère qu’une récompense à la fois, donc les déblocages simultanés (un seul test de vitesse peut franchir trois seuils) sont envoyés à 2 secondes d’intervalle, et chacun est revérifié au moment de l’envoi au cas où il aurait été débloqué pendant l’attente.

Ajouter une récompense signifie donc : une entrée dans `data/achievements.js`, une règle dans `data/achievement-rules.js`, et un nouvel événement métier seulement si aucun événement approprié n’existe déjà. Les composants ne sont jamais touchés.

### Collecteur de rapports

Le rapport de diagnostic partageable emprunte le même bus. Chaque test « my network » émet `<domain>:finished` avec son résultat structuré complet. `composables/use-report-collector.js` fait passer chaque charge utile par son générateur dans `utils/report-builders.js`, conserve le dernier instantané par section (le dernier l’emporte) et les expose en lecture seule à la boîte de dialogue de partage et à la `/r/:id` page. Les formes de section sont mises sur liste blanche par `common/report-schema.js`, le même module par rapport auquel le backend valide les envois.

{% hint style="warning" %}
Les générateurs échouent en douceur : une valeur inconnue supprime silencieusement le champ. Si vous modifiez la sémantique du résultat d’un test, mettez à jour la liste blanche de son générateur et l’énumération du schéma dans la même modification, sinon le champ disparaîtra discrètement des rapports au lieu de produire une erreur.
{% endhint %}

## Séquence de démarrage

`frontend/main.js` garde le chemin critique court. Il crée l’app, Pinia, i18n et le routeur, puis ne conditionne le premier rendu qu’à trois choses, exécutées en parallèle : l’écouteur d’authentification (**seulement** pour un visiteur dont le `auth-hint` indique qu’il était connecté), `store.loadPreferences()`, et `loadActiveLocaleMessages()`. Tout le reste est en fire-and-forget ou reporté après le montage — `store.fetchConfigs()`, l’analytics, la collection d’icônes de drapeaux (des centaines de KB) et Sentry.

Il enregistre aussi un `vite:preloadError` écouteur à l’évaluation du module : après un déploiement, une page chargée depuis l’ancienne version échouera à importer en lazy un chunk haché qui n’existe plus, donc l’application se recharge une fois (avec un verrou temporel par onglet qui empêche une boucle de rechargement lorsque la vraie cause est un réseau hors ligne).

## Locales à la demande

`frontend/locales/i18n.js` crée l’instance i18n avec **vides** messages et une carte de chargeurs d’imports dynamiques (`en`, `zh`, `fr`, `ru`). Une seule locale est active par chargement de page — changer de langue persiste le choix et redémarre l’application — donc `loadActiveLocaleMessages()` charge la locale active plus l’anglais en solution de secours, et `main.js` l’attend avant le montage. Regrouper les quatre de manière eager coûtait environ 44 KB gzip de poids mort.

Le même principe s’applique aux sous-paquets : les jeux de données security-checklist sous `locales/security-checklist/` sont chargés à la demande par cet outil, pas depuis le chemin de démarrage.

Une fois les messages chargés, `updateMeta()` définit `document.documentElement.lang` (avec `zh` déclaré comme `zh-CN`), le titre de la page, ainsi que les balises meta keywords / description. Les remplacements propres à chaque page viennent de `composables/use-document-meta.js`.

## Initialisation dynamique conditionnée par l’environnement

Deux intégrations optionnelles sont conditionnées à la compilation afin qu’un déploiement auto-hébergé sans elles soit livré **sans** aucun code associé.

* **Sentry** — conditionné par `VITE_SENTRY_DSN_FRONTEND`. Sans le DSN, `sentry-init.js` n’est jamais importé et le SDK n’est pas dans le bundle. Avec lui, le chunk se charge après le montage — ou immédiatement si une erreur avant l’initialisation survient. Un petit tampon capture les erreurs et rejets non interceptés avant l’initialisation puis les vide ensuite.
* **Auth Firebase** — conditionné par `VITE_FIREBASE_API_KEY` + `VITE_FIREBASE_AUTH_DOMAIN` + `VITE_FIREBASE_PROJECT_ID`. `firebase-init.js` charge `firebase/app` et `firebase/auth` lors du premier `loadFirebaseAuth()` appel — un démarrage déjà connecté, un clic de connexion ou la vérification en arrière-plan — et mémorise le résultat. Un visiteur qui ne se connecte jamais ne télécharge jamais le SDK.

<details>

<summary>Comment l’indice d’authentification choisit le chemin de démarrage</summary>

`utils/auth-hint.js` stocke un drapeau indiquant si la dernière session était connectée. Au démarrage `main.js` le lit :

* `'1'` — charge Firebase et attend l’écouteur d’authentification avant le premier rendu, afin que la première requête authentifiée transporte déjà le jeton.
* `'0'` — monte immédiatement ; le SDK n’est jamais chargé avant un clic de connexion.
* `null` (première visite depuis l’ajout du drapeau, ou stockage effacé) — monte immédiatement, puis vérifie l’auth en arrière-plan après 3 secondes afin que le prochain démarrage emprunte exactement le bon chemin.

</details>

Le code de l’application ne doit jamais importer `@sentry/vue` directement — un import statique ramènerait le SDK dans le bundle principal. Les signaux explicites passent plutôt par le bus d’événements ; `sentry-init.js` s’abonne à `ip-source:exhausted` (une carte IP dont toute la chaîne de sources a échoué) de la même manière que le moteur de récompenses s’abonne à ses propres événements. La configuration se trouve dans [Surveillance des erreurs](/developer/fr/configuration/error-monitoring.md).

## Emplacement des helpers

| Nécessite                                        | Va dans                                        |
| ------------------------------------------------ | ---------------------------------------------- |
| Réactivité ou cycle de vie de Vue                | `composables/` comme `useXxx`                  |
| Rien de spécifique à Vue                         | `utils/` (jamais `use-` préfixé)               |
| prise en charge de shadcn (`cn()`)               | `lib/`                                         |
| Quelque chose dont le backend a également besoin | `common/`, réexporté via un pont dans `utils/` |

Les conventions de rédaction de ces fichiers se trouvent dans [Conventions de codage](/developer/fr/development/coding-conventions.md); le périmètre des tests se trouve dans [Tests](/developer/fr/development/testing.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ipcheck.ing/developer/fr/architecture/frontend.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
