Chargement des valeurs officielles des points de terminaison…
+
+
+Si le tableau ne s’affiche pas, ouvrez [observer-config.json](observer-config.json).
+
+## Modèles de sujets
+
+```text
+meshcore/{IATA}/{PUBLIC_KEY}/packets
+meshcore/{IATA}/{PUBLIC_KEY}/status
+```
+
+`{IATA}` est le véritable code d’emplacement à trois lettres de l’observateur.
+`{PUBLIC_KEY}` est fourni par la radio ou l’intégration. Ne le remplacez jamais
+par une clé privée.
+
+## Authentification et transport
+
+- Utilisez WebSockets sur le port `443`.
+- Exigez TLS et validez les certificats.
+- Utilisez l’option de jeton JWT de MeshCore lorsqu’elle est offerte.
+- Faites correspondre l’audience de chaque jeton à l’hôte de son point de terminaison.
+- Ne mettez jamais un jeton ou un mot de passe dans une URL, une capture d’écran,
+ un billet ou un ensemble de diagnostics.
+
+## Mode de paquets selon la méthode
+
+| Méthode | Paramètre de paquets requis |
+|---|---|
+| Micrologiciel MQTT | `mqtt.packets on`, `bridge.enabled on`, et `mqtt.rx on` |
+| MCtoMQTT / capture d’un compagnon | Configurez le sujet `/packets` |
+| PyMC | `format: letsmesh` |
+| Home Assistant | **Payload Mode** = `packet` |
+| RemoteTerm | Activez le sujet de paquets Community MQTT |
+
+## Ce que chaque vérification confirme
+
+| État | Ce qu’il confirme |
+|---|---|
+| DNS ou port accessible | L’hôte peut joindre le point de terminaison |
+| Connexion au courtier | Le transport et l’authentification fonctionnent |
+| Observateur visible | Son état s’est rendu au service en direct |
+| Paquet récent visible | Le parcours complet de la radio jusqu’à l’affichage fonctionne |
+
+Seul un paquet récent permet de terminer [la vérification de l’observateur](verify.md).
diff --git a/docs/analyzer/broker-reference.md b/docs/analyzer/broker-reference.md
index 4278fb5..9ed64c0 100644
--- a/docs/analyzer/broker-reference.md
+++ b/docs/analyzer/broker-reference.md
@@ -1,50 +1,87 @@
-# Broker Reference
+---
+title: Observer endpoint reference
+description: Find the broker, security, topic, and packet settings used by MeshCore Canada observers.
+audience:
+ - observer-operators
+ - service-operators
+task: reference-observer-endpoints
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: advanced
+estimated_time: 8 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+page_scripts:
+ - assets/javascripts/analyzer-broker-reference.js?v=20260722-2
+---
-All MeshCore.ca observer paths publish to the same redundant broker pair.
+# Observer endpoint reference
-| Broker | Host | Port | Transport | TLS | Token audience |
-|--------|------|------|-----------|-----|----------------|
-| Primary | `mqtt1.meshcore.ca` | `443` | WebSockets | Enabled + verified | `mqtt1.meshcore.ca` |
-| Backup | `mqtt2.meshcore.ca` | `443` | WebSockets | Enabled + verified | `mqtt2.meshcore.ca` |
+Use this reference after you have [chosen an observer setup](intro.md). Follow that setup's guide instead of copying fields from here on their own.
-Use JWT token authentication where the setup tool exposes it. Do not set a static MQTT password unless a path-specific guide tells you to.
+## Broker settings
-## Topic Pattern
+These values come from the shared [observer configuration](observer-config.json).
-Most observer paths publish packet telemetry under this pattern:
+
+
+
+
+
+
Use
+
Host
+
Port
+
Transport
+
TLS
+
Token audience
+
+
+
+
+
+
Loading canonical endpoint values…
+
-```text
-meshcore/{IATA}/{PUBLIC_KEY}/packets
-```
+If the table does not load, open [observer-config.json](observer-config.json).
-Status topics generally use:
+## Topic templates
```text
+meshcore/{IATA}/{PUBLIC_KEY}/packets
meshcore/{IATA}/{PUBLIC_KEY}/status
```
-`{IATA}` must be a real 3-letter airport code nearest to the observer. `{PUBLIC_KEY}` is supplied by the MeshCore node or integration.
+`{IATA}` is the observer's real three-letter location code. `{PUBLIC_KEY}` is supplied by the radio or integration. Never substitute a private key.
+
+## Authentication and transport
-## Payload Mode
+- Use WebSockets on port `443`.
+- Require TLS and verify certificates.
+- Use the MeshCore JWT token option where available.
+- Match each token audience to its endpoint host.
+- Do not put a token or password into a URL, screenshot, issue, or diagnostic bundle.
-MeshCore.ca packet visibility needs packet payload publishing, not only status publishing.
+## Packet mode by method
-| Path | Required packet setting |
-|------|-------------------------|
-| MQTT firmware | `set mqtt.packets on`, `set bridge.enabled on`, `set mqtt.rx on`, `set mqtt.tx advert` |
-| MCtoMQTT | Broker config created by the MeshCore.ca helper |
-| Companion capture | `PACKETCAPTURE_MQTT*_TOPIC_PACKETS` configured |
+| Method | Required packet setting |
+|---|---|
+| MQTT firmware | `mqtt.packets on`, `bridge.enabled on`, and `mqtt.rx on` |
+| MCtoMQTT / companion capture | Configure the `/packets` topic |
| PyMC | `format: letsmesh` |
-| Home Assistant | **Payload Mode** = `packet`, or older **Packets (Lets Mesh)** enabled |
-| RemoteTerm | Community MQTT packet topic template enabled |
+| Home Assistant | **Payload Mode** = `packet` |
+| RemoteTerm | Enable the Community MQTT packet topic |
-## Common Broker Mistakes
+## What each check tells you
-| Symptom | Likely cause |
-|---------|--------------|
-| Primary connects, backup fails | Backup token audience still set to `mqtt1.meshcore.ca` |
-| Observer appears under the wrong city | IATA field is wrong or inconsistent between broker entries |
-| Broker connects but no packets appear | Status is publishing, but packet payload mode is not enabled |
-| Nothing appears | Radio is off the local mesh, IATA code is invalid, or outbound WSS is blocked |
+| State | What it proves |
+|---|---|
+| DNS or port reachable | The host can reach the endpoint |
+| Broker connected | Transport and authentication succeeded |
+| Observer visible | Status reached the live service |
+| Recent packet visible | The radio-to-viewer path works end to end |
-For path-specific commands, use [Troubleshooting](troubleshooting.md).
+Only a recent packet completes [the observer check](verify.md).
diff --git a/docs/analyzer/builds/mctomqtt.fr.md b/docs/analyzer/builds/mctomqtt.fr.md
new file mode 100644
index 0000000..858ada0
--- /dev/null
+++ b/docs/analyzer/builds/mctomqtt.fr.md
@@ -0,0 +1,231 @@
+---
+title: Observer avec MCtoMQTT
+description: Transmettez les paquets d’une radio USB à partir d’un ordinateur Linux ou macOS qui reste en fonction.
+audience:
+ - observer-operators
+ - service-operators
+task: configure-mctomqtt-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: advanced
+estimated_time: 30 minutes
+destructive: true
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Observer avec MCtoMQTT
+
+MCtoMQTT lit les paquets d’une radio MeshCore connectée par USB et les publie à
+partir d’un hôte Linux ou macOS qui reste en fonction.
+
+## Cette méthode vous convient-elle?
+
+
+
Utilisez MCtoMQTT siUn ordinateur Linux ou macOS reste près d’une radio de journalisation des paquets connectée par USB.
+
Choisissez autre chose siRemoteTerm, Home Assistant ou PyMC gère déjà la radio.
+
Gardez en fonctionLa radio, la connexion USB, le service de l’hôte et la connexion Internet.
+
+
+L’outil d’assistance peut aussi configurer `meshcore-packet-capture` pour un
+compagnon relié par BLE, liaison série ou TCP.
+
+## Avant de commencer { #before-you-start }
+
+| Exigence | Vérification |
+|---|---|
+| Radio | La journalisation des paquets fonctionne par USB et les paramètres du réseau local sont exacts |
+| Hôte | Linux ou macOS pour `meshcoretomqtt`; Windows est pris en charge uniquement pour la capture d’un compagnon |
+| Accès | Vous pouvez lire la configuration actuelle et redémarrer le service |
+| Emplacement | Un véritable [code d’emplacement](../iata-codes.md) à trois lettres |
+| Outils | `curl`, un afficheur de texte et `systemctl` sous Linux |
+
+!!! warning "Examinez le script avant de l’exécuter"
+ Ces outils d’assistance ne sont liés ni à une version précise ni à une somme de contrôle. Téléchargez-les et examinez-les avant de les exécuter. L’option `--no-restart` écrit quand même des fichiers; ce n’est pas une simulation.
+
+Fichiers à examiner :
+
+- [Source de l’outil Bash](https://github.com/MeshCore-ca/MeshCore-Canada/blob/main/docs/analyzer/scripts/add-meshcore-ca-broker.sh)
+- [Outil Bash publié](https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh)
+- [Source de l’outil PowerShell pour compagnon](https://github.com/MeshCore-ca/MeshCore-Canada/blob/main/docs/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1)
+
+## Ce qui sera modifié
+
+Sur un hôte à liaison série, l’outil :
+
+- écrit `/etc/mctomqtt/config.d/20-meshcore-ca.toml`;
+- crée une copie horodatée `.bak.` si ce fichier existe;
+- ajoute les points de terminaison principal et de secours de MeshCore Canada ainsi que le code d’emplacement;
+- redémarre `mctomqtt`, sauf si l’option `--no-restart` est utilisée.
+
+Pour la capture d’un compagnon, il met à jour
+`~/.meshcore-packet-capture/.env.local`, crée une sauvegarde horodatée,
+configure les emplacements 1 et 2, désactive les emplacements 3 à 6 et peut
+redémarrer le service de capture.
+
+Les options d’installation téléchargent et exécutent des programmes
+d’installation distincts provenant des projets d’origine. Ne les utilisez pas
+avant d’avoir aussi examiné le programme nommé.
+
+## Configuration
+
+### 1. Télécharger et examiner l’outil
+
+```bash
+workdir="$(mktemp -d)"
+curl -fsSLo "$workdir/add-meshcore-ca-broker.sh" https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh
+less "$workdir/add-meshcore-ca-broker.sh"
+```
+
+Vérifiez l’URL source, les chemins modifiés, les hôtes des points de
+terminaison, le comportement de sauvegarde et le comportement de redémarrage.
+Conservez le fichier pour le reste de la configuration.
+
+### 2. Écrire sans redémarrer
+
+Remplacez `YOW` par le véritable code d’emplacement le plus près de
+l’observateur :
+
+```bash
+bash "$workdir/add-meshcore-ca-broker.sh" --device serial-host --iata YOW --no-restart
+```
+
+L’outil valide la forme du code, rejette `XXX`, affiche un avertissement pour
+les codes absents de sa courte liste et indique les chemins modifiés et
+sauvegardés.
+
+### 3. Examiner le résultat
+
+```bash
+sudo sed -n '1,220p' /etc/mctomqtt/config.d/20-meshcore-ca.toml
+```
+
+Confirmez que :
+
+- le code d’emplacement est exact;
+- l’hôte principal et son audience sont `mqtt1.meshcore.ca`;
+- l’hôte de secours et son audience sont `mqtt2.meshcore.ca`;
+- le port est `443`, le transport est WebSockets et la validation TLS est activée.
+
+Ne collez pas le fichier complet dans un billet public sans l’avoir examiné.
+
+### 4. Redémarrer volontairement
+
+```bash
+sudo systemctl restart mctomqtt
+sudo systemctl status mctomqtt --no-pager
+```
+
+Le service devrait demeurer actif sans erreurs TLS ou d’authentification
+répétées.
+
+### Exécuter directement après l’examen
+
+Après avoir examiné la version actuellement publiée de l’outil, vous pouvez
+exécuter directement ce même fichier :
+
+```bash
+bash <(curl -fsSL https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh) --device serial-host --iata YOW
+```
+
+La méthode de téléchargement présentée plus haut facilite l’examen et la
+récupération.
+
+### Nouvelle installation
+
+L’option `--install-mctomqtt` télécharge et exécute le programme d’installation
+du projet `meshcoretomqtt`. Examinez d’abord ce programme, puis ajoutez
+l’option :
+
+```bash
+bash "$workdir/add-meshcore-ca-broker.sh" --device serial-host --iata YOW --install-mctomqtt
+```
+
+Le programme d’installation du projet d’origine contrôle le choix du port série
+et les modifications de paquets. Il n’est lié ici ni à une version ni à une
+somme de contrôle.
+
+## Capture d’un compagnon
+
+Réglez d’abord la radio du compagnon selon les paramètres du réseau maillé
+local.
+
+=== "Linux ou macOS"
+
+ Examinez le même outil Bash, puis exécutez :
+
+ ```bash
+ bash "$workdir/add-meshcore-ca-broker.sh" --device companion --iata YOW --no-restart
+ ```
+
+ Examinez `~/.meshcore-packet-capture/.env.local`, puis redémarrez votre
+ processus de capture.
+
+=== "Windows PowerShell"
+
+ Téléchargez et examinez l’outil avant de l’exécuter :
+
+ ```powershell
+ $helper = Join-Path $env:TEMP "add-meshcore-ca-packetcapture-broker.ps1"
+ Invoke-WebRequest https://meshcore.ca/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1 -OutFile $helper
+ Get-Content $helper
+ powershell -NoProfile -ExecutionPolicy Bypass -File $helper -Iata YOW
+ ```
+
+ L’outil PowerShell modifie
+ `%USERPROFILE%\.meshcore-packet-capture\.env.local` et crée une sauvegarde.
+ Son option d’installation facultative exécute un programme d’installation
+ provenant d’un autre projet; examinez-le d’abord.
+
+Il n’existe pas de programme d’installation Windows documenté pour
+`meshcoretomqtt` dans le cas d’un hôte série de journalisation des paquets.
+
+## Ce que vous devriez voir
+
+Le service demeure actif, le point de terminaison principal se connecte et son
+nombre de paquets change lorsque la radio capte du trafic à proximité.
+
+## Vérifier dans CoreScope { #check-in-corescope }
+
+1. Confirmez que le service demeure actif.
+2. Trouvez l’observateur dans [CoreScope Observers](https://live.meshcore.ca/#/observers).
+3. Attendez de l’activité normale à proximité.
+4. Confirmez la présence d’un paquet récent dans [CoreScope Packets](https://live.meshcore.ca/#/packets).
+
+Terminez avec [Vérifier votre observateur](../verify.md). Un service en fonction
+ou une connexion au courtier ne confirme pas que les paquets se sont rendus à
+CoreScope.
+
+## Récupération { #recovery }
+
+Utilisez le chemin exact de la sauvegarde affiché par l’outil.
+
+Si un fichier de configuration complémentaire pour l’hôte série existait déjà :
+
+```bash
+sudo cp -- '/etc/mctomqtt/config.d/20-meshcore-ca.toml.bak.' '/etc/mctomqtt/config.d/20-meshcore-ca.toml'
+sudo systemctl restart mctomqtt
+```
+
+Si l’outil a créé un nouveau fichier complémentaire et qu’aucun fichier
+n’existait auparavant :
+
+```bash
+sudo rm -- '/etc/mctomqtt/config.d/20-meshcore-ca.toml'
+sudo systemctl restart mctomqtt
+```
+
+Pour la capture d’un compagnon, restaurez la sauvegarde
+`.env.local.bak.` indiquée par-dessus `.env.local`, conservez ses
+permissions, puis redémarrez le processus de capture.
+
+L’outil ne désinstalle pas les logiciels ajoutés par une option d’installation
+externe. Utilisez la procédure de désinstallation vérifiée du projet d’origine.
+
+## Si la vérification échoue
+
+Consultez le [dépannage](../troubleshooting.md). Partagez uniquement un court
+état de service caviardé et la liste des chemins modifiés.
diff --git a/docs/analyzer/builds/mctomqtt.md b/docs/analyzer/builds/mctomqtt.md
index 9e03b13..4baf031 100644
--- a/docs/analyzer/builds/mctomqtt.md
+++ b/docs/analyzer/builds/mctomqtt.md
@@ -1,157 +1,198 @@
-# MCtoMQTT (USB Serial Host)
+---
+title: Observe with MCtoMQTT
+description: Send packets from a USB radio through an always-on Linux or macOS computer.
+audience:
+ - observer-operators
+ - service-operators
+task: configure-mctomqtt-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: advanced
+estimated_time: 30 minutes
+destructive: true
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
-Bridge a USB-connected MeshCore node to MQTT brokers using `meshcoretomqtt`. This runs on a Linux or macOS host with the device connected over USB serial.
+# Observe with MCtoMQTT
-!!! info "What it does"
- The MCtoMQTT service reads packets from a MeshCore device over its serial port and publishes them to one or more MQTT brokers. A config drop-in file tells it where to send the data.
+MCtoMQTT reads packets from a USB-connected MeshCore radio and publishes them from an always-on Linux or macOS host.
-## Prerequisites
+## Is this method right for you?
-| Requirement | Details |
-|-------------|---------|
-| Device | MeshCore node with packet logging enabled, connected via USB serial |
-| Host | Linux or macOS |
-| Tools | `curl` installed |
-| IATA Code | Your real 3-letter IATA airport code (e.g. `YOW` for Ottawa) |
-| Mesh Settings | Connected radio uses `USA/Canada (Recommended)` and 3-byte path hashes |
+
+
Use MCtoMQTT ifA Linux or macOS computer stays beside a packet-log radio connected over USB.
+
Use something else ifRemoteTerm, Home Assistant, or PyMC already manages the radio.
+
Keep onlineThe radio, USB connection, host service, and internet connection.
+
-## Quick Setup
+The helper can also configure `meshcore-packet-capture` for a companion connected by BLE, serial, or TCP.
-If `meshcoretomqtt` is already installed, run the one-liner to add the MeshCore.ca brokers:
+## Before you start
-```bash
-bash <(curl -fsSL https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh) --device serial-host
-```
+| Requirement | Check |
+|---|---|
+| Radio | Packet logging works over USB and the local mesh settings are correct |
+| Host | Linux or macOS for `meshcoretomqtt`; Windows is supported only for companion capture |
+| Access | You can read the current config and restart the service |
+| Location | A real three-letter [location code](../iata-codes.md) |
+| Tools | `curl`, a text viewer, and `systemctl` on Linux |
+
+!!! warning "Review before running"
+ These helpers are not pinned to a release or checksum. Download and inspect them before running. `--no-restart` still writes files; it is not a dry run.
+
+Files to review:
+
+- [Bash helper source](https://github.com/MeshCore-ca/MeshCore-Canada/blob/main/docs/analyzer/scripts/add-meshcore-ca-broker.sh)
+- [Published Bash helper](https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh)
+- [PowerShell companion helper source](https://github.com/MeshCore-ca/MeshCore-Canada/blob/main/docs/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1)
-This creates a config drop-in at `/etc/mctomqtt/config.d/20-meshcore-ca.toml` pointing at the MeshCore.ca broker pair, then restarts the service.
+## What this changes
-### Fresh Install
+On a serial host, the helper:
-If `meshcoretomqtt` is not yet installed, add `--install-mctomqtt` and the script will run the upstream installer first:
+- writes `/etc/mctomqtt/config.d/20-meshcore-ca.toml`;
+- makes a timestamped `.bak.` copy when that file exists;
+- adds the primary and backup MeshCore Canada endpoints and location code; and
+- restarts `mctomqtt` unless `--no-restart` is used.
+
+For companion capture, it updates `~/.meshcore-packet-capture/.env.local`, makes a timestamped backup, configures slots 1 and 2, disables slots 3–6, and may restart the capture service.
+
+Install flags download and run separate upstream installers. Do not use them until you have reviewed the named upstream installer too.
+
+## Set up
+
+### 1. Download and inspect the helper
```bash
-bash <(curl -fsSL https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh) --device serial-host --install-mctomqtt
+workdir="$(mktemp -d)"
+curl -fsSLo "$workdir/add-meshcore-ca-broker.sh" https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh
+less "$workdir/add-meshcore-ca-broker.sh"
```
-!!! tip
- The upstream installer will walk you through serial port selection.
+Check the source URL, changed paths, endpoint hosts, backup behaviour, and restart behaviour. Keep the file for the rest of this setup.
-## Specifying Your Region
+### 2. Write without restarting
-Pass your IATA code via the `--iata` flag or the `MESHCORE_CA_IATA` environment variable. Use the real 3-letter airport code nearest to you:
+Replace `YOW` with the real location code nearest the observer:
-=== "Flag"
+```bash
+bash "$workdir/add-meshcore-ca-broker.sh" --device serial-host --iata YOW --no-restart
+```
- ```bash
- bash <(curl -fsSL https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh) --device serial-host --iata YOW
- ```
+The helper validates the code shape, rejects `XXX`, warns on codes outside its quick list, and prints the changed and backup paths.
-=== "Environment Variable"
+### 3. Review the result
- ```bash
- MESHCORE_CA_IATA=YOW bash <(curl -fsSL https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh) --device serial-host
- ```
+```bash
+sudo sed -n '1,220p' /etc/mctomqtt/config.d/20-meshcore-ca.toml
+```
-If omitted, the script will prompt interactively.
+Check that:
-!!! warning "Use a real IATA code"
- The helper shows a Canadian quick list when it prompts. If your nearest real airport code is not shown, you can still type it, but continue only if it is a real IATA airport code. Do not use `CAN` as shorthand for Canada; it is a real airport code for Guangzhou and will tag your observer to the wrong region.
+- the location code is correct;
+- primary host and audience are `mqtt1.meshcore.ca`;
+- backup host and audience are `mqtt2.meshcore.ca`;
+- port is `443`, transport is WebSockets, and TLS verification is enabled.
-!!! tip "Check the radio first"
- MCtoMQTT can publish packets from a connected radio, but it cannot fix a radio that is listening on the wrong mesh settings. Before troubleshooting MQTT, confirm the MeshCore node is on **USA/Canada (Recommended)** or `910.525 MHz / 62.5 kHz / SF7 / CR5`, and that companion-style radios use 3-byte path hashes.
+Do not paste the full file into a public issue without reviewing it.
-## What the Script Creates
+### 4. Restart deliberately
-The drop-in config at `/etc/mctomqtt/config.d/20-meshcore-ca.toml`:
+```bash
+sudo systemctl restart mctomqtt
+sudo systemctl status mctomqtt --no-pager
+```
-??? note "View full config"
+The service should stay active without repeated TLS or authentication errors.
- ```toml
- [general]
- iata = "YOW"
+### Run it directly after review
- [[broker]]
- name = "meshcore-ca-1"
- enabled = true
- server = "mqtt1.meshcore.ca"
- port = 443
- transport = "websockets"
- keepalive = 60
- qos = 0
- retain = true
+After reviewing the current published helper, you can run that same file directly:
- [broker.tls]
- enabled = true
- verify = true
+```bash
+bash <(curl -fsSL https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh) --device serial-host --iata YOW
+```
- [broker.auth]
- method = "token"
- audience = "mqtt1.meshcore.ca"
+The downloaded-file method above is easier to review and recover.
- [[broker]]
- name = "meshcore-ca-2"
- enabled = true
- server = "mqtt2.meshcore.ca"
- port = 443
- transport = "websockets"
- keepalive = 60
- qos = 0
- retain = true
+### Fresh installation
- [broker.tls]
- enabled = true
- verify = true
+`--install-mctomqtt` downloads and runs the upstream `meshcoretomqtt` installer. Review that installer first, then add the flag:
- [broker.auth]
- method = "token"
- audience = "mqtt2.meshcore.ca"
- ```
+```bash
+bash "$workdir/add-meshcore-ca-broker.sh" --device serial-host --iata YOW --install-mctomqtt
+```
+
+The upstream installer controls serial-port selection and package changes, and it is not pinned or checksummed here.
-## Companion Devices (BLE / Serial / TCP)
+## Companion capture
-For companion radios (not packet-log serial hosts), use the companion path instead:
+Set the companion radio to the local mesh settings first.
-Before running the companion helper, set the companion to **USA/Canada (Recommended)** and set path hash mode to **3-byte** in the companion app or config tool.
+=== "Linux or macOS"
-=== "Linux / macOS"
+ Review the same Bash helper, then run:
```bash
- bash <(curl -fsSL https://meshcore.ca/analyzer/scripts/add-meshcore-ca-broker.sh) --device companion
+ bash "$workdir/add-meshcore-ca-broker.sh" --device companion --iata YOW --no-restart
```
- Add `--install-packetcapture` for a fresh install. The upstream installer will walk you through BLE, serial, or TCP connection setup.
+ Review `~/.meshcore-packet-capture/.env.local`, then restart your capture process.
=== "Windows PowerShell"
+ Download and inspect the helper before running:
+
```powershell
- powershell -NoProfile -ExecutionPolicy Bypass -Command "iwr https://meshcore.ca/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1 -UseBasicParsing | iex"
+ $helper = Join-Path $env:TEMP "add-meshcore-ca-packetcapture-broker.ps1"
+ Invoke-WebRequest https://meshcore.ca/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1 -OutFile $helper
+ Get-Content $helper
+ powershell -NoProfile -ExecutionPolicy Bypass -File $helper -Iata YOW
```
- Config is stored under `%USERPROFILE%\.meshcore-packet-capture\.env.local`.
+ The PowerShell helper changes `%USERPROFILE%\.meshcore-packet-capture\.env.local` and creates a backup. Its optional install switch executes an upstream installer and must be reviewed first.
+
+There is no documented `meshcoretomqtt` Windows installer for the packet-log serial-host path.
+
+## What you should see
+
+The service stays active, the primary endpoint connects, and its packet count changes when the radio hears nearby traffic.
+
+## Verify in CoreScope
-!!! warning "Windows and Serial Hosts"
- There is no upstream `meshcoretomqtt` Windows installer. Keep packet-log serial hosts on Linux or macOS. The Windows PowerShell helper is for companion radios only.
+1. Confirm the service remains active.
+2. Find the observer in [CoreScope Observers](https://live.meshcore.ca/#/observers).
+3. Wait for normal nearby activity.
+4. Confirm a recent packet in [CoreScope Packets](https://live.meshcore.ca/#/packets).
-## Quick Reference
+Finish with [Check your observer](../verify.md). A running service or broker connection is not proof that packets reached CoreScope.
-| Path | Manager | Connection | Config Location |
-|------|---------|------------|-----------------|
-| Serial Host | meshcoretomqtt | USB serial | `/etc/mctomqtt/config.d/20-meshcore-ca.toml` |
-| Companion | meshcore-packet-capture | BLE, serial, or TCP | `~/.meshcore-packet-capture/.env.local` |
+## Recovery
+
+Use the exact backup path printed by the helper.
+
+If a serial-host drop-in existed before:
+
+```bash
+sudo cp -- '/etc/mctomqtt/config.d/20-meshcore-ca.toml.bak.' '/etc/mctomqtt/config.d/20-meshcore-ca.toml'
+sudo systemctl restart mctomqtt
+```
+
+If the helper created a new drop-in and no prior file existed:
+
+```bash
+sudo rm -- '/etc/mctomqtt/config.d/20-meshcore-ca.toml'
+sudo systemctl restart mctomqtt
+```
-## Script Options
+For companion capture, restore the printed `.env.local.bak.` over `.env.local`, preserve its permissions, and restart the capture process.
-| Flag | Description |
-|------|-------------|
-| `--iata CODE` | Real 3-letter IATA airport code |
-| `--list-iata` | Show the Canadian quick-list choices |
-| `--device TYPE` | `serial-host` or `companion` |
-| `--install-mctomqtt` | Install meshcoretomqtt if not present |
-| `--install-packetcapture` | Install meshcore-packet-capture if not present |
-| `--no-restart` | Patch config without restarting the service |
-| `--config-dir PATH` | Override config dir (default: `/etc/mctomqtt`) |
-| `--service NAME` | Override systemd service name (default: `mctomqtt`) |
+The helper does not uninstall software added by an upstream install flag. Use that upstream project's reviewed uninstall process.
-## Verify
+## If verification fails
-Once your service is running, head to [Check Your Observer](../verify.md) to confirm it's reporting correctly.
+Use [Troubleshooting](../troubleshooting.md). Share only a short redacted service status and the list of changed paths.
diff --git a/docs/analyzer/builds/meshcore-ha.fr.md b/docs/analyzer/builds/meshcore-ha.fr.md
new file mode 100644
index 0000000..815380a
--- /dev/null
+++ b/docs/analyzer/builds/meshcore-ha.fr.md
@@ -0,0 +1,121 @@
+---
+title: Observer avec Home Assistant
+description: Transmettez à CoreScope les paquets d’une intégration MeshCore déjà configurée dans Home Assistant.
+audience:
+ - observer-operators
+ - home-assistant-users
+task: configure-home-assistant-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: intermediate
+estimated_time: 15 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Observer avec Home Assistant
+
+Utilisez l’intégration MeshCore que vous exploitez déjà dans Home Assistant pour
+publier la télémétrie des paquets auprès de MeshCore Canada.
+
+## Cette méthode vous convient-elle?
+
+
+
Utilisez Home Assistant siUne intégration MeshCore fonctionnelle et une radio connectée y sont déjà configurées.
+
Choisissez autre chose siVous installeriez Home Assistant uniquement pour observer.
+
Gardez en fonctionHome Assistant, la connexion radio et l’accès Internet.
+
+
+## Vérifier votre écran
+
+Les intégrations MeshCore actuelles utilisent ces champs :
+
+| Contrôle des paquets | Contrôle de l’emplacement |
+|---|---|
+| **Payload Mode** = `packet` | Champ libre **Broker IATA Code** |
+
+Si votre écran est différent, consultez
+[L’écran de Home Assistant ne correspond pas au guide](../troubleshooting.md#home-assistant-screen-does-not-match-the-guide).
+Mettez l’intégration à jour si elle n’accepte pas le bon code d’emplacement; ne
+choisissez pas un code voisin pour contourner le problème.
+
+## Avant de commencer
+
+- [ ] Home Assistant et l’intégration MeshCore fonctionnent correctement.
+- [ ] La radio est connectée par une liaison USB, BLE ou TCP compatible.
+- [ ] La radio utilise les paramètres du réseau maillé local.
+- [ ] Vous avez choisi un véritable [code d’emplacement](../iata-codes.md).
+- [ ] Vous avez lu [Données des observateurs et vie privée](../data-collection-access.md).
+
+## Ce qui sera modifié
+
+Vous ajouterez deux entrées de courtier MQTT et activerez les paquets dans
+**Payload Mode**. N’entrez ni mot de passe Wi-Fi ni mot de passe MQTT statique.
+
+## Configuration
+
+Ouvrez :
+
+**Settings** → **Devices & Services** → **MeshCore** → **Configure** → **Manage MQTT Brokers**
+
+Ajoutez l’entrée principale :
+
+| Champ | Valeur |
+|---|---|
+| Server | `mqtt1.meshcore.ca` |
+| Port | `443` |
+| Transport | `websockets` |
+| Use TLS | Enabled |
+| TLS Verify | Enabled |
+| Username / Password | Leave blank |
+| Use MeshCore Auth Token | Enabled |
+| Token Audience | `mqtt1.meshcore.ca` |
+| Payload Mode | `packet` |
+| Status Topic | `meshcore/{IATA}/{PUBLIC_KEY}/status` |
+| Packets Topic | `meshcore/{IATA}/{PUBLIC_KEY}/packets` |
+
+Réglez le champ d’emplacement de l’intégration au véritable code à trois
+lettres le plus près de l’observateur.
+
+Ajoutez l’entrée de secours avec les mêmes paramètres, en ne modifiant que :
+
+| Champ | Valeur |
+|---|---|
+| Server | `mqtt2.meshcore.ca` |
+| Token Audience | `mqtt2.meshcore.ca` |
+
+Laissez les champs facultatifs du responsable vides, sauf s’ils sont
+nécessaires. Enregistrez les deux entrées.
+
+## Ce que vous devriez voir
+
+Les deux entrées indiquent qu’elles sont connectées, et l’activité radio à
+proximité fait changer le nombre de paquets. Si les courtiers se connectent,
+mais qu’aucun paquet n’apparaît, le mode de paquets est peut-être désactivé ou
+la radio ne capte aucune activité.
+
+## Vérifier dans CoreScope
+
+1. Trouvez l’observateur dans [CoreScope Observers](https://live.meshcore.ca/#/observers).
+2. Attendez de l’activité MeshCore normale à proximité.
+3. Confirmez la présence d’un paquet récent dans [CoreScope Packets](https://live.meshcore.ca/#/packets).
+
+Terminez avec [Vérifier votre observateur](../verify.md). Le badge de connexion
+de Home Assistant ne confirme pas que les paquets se sont rendus à CoreScope.
+
+## Récupération
+
+Désactivez ou supprimez seulement les deux entrées de courtier MeshCore Canada
+que vous avez ajoutées. Ne modifiez pas les autres intégrations de Home
+Assistant ni la connexion radio. Confirmez que l’intégration MeshCore d’origine
+fonctionne toujours.
+
+## Si la vérification échoue
+
+Consultez le [dépannage](../troubleshooting.md). Indiquez les versions de Home
+Assistant et de l’intégration MeshCore, la première étape qui a échoué et une
+erreur caviardée. Examinez toute archive de diagnostics avant de la partager.
diff --git a/docs/analyzer/builds/meshcore-ha.md b/docs/analyzer/builds/meshcore-ha.md
index 934a305..5b265a2 100644
--- a/docs/analyzer/builds/meshcore-ha.md
+++ b/docs/analyzer/builds/meshcore-ha.md
@@ -1,95 +1,105 @@
-# MeshCore-HA (Home Assistant)
+---
+title: Observe with Home Assistant
+description: Send packets from an existing Home Assistant MeshCore integration to CoreScope.
+audience:
+ - observer-operators
+ - home-assistant-users
+task: configure-home-assistant-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: intermediate
+estimated_time: 15 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
-Add the MeshCore.ca broker pair to your Home Assistant MeshCore integration. This connects your HA instance to the Canadian mesh telemetry network.
+# Observe with Home Assistant
-## Prerequisites
+Use the MeshCore integration you already run in Home Assistant to publish packet telemetry to MeshCore Canada.
-| Requirement | Details |
-|-------------|---------|
-| Home Assistant | With the MeshCore integration installed |
-| MeshCore Node | Connected to HA via USB, BLE, or TCP |
-| IATA Code | The same real 3-letter IATA airport code used by your observer (e.g. `YOW` for Ottawa) |
-| Mesh Settings | Connected node uses `USA/Canada (Recommended)` and 3-byte path hashes |
+## Is this method right for you?
-## Setup
+
+
Use Home Assistant ifIt already has a working MeshCore integration and connected radio.
+
Use something else ifYou would install Home Assistant only for observing.
+
Keep onlineHome Assistant, the radio connection, and internet access.
+
-!!! tip "Known-good setup"
- A Home Assistant observer needs three things to line up: both brokers connected, **Payload Mode** set to `packet` (or **Packets (Lets Mesh)** enabled on older screens), and the same real IATA code on both broker entries.
+## Check your screen
- The connected MeshCore node must also be on the MeshCore Canada network settings: **USA/Canada (Recommended)**, or raw radio values `910.525 MHz / 62.5 kHz / SF7 / CR5`, with 3-byte path hashes.
+Current MeshCore integrations use these fields:
-### 1. Open Broker Settings
+| Packet control | Location control |
+|---|---|
+| **Payload Mode** = `packet` | Free-text **Broker IATA Code** |
-Navigate to:
+If your screen looks different, see [Home Assistant screen does not match the guide](../troubleshooting.md#home-assistant-screen-does-not-match-the-guide). Update the integration if it cannot accept the correct location code; do not substitute a nearby code.
-**Settings** > **Devices & Services** > **MeshCore** > **Configure** > **Manage MQTT Brokers**
+## Before you start
-### 2. Configure the Primary Broker
+- [ ] Home Assistant and the MeshCore integration are healthy.
+- [ ] The radio is connected over a supported USB, BLE, or TCP path.
+- [ ] The radio uses the local mesh settings.
+- [ ] You chose a real [location code](../iata-codes.md).
+- [ ] You read [Observer data, access, and privacy](../data-collection-access.md).
+
+## What this changes
+
+You will add two MQTT broker entries and enable packet payloads. Do not enter a Wi-Fi password or static MQTT password.
+
+## Set up
+
+Open:
+
+**Settings** → **Devices & Services** → **MeshCore** → **Configure** → **Manage MQTT Brokers**
+
+Add the primary entry:
| Field | Value |
-|-------|-------|
+|---|---|
| Server | `mqtt1.meshcore.ca` |
| Port | `443` |
| Transport | `websockets` |
-| Use TLS | :material-check: Enabled |
-| TLS Verify | :material-check: Enabled |
+| Use TLS | Enabled |
+| TLS Verify | Enabled |
| Username / Password | Leave blank |
-| Use MeshCore Auth Token | :material-check: Enabled |
+| Use MeshCore Auth Token | Enabled |
| Token Audience | `mqtt1.meshcore.ca` |
-| Auth Token TTL | Leave default |
| Payload Mode | `packet` |
| Status Topic | `meshcore/{IATA}/{PUBLIC_KEY}/status` |
| Packets Topic | `meshcore/{IATA}/{PUBLIC_KEY}/packets` |
-| Client ID Prefix | Optional |
-| Owner Public Key | Optional (TLS verified only) |
-| Owner Email | Optional (TLS verified only) |
-### 3. Configure the Backup Broker
+Set the integration's location field to the real three-letter code nearest the observer.
-Add a second broker with the same settings, changing only:
+Add the backup entry with the same settings, changing only:
| Field | Value |
-|-------|-------|
+|---|---|
| Server | `mqtt2.meshcore.ca` |
| Token Audience | `mqtt2.meshcore.ca` |
-All other fields remain the same as the primary broker.
-
-!!! warning "Packet mode is required"
- If your Home Assistant version shows a checkbox or option named **Packets (Lets Mesh)**, enable it. In newer versions this is the **Payload Mode** setting and it must be `packet`. If this is missed, the broker can appear connected while no packet data shows up.
-
-### 4. Set Your Region
-
-Make sure your IATA code is set in the integration. The `{IATA}` placeholder in the topic templates will be replaced with your code (e.g. `YOW`).
+Leave optional owner fields blank unless they are needed. Save both entries.
-The Home Assistant integration must use the same IATA code as the observer that is publishing packets. If Home Assistant is set to `YOW` but the observer publishes under `YKF`, Home Assistant will subscribe to the wrong topics and look empty.
+## What you should see
-Current MeshCore-HA versions use a free-text **Broker IATA Code** field, so you can type a real code such as `YTR` even if it is missing from a quick list. If your Home Assistant screen only offers a picker and does not let you enter the code manually, update the MeshCore integration before using a neighboring airport code as a workaround.
+Both entries show connected and nearby radio activity changes the packet count. If the brokers connect but no packets appear, packet mode may be off or the radio may hear nothing.
-!!! tip
- You can find common Canadian IATA codes on the [IATA Region Codes](../iata-codes.md) page. If your nearest real airport code is missing from that quick list, you can still use it.
+## Verify in CoreScope
-## Common HA Symptoms
+1. Find the observer in [CoreScope Observers](https://live.meshcore.ca/#/observers).
+2. Wait for normal nearby MeshCore activity.
+3. Confirm a recent packet in [CoreScope Packets](https://live.meshcore.ca/#/packets).
-| Symptom | Most likely fix |
-|---------|-----------------|
-| Brokers show connected but no packets appear | Enable **Packets (Lets Mesh)** or set **Payload Mode** to `packet` |
-| `YTR` or another code is not selectable | Update MeshCore-HA, then type the code in **Broker IATA Code** |
-| One broker works and the backup does not | Check the second broker's **Token Audience** is `mqtt2.meshcore.ca` |
-| Packets appear under the wrong city | Set both broker IATA fields to the nearest real airport code |
+Finish with [Check your observer](../verify.md). Home Assistant's connected badge is not proof that packets reached CoreScope.
-## Quick Reference
+## Recovery
-| Setting | Primary | Backup |
-|---------|---------|--------|
-| Server | `mqtt1.meshcore.ca` | `mqtt2.meshcore.ca` |
-| Port | `443` | `443` |
-| Transport | `websockets` | `websockets` |
-| TLS | Enabled + Verified | Enabled + Verified |
-| Auth | MeshCore JWT Token | MeshCore JWT Token |
-| Audience | `mqtt1.meshcore.ca` | `mqtt2.meshcore.ca` |
-| Payload Mode | `packet` | `packet` |
+Disable or remove only the two MeshCore Canada broker entries you added. Keep unrelated Home Assistant integrations and the radio connection unchanged. Confirm the original MeshCore integration still operates.
-## Verify
+## If verification fails
-After saving, head to [Check Your Observer](../verify.md) to confirm it's reporting correctly.
+Use [Troubleshooting](../troubleshooting.md). Include the Home Assistant version, MeshCore integration version, first failed stage, and a redacted error. Review any diagnostics archive before sharing it.
diff --git a/docs/analyzer/builds/mqtt-firmware.fr.md b/docs/analyzer/builds/mqtt-firmware.fr.md
new file mode 100644
index 0000000..ba89f87
--- /dev/null
+++ b/docs/analyzer/builds/mqtt-firmware.fr.md
@@ -0,0 +1,266 @@
+---
+title: Construire un observateur MQTT autonome
+description: Transformez une carte LoRa Wi-Fi compatible en observateur consacré à CoreScope.
+audience:
+ - observer-operators
+task: configure-standalone-observer
+scope: experimental
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: advanced
+estimated_time: 35 minutes
+destructive: true
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+page_scripts:
+ - assets/javascripts/analyzer-command-builder.js?v=20260722-2
+---
+
+# Construire un observateur MQTT autonome
+
+Un micrologiciel d’observation spécialisé permet à une carte LoRa Wi-Fi
+compatible de transmettre la télémétrie des paquets à proximité sans ordinateur
+hôte distinct.
+
+## Cette méthode vous convient-elle?
+
+
+
Bon choixVous pouvez consacrer une carte LoRa Wi-Fi compatible à l’observation.
+
Choisissez une autre méthodeRemoteTerm, Home Assistant, PyMC ou un hôte USB à proximité gère déjà la radio.
+
Gardez en fonctionLa carte, une alimentation stable, le Wi-Fi 2,4 GHz et l’accès Internet.
+
+
+Cette méthode utilise un micrologiciel tiers. Confirmez la carte et la version
+affichées par l’outil de programmation avant de poursuivre.
+
+## Confirmer la source du micrologiciel
+
+| Élément | Référence |
+|---|---|
+| Outil de programmation | [observer.gessaman.com](https://observer.gessaman.com/) |
+| Branche source consignée | `mqtt-bridge-implementation-flex` |
+| Révision source consignée | `c0c845f5` |
+| Préréglages du courtier | `meshcore-ca-1` et `meshcore-ca-2` |
+
+La révision indique la source utilisée lors de l’examen de ce guide; elle ne
+confirme pas que l’outil en ligne utilise encore cette version. Arrêtez si
+l’outil affiche une carte, une source ou une version que vous n’avez pas
+vérifiée.
+
+## Avant de commencer { #before-you-start }
+
+!!! danger "Faites une sauvegarde avant d’activer Erase device"
+ L’effacement peut supprimer la clé d’identité privée, le nom, les paramètres radio, les données régionales et toute autre configuration enregistrée. Sauvegardez la clé privée et notez la carte, le rôle, la version du micrologiciel et les paramètres. Si vous ne pouvez pas sauvegarder l’identité, arrêtez et utilisez une nouvelle carte ou demandez de l’aide.
+
+- [ ] Confirmez que la carte exacte figure dans l’outil de programmation de l’observateur.
+- [ ] Décidez si ce nœud doit relayer le trafic ou seulement l’observer.
+- [ ] Sauvegardez l’identité et les paramètres actuels.
+- [ ] Gardez à portée de main un câble USB de données fiable et la méthode de récupération de la carte.
+- [ ] Choisissez un véritable [code d’emplacement](../iata-codes.md).
+
+## Ce qui sera modifié
+
+La programmation remplace le micrologiciel de la carte. La configuration
+modifie ensuite son nom, les valeurs radio, le mode de hachage des chemins, le
+code d’emplacement, les identifiants Wi-Fi, les préréglages du courtier, le mode
+de paquets et le comportement de répétition.
+
+Le générateur de commandes s’exécute localement dans ce navigateur. Il
+n’enregistre pas les champs Wi-Fi, ne les place pas dans l’URL et ne les inclut
+pas dans l’aperçu par défaut.
+
+## Configuration
+
+### 1. Programmer la carte
+
+1. Ouvrez [observer.gessaman.com](https://observer.gessaman.com/).
+2. Sous **MQTT Observer Firmware**, choisissez exactement la carte compatible.
+3. Choisissez **Repeater** ou **Room Server**.
+4. Pour une carte neuve ou volontairement réaffectée, activez **Erase device** seulement après avoir franchi l’étape de sauvegarde ci-dessus.
+5. Programmez l’image fusionnée.
+6. Une fois la programmation terminée, privilégiez **Configure via USB**. Utilisez **Console** uniquement pour les paramètres absents de l’écran de configuration.
+
+L’outil devrait confirmer la fin de l’opération et la carte devrait se
+reconnecter par USB.
+
+### 2. Entrer les paramètres communs
+
+Utilisez les paramètres du réseau maillé local. En l’absence d’une configuration
+propre à la communauté, la configuration canadienne de départ est :
+
+| Paramètre | Valeur |
+|---|---|
+| Préréglage radio | **USA/Canada (Recommended)** |
+| Valeurs radio brutes | `910.525 MHz / 62.5 kHz / SF7 / CR5` |
+| Empreintes de chemin | 3 octets (`set path.hash.mode 2`) |
+| Préréglage principal | `meshcore-ca-1` |
+| Préréglage de secours | `meshcore-ca-2` |
+| Wi-Fi | Un réseau 2,4 GHz |
+
+### 3. Générer les commandes
+
+L’interface en ligne de commande n’a pas de règle générale documentée pour les
+guillemets. Le générateur rejette les espaces, les guillemets, les barres
+obliques inverses, les caractères de contrôle et les autres valeurs Wi-Fi
+ambiguës. Utilisez **Configure via USB** pour un réseau qu’il ne peut pas
+représenter de façon sûre.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Chargement des suggestions d’emplacements canadiens…
+
Le SSID et le mot de passe restent uniquement dans cette page. Ils sont effacés lorsque vous la quittez et masqués dans l’aperçu jusqu’à ce que vous choisissiez de les afficher.
+
+
+
+
+
+
+
+
+
Remplissez les champs obligatoires pour générer les commandes.
+
+
+Vérifiez le résumé non sensible et l’aperçu caviardé. Affichez et copiez les
+commandes uniquement sur un ordinateur de confiance. Le presse-papiers
+contiendra les identifiants Wi-Fi; effacez-le après utilisation.
+
+### Entrer les commandes à la main
+
+Si vous préférez les entrer manuellement, définissez d’abord les valeurs non
+sensibles :
+
+```text
+set name YOW-Repeater-01
+set radio 910.525,62.5,7,5
+set path.hash.mode 2
+set mqtt.iata YOW
+set wifi.powersave none
+set mqtt1.preset meshcore-ca-1
+set mqtt2.preset meshcore-ca-2
+set mqtt3.preset none
+set mqtt4.preset none
+set mqtt5.preset none
+set mqtt6.preset none
+set mqtt.status on
+set mqtt.packets on
+set mqtt.raw off
+set mqtt.rx on
+set mqtt.tx advert
+set bridge.enabled on
+set repeat on
+advert
+```
+
+Remplacez le nom et le code d’emplacement. Utilisez `set repeat off` pour un
+nœud qui observe sans relayer.
+
+Entrez les identifiants directement dans la console de l’appareil :
+
+```text
+set wifi.ssid
+set wifi.pwd
+reboot
+```
+
+Ne collez pas les lignes d’identifiants complètes dans une conversation, un
+billet, une capture d’écran, un journal ou une note enregistrée. N’ajoutez pas
+de guillemets génériques de l’interpréteur de commandes à une valeur destinée
+à l’interface de l’appareil.
+
+## Ce que vous devriez voir
+
+Après le redémarrage, exécutez :
+
+```text
+get name
+get wifi.status
+get mqtt.iata
+get mqtt1.preset
+get mqtt2.preset
+get mqtt.status
+get mqtt.packets
+get bridge.enabled
+get path.hash.mode
+```
+
+La carte est correctement configurée lorsque :
+
+- le Wi-Fi et MQTT indiquent qu’ils sont connectés;
+- le code d’emplacement est exact;
+- les préréglages sont `meshcore-ca-1` et `meshcore-ca-2`;
+- la publication des paquets et le mode pont sont activés;
+- le mode de hachage des chemins est `2`.
+
+## Vérifier dans CoreScope { #check-in-corescope }
+
+1. Trouvez l’observateur dans [CoreScope Observers](https://live.meshcore.ca/#/observers).
+2. Attendez de l’activité radio normale à proximité.
+3. Confirmez la présence d’un paquet récent dans [CoreScope Packets](https://live.meshcore.ca/#/packets).
+
+Terminez avec [Vérifier votre observateur](../verify.md). Une connexion Wi-Fi et
+MQTT ne confirme pas que les paquets se sont rendus à CoreScope.
+
+## Récupération { #recovery }
+
+Si la carte ne redémarre pas ou ne se reconnecte pas :
+
+1. débranchez-la et rebranchez-la avec un câble de données fiable;
+2. utilisez la séquence de récupération ou de démarrage propre à la carte publiée par l’outil;
+3. reprogrammez exactement la bonne cible sans l’effacer de nouveau, sauf si la récupération l’exige;
+4. restaurez l’identité et les paramètres sauvegardés uniquement sur la carte prévue;
+5. refaites les vérifications locales dans l’interface en ligne de commande avant de remettre la carte en service.
+
+Si le micrologiciel fonctionne, mais que l’observation échoue, restaurez les
+paramètres précédents consignés ou reprogrammez la dernière version vérifiée et
+fonctionnelle. Gardez privée la sauvegarde de l’identité privée.
+
+## Si la vérification échoue
+
+Consultez le [dépannage](../troubleshooting.md). Partagez uniquement les
+résultats des commandes de lecture après avoir retiré les renseignements privés.
+Ne partagez jamais les commandes Wi-Fi ni une clé privée.
diff --git a/docs/analyzer/builds/mqtt-firmware.md b/docs/analyzer/builds/mqtt-firmware.md
index ccb715e..c0145be 100644
--- a/docs/analyzer/builds/mqtt-firmware.md
+++ b/docs/analyzer/builds/mqtt-firmware.md
@@ -1,242 +1,164 @@
-# MQTT Firmware (Standalone Observer)
+---
+title: Build a standalone MQTT observer
+description: Turn a supported Wi-Fi LoRa board into a dedicated CoreScope observer.
+audience:
+ - observer-operators
+task: configure-standalone-observer
+scope: experimental
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: advanced
+estimated_time: 35 minutes
+destructive: true
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+page_scripts:
+ - assets/javascripts/analyzer-command-builder.js?v=20260722-2
+---
+
+# Build a standalone MQTT observer
+
+Dedicated observer firmware lets a supported Wi-Fi LoRa board send nearby packet telemetry without a separate host computer.
+
+## Is this method right for you?
+
+
+
Good fitYou can dedicate a supported Wi-Fi LoRa board to observing.
+
Choose another methodRemoteTerm, Home Assistant, PyMC, or a nearby USB host already manages the radio.
+
What stays onlineThe board, stable power, 2.4 GHz Wi-Fi, and internet access.
+
+
+This uses third-party firmware. Confirm the board and build shown by the flasher before continuing.
+
+## Confirm the firmware source
+
+| Item | Reference |
+|---|---|
+| Flasher | [observer.gessaman.com](https://observer.gessaman.com/) |
+| Recorded source branch | `mqtt-bridge-implementation-flex` |
+| Recorded source commit | `c0c845f5` |
+| Broker presets | `meshcore-ca-1` and `meshcore-ca-2` |
+
+The commit records the source used when this guide was reviewed; it does not prove the live flasher still uses that build. Stop if the flasher shows a board, source, or build you have not checked.
+
+## Before you start
+
+!!! danger "Back up before enabling Erase device"
+ Erasing can remove the private identity key, name, radio settings, region data, and other saved configuration. Back up the private key and record the board, role, firmware version, and settings. If an identity cannot be backed up, stop and use a new board or ask for help.
+
+- [ ] Confirm the exact board is listed by the observer flasher.
+- [ ] Decide whether this node should repeat traffic or observe only.
+- [ ] Back up the existing identity and settings.
+- [ ] Keep a known-good data USB cable and the board's recovery method nearby.
+- [ ] Choose a real [location code](../iata-codes.md).
+
+## What this changes
-Flash standalone MQTT observer firmware with the [MeshCore observer flasher](https://observer.gessaman.com/). MeshCore Canada no longer hosts separate observer firmware binaries on this page.
+Flashing replaces the board firmware. Setup then changes its name, radio values, path-hash mode, location code, Wi-Fi credentials, broker presets, packet mode, and repeat behaviour.
-!!! success "MeshCore Canada presets verified"
- The observer firmware currently offered by `observer.gessaman.com` is built from Adam Gessaman's `mqtt-bridge-implementation-flex` branch at commit `c0c845f5`. That branch includes the built-in presets `meshcore-ca-1` and `meshcore-ca-2`, pointing to `mqtt1.meshcore.ca` and `mqtt2.meshcore.ca`.
+The command builder runs locally in this browser. It does not store Wi-Fi fields, place them in the URL, or include them in the default preview.
-## Flash The Observer
+## Set up
+
+### 1. Flash the board
1. Open [observer.gessaman.com](https://observer.gessaman.com/).
-2. Pick your board under **MQTT Observer Firmware**.
+2. Under **MQTT Observer Firmware**, choose the exact supported board.
3. Choose **Repeater** or **Room Server**.
-4. For a new board or a board you are repurposing, enable **Erase device** and flash the merged image.
-5. When flashing finishes, use **Configure via USB** for the repeater or room server setup screen, or use **Console** for CLI setup.
+4. For a new or intentionally repurposed board, enable **Erase device** only after the backup gate above.
+5. Flash the merged image.
+6. When flashing finishes, prefer **Configure via USB**. Use **Console** only for settings the setup screen does not expose.
-!!! warning "First flash can erase settings"
- First flashing observer firmware, especially on boards with a changed partition layout, can wipe stored settings and identity data. Back up an existing device private key before repurposing it.
+The flasher should report completion and the board should reconnect over USB.
-## Required MeshCore Canada Settings
+### 2. Enter the shared settings
-Use the repeater or room server setup screen where possible. If a setting is not exposed in the setup screen, use the console on the flasher page and paste the CLI commands below.
+Use the local mesh settings. When no community override exists, the Canadian onboarding baseline is:
| Setting | Value |
-|---------|-------|
+|---|---|
| Radio preset | **USA/Canada (Recommended)** |
| Raw radio values | `910.525 MHz / 62.5 kHz / SF7 / CR5` |
-| CLI radio command | `set radio 910.525,62.5,7,5` |
-| Path hash mode | 3-byte advert path hashes: `set path.hash.mode 2` |
-| MQTT slot 1 | `meshcore-ca-1` |
-| MQTT slot 2 | `meshcore-ca-2` |
-| IATA | A real 3-letter IATA airport code near the observer |
-| WiFi | 2.4 GHz network credentials |
-
-!!! note "IATA codes"
- Use a real airport code such as `YOW`, `YYZ`, `YUL`, `YVR`, or `YYC`. Do not use placeholders such as `XXX` or `HOME`. Do not use `CAN` as shorthand for Canada; it is an airport code for Guangzhou. See the [IATA region code list](../iata-codes.md) for Canadian quick choices.
+| Path hashes | 3 bytes (`set path.hash.mode 2`) |
+| Primary preset | `meshcore-ca-1` |
+| Backup preset | `meshcore-ca-2` |
+| Wi-Fi | A 2.4 GHz network |
-## Command Builder
+### 3. Build the commands
-Use this builder to create a CLI block for the flasher **Console**. It runs only in your browser.
+The CLI has no documented general quoting contract. The builder rejects spaces, quotes, backslashes, control characters, and other ambiguous Wi-Fi values. Use **Configure via USB** for a network it cannot represent safely.
-
+
+
-
-
+
Loading Canadian location suggestions…
+
SSID and password stay in this page only. They are cleared when you leave and hidden from the preview until you reveal them.
+
+
-
+
+
+
-
+
Complete the required fields to build commands.
-
-
-
-
-## Manual CLI Reference
-
-If you prefer to type commands manually, replace `YOW`, `YourWiFiNetwork`, and `YourWiFiPassword` with your own values:
+Check the non-sensitive summary and redacted preview. Reveal and copy commands only on a trusted computer. The clipboard will contain Wi-Fi credentials, so clear it after use.
+
+### Enter commands by hand
+
+If you prefer manual entry, set the non-sensitive values first:
```text
set name YOW-Repeater-01
set radio 910.525,62.5,7,5
set path.hash.mode 2
set mqtt.iata YOW
-set wifi.ssid YourWiFiNetwork
-set wifi.pwd YourWiFiPassword
set wifi.powersave none
set mqtt1.preset meshcore-ca-1
set mqtt2.preset meshcore-ca-2
@@ -252,65 +174,64 @@ set mqtt.tx advert
set bridge.enabled on
set repeat on
advert
-reboot
```
-For an observe-only node that should not repeat mesh traffic, use:
+Replace the name and location code. Use `set repeat off` for an observe-only node.
+
+Enter credentials directly in the device console:
```text
-set repeat off
+set wifi.ssid
+set wifi.pwd
+reboot
```
-## Verify
+Do not paste completed credential lines into chat, issues, screenshots, logs, or saved notes. Do not add generic shell quotes to a device CLI value.
+
+## What you should see
-After the device reboots, reopen the flasher **Console** and run:
+After reboot, run:
```text
+get name
get wifi.status
get mqtt.iata
get mqtt1.preset
get mqtt2.preset
get mqtt.status
+get mqtt.packets
+get bridge.enabled
get path.hash.mode
```
-Expected broker presets:
+The board is configured correctly when:
-```text
-get mqtt1.preset
-> meshcore-ca-1
-get mqtt2.preset
-> meshcore-ca-2
-```
+- Wi-Fi and MQTT report connected;
+- the location code is correct;
+- presets are `meshcore-ca-1` and `meshcore-ca-2`;
+- packet publishing and bridge mode are on; and
+- path hash mode is `2`.
-Once WiFi and MQTT are connected, use [Check Your Observer](../verify.md) to confirm packets are reaching MeshCore Canada.
+## Verify in CoreScope
-## Useful Links
+1. Find the observer in [CoreScope Observers](https://live.meshcore.ca/#/observers).
+2. Wait for normal nearby radio activity.
+3. Confirm a recent packet in [CoreScope Packets](https://live.meshcore.ca/#/packets).
-
+Finish with [Check your observer](../verify.md). Connected Wi-Fi and MQTT are not proof that packets reached CoreScope.
-- :material-flash:{ .lg .middle } **Observer Flasher**
+## Recovery
- ---
+If the board does not reboot or reconnect:
- Flash MQTT observer firmware and open the serial console.
+1. disconnect and reconnect with a known-good data cable;
+2. use the board-specific recovery/boot sequence published by the flasher;
+3. reflash the exact correct target without erasing again unless recovery requires it;
+4. restore the backed-up identity and settings only to the intended board; and
+5. repeat the local CLI checks before returning the board to service.
- [:octicons-arrow-right-24: observer.gessaman.com](https://observer.gessaman.com/)
+If the firmware runs but observing fails, restore the recorded prior settings or reflash the last known-good reviewed build. Keep the private identity backup private.
-- :material-airplane:{ .lg .middle } **IATA Region Codes**
+## If verification fails
- ---
-
- Pick the real 3-letter airport code nearest to the observer.
-
- [:octicons-arrow-right-24: IATA codes](../iata-codes.md)
-
-- :material-check-circle:{ .lg .middle } **Check Your Observer**
-
- ---
-
- Confirm that the observer is online and reporting.
-
- [:octicons-arrow-right-24: verify status](../verify.md)
-
-
+Use [Troubleshooting](../troubleshooting.md). Share only read-command output after removing private details. Never share Wi-Fi commands or a private key.
diff --git a/docs/analyzer/builds/pymc.fr.md b/docs/analyzer/builds/pymc.fr.md
new file mode 100644
index 0000000..4373efa
--- /dev/null
+++ b/docs/analyzer/builds/pymc.fr.md
@@ -0,0 +1,155 @@
+---
+title: Observer avec PyMC
+description: Transmettez à CoreScope les paquets d’un service de répéteur PyMC déjà en fonction.
+audience:
+ - observer-operators
+ - service-operators
+task: configure-pymc-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: advanced
+estimated_time: 25 minutes
+destructive: true
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Observer avec PyMC
+
+Ajoutez les points de terminaison principal et de secours de MeshCore Canada à
+un service de répéteur PyMC que vous exploitez déjà.
+
+## Cette méthode vous convient-elle?
+
+
+
Utilisez PyMC siUn service de répéteur PyMC fonctionnel gère déjà la radio.
+
Choisissez autre chose siVous installeriez Python et PyMC uniquement pour observer le trafic.
+
Gardez en fonctionL’hôte PyMC, le service, la connexion radio et l’accès Internet.
+
+
+## Vérifier votre installation
+
+Cette procédure vise un service systemd Linux nommé `pymc-repeater` dont la
+configuration se trouve dans `/etc/pymc_repeater/config.yaml`. Confirmez la
+version, le nom du service, le chemin et le format de configuration avant toute
+modification. Si les chemins diffèrent, ou si vous utilisez macOS ou Windows,
+suivez la documentation de la version de PyMC installée.
+
+## Avant de commencer { #before-you-start }
+
+- [ ] PyMC fonctionne correctement avant le changement.
+- [ ] Vous connaissez les véritables chemins du service et de sa configuration.
+- [ ] La radio utilise les paramètres du réseau maillé local.
+- [ ] Vous avez choisi un véritable [code d’emplacement](../iata-codes.md).
+- [ ] Vous pouvez restaurer une copie de sauvegarde appartenant à `root`.
+
+Notez l’état actuel du service :
+
+```bash
+sudo systemctl status pymc-repeater --no-pager
+sudo cp -- /etc/pymc_repeater/config.yaml /etc/pymc_repeater/config.yaml.pre-meshcore-ca
+```
+
+## Ce qui sera modifié
+
+Vous modifierez la configuration YAML de PyMC et redémarrerez le service. Le
+changement ajoute un code d’emplacement et deux entrées de courtier chiffrées
+et authentifiées par jeton; il ne modifie pas le micrologiciel de la radio.
+
+## Configuration
+
+Dans `/etc/pymc_repeater/config.yaml`, définissez le code d’emplacement dans
+`mqtt` :
+
+```yaml
+mqtt:
+ iata_code: YOW
+```
+
+Remplacez `YOW` par le véritable code le plus près de l’observateur.
+
+Sous `mqtt.brokers`, ajoutez :
+
+```yaml
+- name: MeshCore-CA
+ enabled: true
+ host: mqtt1.meshcore.ca
+ port: 443
+ transport: websockets
+ format: letsmesh
+ disallowed_packet_types: []
+ retain_status: false
+ tls:
+ enabled: true
+ insecure: false
+ use_jwt_auth: true
+ audience: mqtt1.meshcore.ca
+- name: MeshCore-CA Backup
+ enabled: true
+ host: mqtt2.meshcore.ca
+ port: 443
+ transport: websockets
+ format: letsmesh
+ disallowed_packet_types: []
+ retain_status: false
+ tls:
+ enabled: true
+ insecure: false
+ use_jwt_auth: true
+ audience: mqtt2.meshcore.ca
+```
+
+N’ajoutez pas de mot de passe MQTT. Laissez le courriel facultatif du
+responsable vide, sauf s’il est nécessaire à l’exploitation.
+
+Relisez la section modifiée, puis redémarrez le service :
+
+```bash
+sudo systemctl restart pymc-repeater
+sudo systemctl status pymc-repeater --no-pager
+```
+
+Si le service échoue, consultez un court extrait du journal local :
+
+```bash
+sudo journalctl -u pymc-repeater -n 80 --no-pager
+```
+
+Examinez les résultats avant de les partager. Suivez la
+[liste de caviardage](../troubleshooting.md#what-to-share-when-asking-for-help).
+
+## Ce que vous devriez voir
+
+Le service demeure actif sans erreur YAML, TLS ou de jeton, et son nombre de
+paquets change lorsque la radio capte du trafic à proximité.
+
+## Vérifier dans CoreScope { #check-in-corescope }
+
+1. Trouvez l’observateur dans [CoreScope Observers](https://live.meshcore.ca/#/observers).
+2. Attendez de l’activité normale à proximité.
+3. Confirmez la présence d’un paquet récent dans [CoreScope Packets](https://live.meshcore.ca/#/packets).
+
+Terminez avec [Vérifier votre observateur](../verify.md). Un service systemd en
+bon état ne confirme pas que les paquets se sont rendus à CoreScope.
+
+## Récupération { #recovery }
+
+Restaurez exactement la sauvegarde créée avant la modification :
+
+```bash
+sudo cp -- /etc/pymc_repeater/config.yaml.pre-meshcore-ca /etc/pymc_repeater/config.yaml
+sudo systemctl restart pymc-repeater
+sudo systemctl status pymc-repeater --no-pager
+```
+
+Conservez le fichier défectueux en privé s’il peut servir au diagnostic. Ne le
+publiez pas sans d’abord retirer les renseignements secrets et personnels.
+
+## Si la vérification échoue
+
+Consultez le [dépannage](../troubleshooting.md). Indiquez la version de PyMC, le
+nom du service, la première étape qui a échoué et un court extrait caviardé du
+journal.
diff --git a/docs/analyzer/builds/pymc.md b/docs/analyzer/builds/pymc.md
index 2ff309d..de60dee 100644
--- a/docs/analyzer/builds/pymc.md
+++ b/docs/analyzer/builds/pymc.md
@@ -1,33 +1,69 @@
-# PyMC (Python MeshCore)
+---
+title: Observe with PyMC
+description: Send packets from an existing PyMC repeater service to CoreScope.
+audience:
+ - observer-operators
+ - service-operators
+task: configure-pymc-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: advanced
+estimated_time: 25 minutes
+destructive: true
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Observe with PyMC
+
+Add MeshCore Canada's primary and backup endpoints to a PyMC repeater service you already operate.
+
+## Is this method right for you?
+
+
+
Use PyMC ifA working PyMC repeater service already manages the radio.
+
Use something else ifYou would install Python and PyMC only to observe traffic.
+
Keep onlineThe PyMC host, service, radio connection, and internet access.
+
+
+## Check your installation
+
+This procedure is for a Linux systemd service named `pymc-repeater` with config at `/etc/pymc_repeater/config.yaml`. Confirm the version, service name, path, and config format before editing. For different paths, macOS, or Windows, follow the installed PyMC version's documentation.
+
+## Before you start
+
+- [ ] PyMC is healthy before the change.
+- [ ] You know the actual service and config paths.
+- [ ] The radio uses the local mesh settings.
+- [ ] You chose a real [location code](../iata-codes.md).
+- [ ] You can restore a root-owned backup.
+
+Record the current service state:
-Add the MeshCore.ca broker pair to an existing pyMC repeater installation. PyMC connects to your MeshCore device and forwards traffic to MQTT brokers using a YAML config file.
-
-## Prerequisites
+```bash
+sudo systemctl status pymc-repeater --no-pager
+sudo cp -- /etc/pymc_repeater/config.yaml /etc/pymc_repeater/config.yaml.pre-meshcore-ca
+```
-| Requirement | Details |
-|-------------|---------|
-| PyMC | A working pyMC repeater installation |
-| IATA Code | Your real 3-letter IATA airport code (e.g. `YOW` for Ottawa) |
-| Mesh Settings | Repeater radio uses `USA/Canada (Recommended)` and 3-byte path hashes |
+## What this changes
-## Configuration
+You will edit PyMC's YAML config and restart the service. The change adds a location code and two encrypted, token-authenticated broker entries; it does not change the radio firmware.
-### 1. Set Your IATA Code
+## Set up
-In `/etc/pymc_repeater/config.yaml`, set your region code under the MQTT section:
+In `/etc/pymc_repeater/config.yaml`, set the location code inside `mqtt`:
```yaml
mqtt:
iata_code: YOW
```
-Use the real 3-letter airport code nearest to you. The public broker rejects placeholders and made-up region names such as `XXX` or `HOME`. Do not use `CAN` as shorthand for Canada; it is a real airport code for Guangzhou and will tag your observer to the wrong region.
+Replace `YOW` with the real code nearest the observer.
-Also confirm the underlying repeater is on the MeshCore Canada network settings: **USA/Canada (Recommended)**, or raw radio values `910.525 MHz / 62.5 kHz / SF7 / CR5`, with 3-byte path hashes.
-
-### 2. Add the Broker Block
-
-Paste the following under `mqtt.brokers` in your config file:
+Under `mqtt.brokers`, add:
```yaml
- name: MeshCore-CA
@@ -58,35 +94,47 @@ Paste the following under `mqtt.brokers` in your config file:
audience: mqtt2.meshcore.ca
```
-### 3. Optional Fields
+Do not add an MQTT password. Leave optional owner email blank unless it is operationally needed.
-You can also set these optional fields in the `mqtt` section:
+Review the edited section, then restart:
-```yaml
-mqtt:
- owner: "your-public-key"
- email: "you@example.com"
+```bash
+sudo systemctl restart pymc-repeater
+sudo systemctl status pymc-repeater --no-pager
```
-### 4. Restart the Service
+If the service fails, inspect a short local excerpt:
```bash
-sudo systemctl restart pymc-repeater
+sudo journalctl -u pymc-repeater -n 80 --no-pager
```
-## Quick Reference
+Review any output before sharing it. Follow the [redaction checklist](../troubleshooting.md#what-to-share-when-asking-for-help).
+
+## What you should see
+
+The service stays active without YAML, TLS, or token errors, and its packet count changes when the radio hears nearby traffic.
+
+## Verify in CoreScope
+
+1. Find the observer in [CoreScope Observers](https://live.meshcore.ca/#/observers).
+2. Wait for normal nearby activity.
+3. Confirm a recent packet in [CoreScope Packets](https://live.meshcore.ca/#/packets).
+
+Finish with [Check your observer](../verify.md). A healthy systemd service is not proof that packets reached CoreScope.
+
+## Recovery
+
+Restore the exact backup made before editing:
+
+```bash
+sudo cp -- /etc/pymc_repeater/config.yaml.pre-meshcore-ca /etc/pymc_repeater/config.yaml
+sudo systemctl restart pymc-repeater
+sudo systemctl status pymc-repeater --no-pager
+```
-| Setting | Value |
-|---------|-------|
-| Config file | `/etc/pymc_repeater/config.yaml` |
-| Primary broker | `mqtt1.meshcore.ca` |
-| Backup broker | `mqtt2.meshcore.ca` |
-| Port | `443` |
-| Transport | `websockets` |
-| TLS | Enabled, verified |
-| Auth | JWT token (`use_jwt_auth: true`) |
-| Format | `letsmesh` |
+Keep the failed file privately if it helps with diagnosis. Do not post it without removing secrets and personal details.
-## Verify
+## If verification fails
-After restarting, head to [Check Your Observer](../verify.md) to confirm it's reporting correctly.
+Use [Troubleshooting](../troubleshooting.md). Include the PyMC version, service name, first failed stage, and a short redacted log excerpt.
diff --git a/docs/analyzer/data-collection-access.fr.md b/docs/analyzer/data-collection-access.fr.md
new file mode 100644
index 0000000..1bdf715
--- /dev/null
+++ b/docs/analyzer/data-collection-access.fr.md
@@ -0,0 +1,91 @@
+---
+title: Données des observateurs et vie privée
+description: Voyez ce qu’un observateur envoie, où ces données apparaissent et comment éviter d’y inclure des renseignements privés.
+audience:
+ - observer-operators
+ - community-members
+task: understand-observer-data
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 6 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Données des observateurs et vie privée
+
+Considérez comme publiques toutes les données captées par un observateur. Un
+observateur ne déchiffre pas les messages privés, mais il peut transmettre les
+détails des paquets et des renseignements sur son état. N’inscrivez pas de noms,
+d’emplacements, d’identifiants ou d’autres renseignements sensibles dans les
+messages MeshCore.
+
+
+
ObservateurTransmet les données de paquets et l’état provenant des paramètres radio qu’il utilise.
+
CoreScopePeut afficher publiquement les observateurs, les paquets et les données cartographiques.
+
CourtierLes abonnements directs sont restreints, mais les données affichées dans CoreScope demeurent publiques.
+
+
+## Résumé de la politique
+
+| Question | Réponse |
+|---|---|
+| Qui gère le service? | Les administrateurs de l’infrastructure de MeshCore Canada |
+| Où puis-je poser mes questions? | [Forum de MeshCore Canada](https://forum.meshcore.ca/) |
+| Combien de temps les données sont-elles conservées? | Aucune période de conservation publique n’a encore été annoncée |
+
+Ne présumez pas que les données seront supprimées après une période précise.
+Communiquez avec l’équipe d’infrastructure si vous devez connaître l’échéancier
+de conservation ou de suppression.
+
+## Parcours des données
+
+
+
Paquet radiotransmis sur le réseau maillé configuré
+
Observateurcapte et transmet la télémétrie
+
Infrastructurela reçoit et peut la conserver
+
CoreScopepeut l’afficher publiquement
+
+
+Changer le préréglage radio modifie ce que l’observateur peut capter. Le choix
+d’un canal public ou privé ne rend pas privée la télémétrie qui accompagne les
+paquets.
+
+## Collecte, accès et conservation
+
+| Donnée | Pourquoi elle est utilisée | Où elle peut apparaître | Qui peut y accéder | Conservation |
+|---|---|---|---|---|
+| État de l’observateur | Indiquer si un observateur est en ligne | CoreScope | Visiteurs; exploitants de l’infrastructure | Non précisée publiquement |
+| Télémétrie des paquets captés | Montrer l’activité du réseau et aider à diagnostiquer la couverture | Vues de paquets et cartes de CoreScope | Visiteurs; abonnés autorisés; exploitants de l’infrastructure | Non précisée publiquement |
+| Code d’emplacement | Regrouper un observateur près d’un lieu connu | Listes d’observateurs, sujets et cartes | Visiteurs et utilisateurs du courtier | Suit la conservation de l’observateur ou des paquets |
+| Coordonnées facultatives du responsable | Aider à identifier un service | Données d’état selon l’intégration | Peuvent être transmises à l’infrastructure et à CoreScope | Non précisée publiquement |
+| Identifiants du courtier | Authentifier l’observateur | Devraient rester dans l’intégration locale | Exploitant local et service d’authentification | Ne jamais les inclure dans des diagnostics publics |
+
+MeshCore Canada n’offre pas d’abonnement direct général au courtier. L’accès
+direct est limité à CoreScope, aux administrateurs des réseaux maillés locaux
+et aux personnes autorisées par les administrateurs de l’infrastructure.
+
+## Où les données apparaissent
+
+[CoreScope](https://live.meshcore.ca/) affiche des renseignements sur les
+observateurs, les paquets et la carte. D’autres services autorisés de MeshCore
+Canada peuvent utiliser le même flux.
+
+Les contrôles d’accès au courtier ne rendent pas les renseignements privés une
+fois que CoreScope les affiche.
+
+## Avant d’exploiter un observateur
+
+- [ ] Informez les personnes qui utilisent le réseau local qu’un observateur est actif.
+- [ ] Utilisez un nom général pour l’observateur, et non une adresse résidentielle ou le nom d’une personne.
+- [ ] Laissez les champs facultatifs de courriel du responsable vides, sauf s’ils sont nécessaires à l’exploitation.
+- [ ] Ne collez jamais de mots de passe Wi-Fi, de jetons MQTT, de clés privées ou de journaux non caviardés dans une demande d’aide.
+- [ ] Lisez les étapes de vérification et de récupération propres à la méthode avant d’apporter des changements.
+
+Retournez à [Choisir une méthode d’observation](intro.md), ou voyez
+[quoi retirer avant de demander de l’aide](troubleshooting.md#what-to-share-when-asking-for-help).
diff --git a/docs/analyzer/data-collection-access.md b/docs/analyzer/data-collection-access.md
index 518d2e8..fac423e 100644
--- a/docs/analyzer/data-collection-access.md
+++ b/docs/analyzer/data-collection-access.md
@@ -1,47 +1,77 @@
-# MQTT Data Collection & Access
-
-!!! warning "Treat MeshCore RF traffic as public data"
- MeshCore traffic is intended for shared mesh use, and different networks may use different presets or frequencies (including non-default settings). All channels that use a shared public key (and private keys) should be considered inherently insecure. Any node transmitting MeshCore packets over matching settings can be heard by observers on that mesh, not just one published default profile. Traffic forwarded over MQTT through this path should be treated as potentially public. Do not transmit names, locations, notes, or other personal information unless you are comfortable with that information being stored and viewable publicly. Assume that even with encryption on a private channel / setting can potentially be collected and decrypted by anyone with the means and know-how to do so.
-
-## What We Collect
-
-MeshCore Canada MQTT receives packet data from observer nodes that capture MeshCore packets and forward telemetry from matched channels.
-
-Observers listen for all MeshCore traffic they can hear on the channels and presets they are configured for. If a packet is heard by an observer and that observer has packet publishing enabled, that traffic can be sent to the MeshCore Canada MQTT brokers.
-
-## Where Data Goes
-
-| Step | What happens |
-|------|--------------|
-| Radio traffic | Nodes transmit MeshCore packets on the frequencies and settings configured for their local mesh and presets. |
-| Observer capture | MeshCore Canada observers and other authorized observers listen to all traffic they can hear on their configured channels. |
-| MQTT publish | Observer paths publish packet data to MeshCore Canada MQTT infrastructure. |
-| Storage and display | Data is stored on MeshCore Canada infrastructure and may be displayed by Beacon, CoreScope, and other public websites operated by MeshCore Canada or approved third-party operators. |
-
-## MQTT Subscription Access
-
-Direct MQTT subscription access is not handed out to everyone. It is limited to local mesh administrators, approved tools, and people approved by MeshCore Canada administration.
-
-Even when direct broker subscription access is limited, the data can still be viewable by everyone through Beacon, CoreScope, and other public websites that consume the MQTT feed using approved MQTT read accounts.
-
-## MQTT Read Access
-
-| Tool or service | Operator | Purpose |
-|-----------------|----------|---------|
-| Beacon | MeshCore Canada operators | Public viewer for MeshCore packet data. |
-| CoreScope (`live.meshcore.ca`) | MeshCore Canada operators | Public observer, packet, and map tools. |
-
-## Infrastructure Administrators
-
-The MeshCore Canada infrastructure administrators control the MQTT brokers and related infrastructure.
-
-| Administrator | Profile |
-|---------------|---------|
-| Mr. Alderson | [github.com/MrAlders0n](https://github.com/MrAlders0n) |
-| Ded | [github.com/446564](https://github.com/446564) |
-| n30nex | [github.com/n30nex](https://github.com/n30nex) |
-| Kranic | [forum.meshcore.ca/u/djkranic](https://forum.meshcore.ca/u/djkranic) |
-
-Questions about privacy, MQTT access, or the MeshCore Canada project should be directed to these administrators.
-
-General discussion and support is also available on the forum at [https://forum.meshcore.ca/](https://forum.meshcore.ca/).
+---
+title: Observer data and privacy
+description: See what an observer sends, where it appears, and how to keep private information out of it.
+audience:
+ - observer-operators
+ - community-members
+task: understand-observer-data
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 6 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Observer data and privacy
+
+Treat anything heard by an observer as public. An observer does not decrypt private messages, but it can forward packet details and status information. Do not put private names, locations, credentials, or other sensitive information in MeshCore messages.
+
+
+
ObserverSends packet data and status from the radio settings it uses.
+
CoreScopeCan show observer, packet, and map data publicly.
+
BrokerDirect subscriptions are restricted, but data shown in CoreScope is still public.
+
+
+## Policy summary
+
+| Question | Answer |
+|---|---|
+| Who manages this? | MeshCore Canada infrastructure administrators |
+| Where can I ask questions? | [MeshCore Canada forum](https://forum.meshcore.ca/) |
+| How long is data kept? | A public retention period has not yet been published |
+
+Do not assume data will be deleted after a certain time. Ask the infrastructure team if you need a retention or deletion timeline.
+
+## Data flow
+
+
+
Radio packettransmitted on the configured mesh
+
Observerhears and forwards telemetry
+
Infrastructurereceives and may store it
+
CoreScopemay display it publicly
+
+
+Changing the radio preset changes what the observer can hear. Public and private channel choices do not make the surrounding packet telemetry private.
+
+## Collection, access, and retention
+
+| Data | Why it is used | Where it may appear | Who can access it | Retention |
+|---|---|---|---|---|
+| Observer status | Show whether an observer is online | CoreScope | Public viewers; infrastructure operators | Not publicly specified |
+| Heard packet telemetry | Show mesh activity and help diagnose coverage | CoreScope packet views and maps | Public viewers; approved subscribers; infrastructure operators | Not publicly specified |
+| Location code | Group an observer near a known place | Observer lists, topics, maps | Public viewers and broker users | Follows observer/packet retention |
+| Optional owner details | Help identify a service | Integration-dependent status data | May reach infrastructure and CoreScope | Not publicly specified |
+| Broker credentials | Authenticate the observer | Should remain only in the local integration | Local operator and authentication service | Never include in public diagnostics |
+
+MeshCore Canada does not offer general direct broker subscriptions. Direct access is limited to CoreScope, local mesh administrators, and people approved by the infrastructure administrators.
+
+## Where it appears
+
+[CoreScope](https://live.meshcore.ca/) shows observer, packet, and map information. Other approved MeshCore Canada services may use the same feed.
+
+Broker access controls do not make information private once CoreScope displays it.
+
+## Before you operate an observer
+
+- [ ] Tell people using the local mesh that an observer is active.
+- [ ] Use a broad observer name, not a home address or person's name.
+- [ ] Leave optional owner email fields blank unless they are operationally needed.
+- [ ] Never paste Wi-Fi passwords, MQTT tokens, private keys, or unredacted logs into support messages.
+- [ ] Read the method's verification and recovery steps before making changes.
+
+Return to [Choose an observer setup](intro.md), or see [what to remove before asking for help](troubleshooting.md#what-to-share-when-asking-for-help).
diff --git a/docs/analyzer/iata-codes.fr.md b/docs/analyzer/iata-codes.fr.md
new file mode 100644
index 0000000..e175b11
--- /dev/null
+++ b/docs/analyzer/iata-codes.fr.md
@@ -0,0 +1,74 @@
+---
+title: Trouver le code d’emplacement d’un observateur
+description: Trouvez, dans notre liste canadienne, le code à trois lettres de l’aéroport le plus proche de votre observateur.
+audience:
+ - observer-operators
+task: choose-location-code
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-19
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 3 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+page_scripts:
+ - assets/javascripts/analyzer-location-codes.js?v=20260722-2
+---
+
+# Trouver le code d’emplacement d’un observateur
+
+Un observateur indique sa zone générale avec le code à trois lettres d’un
+aéroport réel. Ce code ne définit pas les limites d’une région MeshCore.
+
+Choisissez le code de l’aéroport le plus proche de l’observateur. Utilisez le
+même code dans chaque entrée du courtier.
+
+
+
+
+ Rechercher par code ou par lieu
+
+
+
+ Province ou territoire
+
+
+
+
Chargement des codes d’emplacement…
+
+
+
+
+
Code
+
Lieu
+
Province ou territoire
+
+
+
+
+
+
+
+## À propos de cette liste
+
+Les [données de référence des codes d’emplacement](location-codes.json)
+alimentent l’outil de recherche et les suggestions du générateur de commandes.
+Il s’agit d’une courte liste canadienne organisée pour ce site, et non d’un
+registre officiel complet des codes d’aéroport.
+
+Si le code de l’aéroport le plus proche n’y figure pas :
+
+1. confirmez-le auprès d’une source aéroportuaire fiable;
+2. entrez le code à trois lettres dans une méthode qui accepte du texte libre;
+3. demandez à MeshCore Canada d’ajouter ce lieu à la liste.
+
+N’utilisez pas `CAN` pour représenter le Canada : il s’agit du code d’un
+aéroport de Guangzhou. N’utilisez pas non plus de valeurs temporaires comme
+`XXX` ou `HOME`.
+
+Retournez à [Choisir une méthode d’observation](intro.md).
diff --git a/docs/analyzer/iata-codes.md b/docs/analyzer/iata-codes.md
index 7523741..1c6ce73 100644
--- a/docs/analyzer/iata-codes.md
+++ b/docs/analyzer/iata-codes.md
@@ -1,147 +1,67 @@
-# IATA Region Codes
-
-Each observer identifies its region with a real 3-letter IATA airport code. Use the airport code nearest to the observer's real location.
-
-The firmware and helper scripts are not limited to the list below. If your nearest real IATA code is missing here, you can still use it and the public broker will accept it as long as it is a valid airport code. The live site will add observed regions to the picker automatically, but codes missing from the friendly-name list may appear as the bare code until we add a label.
-
-Do not use placeholders or made-up region names such as `XXX` or `HOME`. Do not use `CAN` as shorthand for Canada; it is a real airport code for Guangzhou and will tag your observer to the wrong region.
-
-Host-side helper scripts show this same quick list interactively when you omit `--iata`.
-
-??? note "Ontario"
-
- | Code | Region |
- |------|--------|
- | YYZ | Toronto (Pearson) |
- | YTZ | Toronto (Billy Bishop) |
- | YOW | Ottawa |
- | YHM | Hamilton |
- | YKF | Kitchener / Waterloo |
- | YXU | London |
- | YOO | Oshawa |
- | YKZ | Buttonville / Markham |
- | YAM | Sault Ste. Marie |
- | YQT | Thunder Bay |
- | YSB | Sudbury |
- | YTS | Timmins |
- | YQG | Windsor |
- | YYB | North Bay |
- | YGK | Kingston |
- | YPQ | Peterborough |
- | YTR | Trenton / Quinte West |
- | YHD | Dryden |
- | YPL | Pickle Lake |
-
-??? note "Quebec"
-
- | Code | Region |
- |------|--------|
- | YUL | Montreal (Trudeau) |
- | YMX | Montreal (Mirabel) |
- | YQB | Quebec City |
- | YND | Gatineau (Ottawa area) |
- | YBG | Bagotville / Saguenay |
- | YVO | Val-d'Or |
- | YHU | Montreal (St-Hubert) |
- | YRJ | Roberval |
- | YGL | La Grande Riviere |
- | YSC | Sherbrooke |
- | YTQ | Tasiujaq |
- | YUY | Rouyn-Noranda |
- | YZV | Sept-Iles |
- | YGP | Gaspe |
- | YRQ | Trois-Rivieres |
- | YBC | Baie-Comeau |
-
-??? note "British Columbia"
-
- | Code | Region |
- |------|--------|
- | YVR | Vancouver |
- | YYJ | Victoria |
- | YXX | Abbotsford / Fraser Valley |
- | YLW | Kelowna |
- | YXS | Prince George |
- | YPR | Prince Rupert |
- | YXT | Terrace |
- | YQQ | Comox / Courtenay |
- | YCD | Nanaimo |
- | YYD | Smithers |
- | YDQ | Dawson Creek |
- | YXJ | Fort St. John |
- | YYF | Penticton |
- | YCG | Castlegar |
- | YKA | Kamloops |
- | YXC | Cranbrook |
-
-??? note "Alberta"
-
- | Code | Region |
- |------|--------|
- | YYC | Calgary |
- | YEG | Edmonton |
- | YMM | Fort McMurray |
- | YQU | Grande Prairie |
- | YQL | Lethbridge |
- | YXH | Medicine Hat |
-
-??? note "Saskatchewan"
-
- | Code | Region |
- |------|--------|
- | YQR | Regina |
- | YXE | Saskatoon |
- | YPA | Prince Albert |
-
-??? note "Manitoba"
-
- | Code | Region |
- |------|--------|
- | YWG | Winnipeg |
- | YBR | Brandon |
- | YTH | Thompson |
- | YDN | Dauphin |
- | YPG | Portage la Prairie |
-
-??? note "New Brunswick"
-
- | Code | Region |
- |------|--------|
- | YFC | Fredericton |
- | YSJ | Saint John |
- | YQM | Moncton |
- | ZBF | Bathurst |
-
-??? note "Nova Scotia"
-
- | Code | Region |
- |------|--------|
- | YHZ | Halifax |
- | YQY | Sydney |
- | YQI | Yarmouth |
-
-??? note "Prince Edward Island"
-
- | Code | Region |
- |------|--------|
- | YYG | Charlottetown |
-
-??? note "Newfoundland and Labrador"
-
- | Code | Region |
- |------|--------|
- | YYT | St. John's |
- | YQX | Gander |
- | YDF | Deer Lake |
- | YYR | Goose Bay |
- | YWK | Wabush |
-
-??? note "Territories (YT / NT / NU)"
-
- | Code | Region |
- |------|--------|
- | YXY | Whitehorse (Yukon) |
- | YZF | Yellowknife (NWT) |
- | YFB | Iqaluit (Nunavut) |
- | YEV | Inuvik (NWT) |
- | YHY | Hay River (NWT) |
+---
+title: Find an observer location code
+description: Search the Canadian quick list for the real three-letter airport code nearest to an observer.
+audience:
+ - observer-operators
+task: choose-location-code
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-19
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 3 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+page_scripts:
+ - assets/javascripts/analyzer-location-codes.js?v=20260722-2
+---
+
+# Find an observer location code
+
+An observer uses a real three-letter airport code as a broad location label. It does not define a MeshCore region boundary.
+
+Use the nearest sensible code for the observer's actual area. Use the same code in every broker entry.
+
+
+
+
+ Search by code or place
+
+
+
+ Province or territory
+
+
+
+
Loading location codes…
+
+
+
+
+
Code
+
Place
+
Province or territory
+
+
+
+
+
+
+
+## About this list
+
+The [canonical location-code data](location-codes.json) generates the search tool and the command-builder suggestions. It is a curated Canadian quick list, not a complete official airport-code registry.
+
+If the nearest real airport code is missing:
+
+1. verify it against a reliable airport source;
+2. type the three-letter code into a method that accepts free text; and
+3. ask MeshCore Canada to add the friendly place name.
+
+Do not use `CAN` for Canada; it is an airport code for Guangzhou. Do not use placeholders such as `XXX` or `HOME`.
+
+Return to [Choose an observer method](intro.md).
diff --git a/docs/analyzer/intro.fr.md b/docs/analyzer/intro.fr.md
new file mode 100644
index 0000000..514bd68
--- /dev/null
+++ b/docs/analyzer/intro.fr.md
@@ -0,0 +1,102 @@
+---
+title: Configurer un observateur de réseau
+description: Choisissez la méthode d’observation qui convient à votre installation actuelle, puis vérifiez-la dans CoreScope.
+audience:
+ - observer-operators
+task: choose-observer-method
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 5 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+page_scripts:
+ - assets/javascripts/analyzer-method-chooser.js?v=20260722-2
+---
+
+# Configurer un observateur de réseau
+
+Un observateur transmet à CoreScope les données des paquets MeshCore captés à
+proximité. Il ne déchiffre pas les messages privés et n’a pas besoin de relayer
+le trafic.
+
+## Comment les éléments communiquent
+
+
+
Radiocapte les paquets MeshCore à proximité
+
Observateurtransmet les données des paquets
+
MeshCore Canadales reçoit par deux points de terminaison communs
+
Outils en directCoreScope affiche les observateurs et les paquets
+
+
+Les points de terminaison communs utilisent MQTT, mais vous n’avez pas besoin
+d’apprendre MQTT avant de choisir une méthode.
+
+!!! warning "Les observateurs partagent ce qu’ils captent"
+ Les renseignements sur l’observateur et les données des paquets captés peuvent apparaître dans CoreScope. Ne transmettez pas de renseignements sensibles. Lisez [Données des observateurs et vie privée](data-collection-access.md) avant d’en activer un.
+
+## Choisir votre méthode
+
+Commencez par l’outil qui gère déjà votre radio.
+
+
+
+ Quel élément fait déjà partie de votre installation?
+
+
+
+
+
+Les mêmes choix figurent dans ce tableau :
+
+| Votre installation actuelle | Méthode recommandée | Hôte qui doit rester en fonction |
+|---|---|---|
+| RemoteTerm gère déjà la radio | [RemoteTerm](remoteterm.md) | L’hôte RemoteTerm |
+| Home Assistant utilise déjà MeshCore | [Home Assistant](builds/meshcore-ha.md) | Home Assistant |
+| PyMC gère déjà un répéteur | [PyMC](builds/pymc.md) | L’hôte PyMC |
+| Un hôte Linux ou macOS est relié par USB | [MCtoMQTT](builds/mctomqtt.md) | L’hôte USB |
+| Une carte LoRa Wi-Fi compatible peut y être consacrée | [Micrologiciel MQTT autonome](builds/mqtt-firmware.md) | Aucun hôte distinct |
+
+Si aucune méthode ne convient, demandez conseil sur le
+[forum de MeshCore Canada](https://forum.meshcore.ca/) avant d’installer un
+nouvel outil.
+
+## Ce qu’il faut pour chaque méthode
+
+Quelle que soit la méthode choisie, il vous faut :
+
+- une radio déjà configurée pour le réseau maillé local;
+- un véritable [code d’emplacement](iata-codes.md) à trois lettres;
+- les points de terminaison principal et de secours de MeshCore Canada;
+- des connexions chiffrées avec validation des certificats;
+- la publication des paquets, et non seulement de l’état;
+- un hôte ou une carte qui reste en fonction.
+
+La configuration canadienne de départ est **USA/Canada (Recommended)**,
+`910.525 MHz / 62.5 kHz / SF7 / CR5`, avec le hachage des chemins sur
+3 octets. Une configuration locale publiée a priorité.
+
+Pour connaître les champs exacts du courtier, consultez la
+[référence des points de terminaison](broker-reference.md).
+
+## Terminer la vérification
+
+L’installation n’est pas terminée dès qu’un écran indique « connected ». Elle
+l’est lorsque :
+
+1. votre observateur apparaît dans [CoreScope Observers](https://live.meshcore.ca/#/observers);
+2. un paquet capté par votre radio apparaît dans [CoreScope Packets](https://live.meshcore.ca/#/packets).
+
+Terminez avec [Vérifier votre observateur](verify.md). En cas de problème,
+consultez le [dépannage](troubleshooting.md).
diff --git a/docs/analyzer/intro.md b/docs/analyzer/intro.md
index 23e8044..f1edbb7 100644
--- a/docs/analyzer/intro.md
+++ b/docs/analyzer/intro.md
@@ -1,124 +1,92 @@
-# Analyzer & MQTT Packet Broker
-
-MeshCore observers capture mesh traffic and publish packet telemetry to MQTT brokers, feeding CoreScope dashboards, maps, and packet inspectors. Pick the observer path that matches your hardware and host setup.
-
-!!! tip "Observer setup checklist"
- Every observer path needs the same basics: a MeshCore radio already on the MeshCore Canada network settings, a real 3-letter IATA airport code, the MeshCore.ca broker pair, JWT token authentication, TLS on port `443`, and packet publishing enabled. If the setup screen has two broker entries, use the same IATA code on both entries.
-
- MeshCore Canada network settings are **USA/Canada (Recommended)**, or raw radio values `910.525 MHz / 62.5 kHz / SF7 / CR5`, with 3-byte path hashes. Standalone observer firmware from the observer flasher includes selectable MeshCore Canada broker presets; set `meshcore-ca-1` and `meshcore-ca-2`, then set `set path.hash.mode 2`, IATA, WiFi, and packet publishing during onboarding. On retained preferences or generic CLI devices, also run `set radio 910.525,62.5,7,5`.
-
-## Choose Your Observer Path
-
-
-
-- :material-chip:{ .lg .middle } **MQTT Firmware**
-
- ---
-
- Flash observer firmware directly onto a WiFi-capable LoRa board. No host computer required after setup.
-
- Best for: Heltec V3, Heltec V4 OLED, and other published direct MQTT targets.
-
- [:octicons-arrow-right-24: MQTT Firmware Guide](builds/mqtt-firmware.md)
-
-- :material-usb:{ .lg .middle } **MCtoMQTT**
-
- ---
-
- Bridge a USB-connected MeshCore node to MQTT via a Linux or macOS host.
-
- Best for: fixed repeaters and room servers with a nearby host machine.
-
- [:octicons-arrow-right-24: MCtoMQTT Guide](builds/mctomqtt.md)
-
-- :material-language-python:{ .lg .middle } **PyMC**
-
- ---
-
- Add the MeshCore.ca broker pair to an existing pyMC repeater installation.
-
- Best for: Python-based repeater setups.
-
- [:octicons-arrow-right-24: PyMC Guide](builds/pymc.md)
-
-- :material-home-assistant:{ .lg .middle } **Home Assistant**
-
- ---
-
- Add MeshCore.ca brokers to the Home Assistant MeshCore integration.
-
- Best for: Home Assistant users with a connected MeshCore node.
-
- [:octicons-arrow-right-24: MeshCore-HA Guide](builds/meshcore-ha.md)
-
-- :octicons-terminal-24:{ .lg .middle } **RemoteTerm**
-
- ---
-
- Use RemoteTerm's Community MQTT fanout to report packets from a managed radio.
-
- Best for: RemoteTerm users already connected over serial, TCP, or BLE.
-
- [:octicons-arrow-right-24: RemoteTerm Setup](remoteterm.md)
-
+---
+title: Set up a network observer
+description: Choose the observer setup that matches what you already run, then check it in CoreScope.
+audience:
+ - observer-operators
+task: choose-observer-method
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 5 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+page_scripts:
+ - assets/javascripts/analyzer-method-chooser.js?v=20260722-2
+---
+
+# Set up a network observer
+
+An observer forwards nearby MeshCore packet data to CoreScope. It does not decrypt private messages or need to repeat traffic.
+
+## How the pieces fit
+
+
+
Radiohears nearby MeshCore packets
+
Observerforwards the packet data
+
MeshCore Canadareceives it through two shared endpoints
+
Live toolsCoreScope shows observers and packets
+
+
+The shared endpoints use MQTT, but you do not need to learn MQTT before choosing a setup.
+
+!!! warning "Observers share what they hear"
+ Observer details and heard packet data can appear in CoreScope. Do not transmit sensitive information. Read [Observer data and privacy](data-collection-access.md) before turning one on.
+
+## Choose your setup
+
+Start with what already manages your radio.
+
+
+
+ What is already part of this setup?
+
+
+
-## Shared References
-
-
-
-- :material-check-circle:{ .lg .middle } **Check Your Observer**
-
- ---
-
- Confirm your observer is online and reporting to CoreScope.
-
- [:octicons-arrow-right-24: Check Your Observer](verify.md)
+The same choices are listed here:
-- :material-wrench:{ .lg .middle } **Troubleshooting**
+| Your current setup | Recommended method | Host that must stay on |
+|---|---|---|
+| RemoteTerm already manages the radio | [RemoteTerm](remoteterm.md) | The RemoteTerm host |
+| Home Assistant already has MeshCore | [Home Assistant](builds/meshcore-ha.md) | Home Assistant |
+| PyMC already manages a repeater | [PyMC](builds/pymc.md) | The PyMC host |
+| A Linux or macOS host is connected by USB | [MCtoMQTT](builds/mctomqtt.md) | The USB host |
+| A supported Wi-Fi LoRa board can be dedicated | [Standalone MQTT firmware](builds/mqtt-firmware.md) | No separate host |
- ---
+If none fit, ask in the [MeshCore Canada forum](https://forum.meshcore.ca/) before installing anything new.
- Path-specific diagnostics for firmware, host bridges, PyMC, Home Assistant, and RemoteTerm.
+## What every setup needs
- [:octicons-arrow-right-24: Troubleshooting](troubleshooting.md)
+Whichever setup you choose, you need:
-- :material-server-network:{ .lg .middle } **Broker Reference**
+- a radio already set for the local mesh;
+- a real three-letter [location code](iata-codes.md);
+- the MeshCore Canada primary and backup endpoints;
+- encrypted connections with certificate checks;
+- packet publishing, not status-only publishing; and
+- an always-on host or board.
- ---
+The Canadian onboarding baseline is **USA/Canada (Recommended)**, `910.525 MHz / 62.5 kHz / SF7 / CR5`, with 3-byte path hashes. A published local setting takes priority.
- Broker hosts, ports, TLS, JWT audience, and topic conventions.
-
- [:octicons-arrow-right-24: Broker Details](broker-reference.md)
-
-- :material-eye:{ .lg .middle } **Data Collection & Access**
-
- ---
-
- Learn what observer data is collected, where it is stored, and who administers MQTT access.
-
- [:octicons-arrow-right-24: Data Collection & Access](data-collection-access.md)
-
-- :material-airplane:{ .lg .middle } **IATA Codes**
-
- ---
-
- Canadian quick-list codes and guidance for choosing a real region code.
-
- [:octicons-arrow-right-24: IATA Region Codes](iata-codes.md)
-
-
Utilisez RemoteTerm siIl se connecte déjà à la radio par liaison série, TCP ou BLE.
+
Choisissez autre chose siVous installeriez RemoteTerm uniquement pour observer.
+
Gardez en fonctionL’hôte RemoteTerm, la connexion radio et l’accès Internet.
+
+
+RemoteTerm évolue rapidement. Si les libellés ne correspondent pas, consultez
+les instructions du projet pour la version installée avant d’enregistrer les
+changements.
+
+## Avant de commencer
+
+- [ ] RemoteTerm est installé à partir de sa source vérifiée.
+- [ ] La connexion à la radio est stable.
+- [ ] La radio utilise les paramètres du réseau maillé local.
+- [ ] Vous avez choisi un véritable [code d’emplacement](iata-codes.md).
+- [ ] Vous avez lu [Données des observateurs et vie privée](data-collection-access.md).
+
+## Ce qui sera modifié
+
+Vous ajouterez une entrée Community MQTT principale et une entrée de secours.
+Elles transmettent les données de paquets par des connexions WebSocket chiffrées
+et ne modifient pas le micrologiciel de la radio.
+
+## Configuration
+
+Ouvrez **Settings** → **MQTT & Automation**, ajoutez
+**Community MQTT / meshcoretomqtt**, puis entrez :
+
+| Champ | Valeur principale |
+|---|---|
+| Name | `MeshCore.ca 1` |
+| Broker Host | `mqtt1.meshcore.ca` |
+| Broker Port | `443` |
+| Transport | `WebSockets` |
+| Authentication | `Token` |
+| WebSocket Path | `/` |
+| Token Audience | `mqtt1.meshcore.ca` |
+| Use TLS | Enabled |
+| Verify TLS certificates | Enabled |
+| Region Code | Le véritable code d’emplacement à trois lettres le plus près |
+| Packet Topic Template | `meshcore/{IATA}/{PUBLIC_KEY}/packets` |
+
+Laissez le champ facultatif de courriel du responsable vide, sauf s’il est
+nécessaire à l’exploitation. Enregistrez l’entrée en la laissant activée.
+
+Ajoutez une entrée de secours avec les mêmes valeurs, en ne modifiant que :
+
+| Champ | Valeur de secours |
+|---|---|
+| Name | `MeshCore.ca 2` |
+| Broker Host | `mqtt2.meshcore.ca` |
+| Token Audience | `mqtt2.meshcore.ca` |
+
+Utilisez le même code d’emplacement dans les deux entrées.
+
+
+
+!!! note "Diffusion MQTT sous Windows"
+ Si les instructions actuelles de RemoteTerm exigent l’option Uvicorn `--loop none` pour la diffusion MQTT sous Windows, utilisez cette option de lancement. Confirmez-la dans les instructions correspondant à la version de RemoteTerm installée.
+
+## Ce que vous devriez voir
+
+Les deux entrées demeurent activées sans erreurs TLS ou de jeton répétées, et le
+nombre de paquets dans RemoteTerm change lorsqu’il capte de l’activité à
+proximité.
+
+## Vérifier dans CoreScope
+
+1. Ouvrez [CoreScope Observers](https://live.meshcore.ca/#/observers) et trouvez l’observateur RemoteTerm.
+2. Produisez de l’activité normale à proximité, ou attendez qu’il y en ait.
+3. Ouvrez [CoreScope Packets](https://live.meshcore.ca/#/packets) et confirmez qu’un paquet récent y apparaît.
+
+Terminez avec [Vérifier votre observateur](verify.md). Une entrée connectée sans
+paquet récent ne confirme pas que tout le parcours fonctionne.
+
+## Récupération
+
+Désactivez ou supprimez seulement les deux entrées Community MQTT que vous avez
+ajoutées. Ne supprimez aucune autre automatisation de RemoteTerm. Confirmez que
+RemoteTerm gère toujours la radio normalement.
+
+## Si la vérification échoue
+
+Consultez le [dépannage](troubleshooting.md). Si seule la connexion de secours
+échoue, vérifiez que son hôte et l’audience de son jeton sont tous deux
+`mqtt2.meshcore.ca`.
diff --git a/docs/analyzer/remoteterm.md b/docs/analyzer/remoteterm.md
index d5905d0..0195999 100644
--- a/docs/analyzer/remoteterm.md
+++ b/docs/analyzer/remoteterm.md
@@ -1,47 +1,98 @@
-# RemoteTerm Setup
+---
+title: Observe with RemoteTerm
+description: Send packets from a radio already managed by RemoteTerm to CoreScope.
+audience:
+ - observer-operators
+task: configure-remoteterm-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: intermediate
+estimated_time: 15 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
-[RemoteTerm for MeshCore](https://github.com/jkingsman/Remote-Terminal-for-MeshCore) can act as an observer by forwarding raw RF packets through its **Community MQTT / meshcoretomqtt** integration. This does not publish decrypted messages.
+# Observe with RemoteTerm
-Install and start RemoteTerm using the upstream instructions, connect your MeshCore radio over serial, TCP, or BLE, then open the RemoteTerm web UI.
+[RemoteTerm for MeshCore](https://github.com/jkingsman/Remote-Terminal-for-MeshCore) can forward packet data from a radio it already manages. It does not decrypt private messages.
-## Configure Community MQTT
+## Is this method right for you?
-In RemoteTerm, open **Settings** -> **MQTT & Automation**, add **Community MQTT/meshcoretomqtt**, and use these values:
+
+
Use RemoteTerm ifIt already connects to the radio over serial, TCP, or BLE.
+
Use something else ifYou would install RemoteTerm only for observing.
+
Keep onlineThe RemoteTerm host, radio connection, and internet access.
+
-| Field | Value |
-|-------|-------|
-| Name | `MeshCore.ca 1` or any descriptive name |
+RemoteTerm changes quickly. If the labels do not match, check the upstream instructions for your installed version before saving.
+
+## Before you start
+
+- [ ] RemoteTerm is installed from its reviewed upstream source.
+- [ ] The radio connection is stable.
+- [ ] The radio is on the local mesh settings.
+- [ ] You chose a real [location code](iata-codes.md).
+- [ ] You read [Observer data, access, and privacy](data-collection-access.md).
+
+## What this changes
+
+You will add a primary and backup Community MQTT entry. They send packet data over encrypted WebSocket connections and do not change the radio firmware.
+
+## Set up
+
+Open **Settings** → **MQTT & Automation**, add **Community MQTT / meshcoretomqtt**, and enter:
+
+| Field | Primary value |
+|---|---|
+| Name | `MeshCore.ca 1` |
| Broker Host | `mqtt1.meshcore.ca` |
| Broker Port | `443` |
| Transport | `WebSockets` |
| Authentication | `Token` |
| WebSocket Path | `/` |
-| Token Audience | `mqtt1.meshcore.ca` or leave blank to default to the broker host |
-| Owner Email | Optional |
+| Token Audience | `mqtt1.meshcore.ca` |
| Use TLS | Enabled |
| Verify TLS certificates | Enabled |
-| Region Code (IATA) | Your nearest real 3-letter IATA airport code |
+| Region Code | Your nearest real three-letter location code |
| Packet Topic Template | `meshcore/{IATA}/{PUBLIC_KEY}/packets` |
-Click **Save as Enabled**.
-
-## Add the Backup Broker
+Leave optional owner email blank unless it is operationally needed. Save the entry as enabled.
-To publish to the redundant broker as well, add a second Community MQTT integration with the same settings, changing only:
+Add a backup entry with the same values, changing only:
-| Field | Value |
-|-------|-------|
+| Field | Backup value |
+|---|---|
| Name | `MeshCore.ca 2` |
| Broker Host | `mqtt2.meshcore.ca` |
| Token Audience | `mqtt2.meshcore.ca` |
-Use the same IATA code on both entries.
+Use the same location code in both entries.
-
+
!!! note "Windows MQTT fanout"
- If you run RemoteTerm on Windows and enable MQTT fanout, start Uvicorn with `--loop none` as described in the RemoteTerm README so the MQTT client can connect reliably.
+ If RemoteTerm's current upstream instructions require Uvicorn `--loop none` for Windows MQTT fanout, use that documented launch option. Confirm against the installed RemoteTerm version.
+
+## What you should see
+
+Both entries stay enabled without repeated TLS or token errors, and RemoteTerm's packet count changes when it hears nearby activity.
+
+## Verify in CoreScope
+
+1. Open [CoreScope Observers](https://live.meshcore.ca/#/observers) and find the RemoteTerm observer.
+2. Create or wait for normal nearby activity.
+3. Open [CoreScope Packets](https://live.meshcore.ca/#/packets) and confirm a recent packet appears.
+
+Finish with [Check your observer](verify.md). A connected entry without a recent packet is not proof that the whole path works.
+
+## Recovery
+
+Disable or remove only the two Community MQTT entries you added. Do not remove unrelated RemoteTerm automation. Confirm RemoteTerm still manages the radio normally.
-## Verify
+## If verification fails
-After saving, use [Check Your Observer](verify.md). If the observer connects but no packets appear, confirm the radio is on the MeshCore Canada preset and that RemoteTerm is publishing packet topics, not only status.
+Use [Troubleshooting](troubleshooting.md). If only the backup fails, check that its host and token audience are both `mqtt2.meshcore.ca`.
diff --git a/docs/analyzer/scripts/add-meshcore-ca-broker.sh b/docs/analyzer/scripts/add-meshcore-ca-broker.sh
index 87a41e9..619ab14 100644
--- a/docs/analyzer/scripts/add-meshcore-ca-broker.sh
+++ b/docs/analyzer/scripts/add-meshcore-ca-broker.sh
@@ -107,6 +107,8 @@ upper_iata() {
printf '%s' "$1" | tr '[:lower:]' '[:upper:]'
}
+# Keep this quick-list snapshot aligned with ../location-codes.json. The
+# analyzer content test fails when the published JSON and helper drift.
IATA_CHOICES="$(cat <<'EOF'
Ontario|YYZ|Toronto (Pearson)
Ontario|YTZ|Toronto (Billy Bishop)
@@ -142,6 +144,7 @@ Quebec|YUY|Rouyn-Noranda
Quebec|YZV|Sept-Iles
Quebec|YGP|Gaspe
Quebec|YRQ|Trois-Rivieres
+Quebec|YBC|Baie-Comeau
British Columbia|YVR|Vancouver
British Columbia|YYJ|Victoria
British Columbia|YXX|Abbotsford / Fraser Valley
@@ -158,7 +161,6 @@ British Columbia|YYF|Penticton
British Columbia|YCG|Castlegar
British Columbia|YKA|Kamloops
British Columbia|YXC|Cranbrook
-British Columbia|YBC|Baie-Comeau
Alberta|YYC|Calgary
Alberta|YEG|Edmonton
Alberta|YMM|Fort McMurray
@@ -273,8 +275,12 @@ EOF
exit 1
fi
IATA="$(upper_iata "$IATA" | tr -d '[:space:]')"
- if [ "$IATA" = "XXX" ]; then
- echo "XXX is a placeholder. Use the real 3-letter IATA airport code nearest to you." >&2
+ if [ "$IATA" = "XXX" ] || [ "$IATA" = "HOME" ]; then
+ echo "$IATA is a placeholder. Use the real 3-letter IATA airport code nearest to you." >&2
+ exit 1
+ fi
+ if [ "$IATA" = "CAN" ]; then
+ echo "CAN is Guangzhou's airport code, not shorthand for Canada. Use the real code nearest to you." >&2
exit 1
fi
if ! printf '%s' "$IATA" | grep -Eq '^[A-Z]{3}$'; then
diff --git a/docs/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1 b/docs/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1
index 20fb8cf..9d747ea 100644
--- a/docs/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1
+++ b/docs/analyzer/scripts/add-meshcore-ca-packetcapture-broker.ps1
@@ -31,6 +31,8 @@ function Prompt-YesNo {
}
}
+# Keep this quick-list snapshot aligned with ../location-codes.json. The
+# analyzer content test fails when the published JSON and helper drift.
$KnownIataCodes = @(
"YYZ","YTZ","YOW","YHM","YKF","YXU","YOO","YKZ","YAM","YQT","YSB","YTS","YQG","YYB","YGK","YPQ","YTR","YHD","YPL","YND",
"YUL","YMX","YQB","YBG","YVO","YHU","YRJ","YGL","YSC","YTQ","YUY","YZV","YGP","YRQ",
@@ -53,8 +55,11 @@ function Resolve-Iata {
$candidate = Read-Host "Enter 3-letter IATA airport code"
$candidate = $candidate.Trim().ToUpperInvariant()
}
- if ($candidate -eq "XXX") {
- throw "XXX is a placeholder. Use the real 3-letter IATA airport code nearest to you."
+ if ($candidate -in @("XXX", "HOME")) {
+ throw "$candidate is a placeholder. Use the real 3-letter IATA airport code nearest to you."
+ }
+ if ($candidate -eq "CAN") {
+ throw "CAN is Guangzhou's airport code, not shorthand for Canada. Use the real code nearest to you."
}
if ($candidate -notmatch '^[A-Z]{3}$') {
throw "IATA code must be exactly 3 letters."
diff --git a/docs/analyzer/troubleshooting.fr.md b/docs/analyzer/troubleshooting.fr.md
new file mode 100644
index 0000000..fbcfe04
--- /dev/null
+++ b/docs/analyzer/troubleshooting.fr.md
@@ -0,0 +1,177 @@
+---
+title: Dépanner un observateur
+description: Trouvez où votre observateur a cessé de fonctionner et demandez de l’aide sans dévoiler de renseignements secrets.
+audience:
+ - observer-operators
+task: troubleshoot-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: intermediate
+estimated_time: 15 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Dépanner un observateur
+
+Commencez par ce que vous pouvez constater. Modifiez un seul élément à la fois
+pour savoir ce qui a réglé le problème.
+
+## L’observateur n’apparaît jamais { #observer-never-appears }
+
+Vérifiez les éléments suivants dans l’ordre :
+
+1. **Radio :** confirmez qu’elle est alimentée, connectée à la méthode d’observation et réglée selon le réseau maillé local.
+2. **Appareil ou service :** confirmez que le processus de l’observateur fonctionne.
+3. **Courtier :** confirmez que le point de terminaison principal indique une connexion avec validation TLS.
+4. **Affichage :** attendez quelques minutes, actualisez [CoreScope Observers](https://live.meshcore.ca/#/observers), puis recherchez le nom exact de l’observateur.
+
+Ces commandes consultent seulement l’état des services :
+
+=== "MCtoMQTT"
+
+ ```bash
+ sudo systemctl status mctomqtt --no-pager
+ sudo journalctl -u mctomqtt -n 80 --no-pager
+ ```
+
+=== "Capture du compagnon"
+
+ ```bash
+ sudo systemctl status meshcore-capture --no-pager
+ sudo journalctl -u meshcore-capture -n 80 --no-pager
+ ```
+
+=== "PyMC"
+
+ ```bash
+ sudo systemctl status pymc-repeater --no-pager
+ sudo journalctl -u pymc-repeater -n 80 --no-pager
+ ```
+
+Avant de partager les résultats, suivez les consignes de la section
+[Quoi transmettre lorsque vous demandez de l’aide](#what-to-share-when-asking-for-help).
+
+Pour un micrologiciel autonome, exécutez seulement ces commandes de lecture
+dans l’interface en ligne de commande de l’appareil :
+
+```text
+get name
+get wifi.status
+get mqtt.iata
+get mqtt.status
+get mqtt1.preset
+get mqtt2.preset
+get path.hash.mode
+```
+
+Vous devriez voir la connexion Wi-Fi et au moins le courtier principal
+connectés, un code d’emplacement à trois lettres ainsi que les préréglages
+`meshcore-ca-1` et `meshcore-ca-2`.
+
+## L’observateur apparaît, mais aucun paquet n’arrive { #observer-appears-but-no-packets-arrive }
+
+Une connexion au courtier ne confirme pas la transmission des paquets.
+
+1. Confirmez que la radio capte l’activité normale du réseau maillé à proximité.
+2. Confirmez que la publication des paquets est activée pour la méthode choisie.
+3. Confirmez que le code d’emplacement et le sujet des paquets concordent.
+4. Consultez [CoreScope Packets](https://live.meshcore.ca/#/packets) après de l’activité à proximité.
+
+| Méthode | Paramètre de paquets |
+|---|---|
+| Micrologiciel MQTT | `get mqtt.packets` indique `on`, `get bridge.enabled` indique `on` et `get mqtt.rx` indique `on` |
+| MCtoMQTT / capture du compagnon | Le sujet des paquets se termine par `/packets`, et non seulement par `/status` |
+| PyMC | Le champ `format` du courtier est `letsmesh` |
+| Home Assistant | **Payload Mode** est réglé à `packet`, ou l’ancien réglage **Packets (Lets Mesh)** est activé |
+| RemoteTerm | Le sujet de paquets Community MQTT est activé |
+
+Si la radio ne reçoit aucun paquet, vérifiez d’abord le préréglage radio,
+l’antenne, la connexion et l’activité locale. Ne modifiez les paramètres du
+courtier qu’après ces vérifications.
+
+## Seule la connexion de secours échoue { #only-the-backup-connection-fails }
+
+Comparez les deux entrées. L’hôte de secours et l’audience du jeton doivent être
+`mqtt2.meshcore.ca`. Un jeton destiné à l’hôte principal ne peut pas
+s’authentifier auprès de l’hôte de secours.
+
+Ne désactivez pas la validation TLS pour réussir la connexion.
+
+## L’observateur apparaît au mauvais endroit { #observer-appears-in-the-wrong-place }
+
+Vérifiez chaque champ d’emplacement configuré. Utilisez le même véritable
+[code d’emplacement](iata-codes.md) à trois lettres dans les deux entrées du
+courtier et dans la méthode d’observation.
+
+N’utilisez pas :
+
+- `CAN` comme abréviation du Canada;
+- `XXX` ou `HOME`;
+- un code voisin simplement parce qu’un ancien sélecteur n’affiche pas le bon code.
+
+Mettez l’intégration à jour si elle n’accepte pas le bon code.
+
+## L’observateur se connecte et se déconnecte sans arrêt { #observer-connects-and-disconnects-repeatedly }
+
+Vérifiez, dans cet ordre :
+
+1. la stabilité de l’alimentation et de la connexion USB;
+2. la mise en veille de l’hôte ou les redémarrages du service;
+3. la stabilité d’Internet et du DNS;
+4. l’exactitude de l’horloge système;
+5. les erreurs répétées de jeton, de TLS ou de WebSocket dans les journaux locaux.
+
+Notez l’heure et le fuseau horaire d’une déconnexion. Un administrateur pourra
+ainsi comparer votre signalement aux journaux de l’infrastructure sans que vous
+ayez à dévoiler vos identifiants.
+
+## L’écran de Home Assistant ne correspond pas au guide { #home-assistant-screen-does-not-match-the-guide }
+
+Les écrans actuels utilisent **Payload Mode** et un champ d’emplacement en texte
+libre. Les anciens écrans peuvent plutôt afficher **Packets (Lets Mesh)** et un
+sélecteur.
+
+Mettez l’intégration à jour avant de choisir un mauvais emplacement. Si l’écran
+actuel diffère encore, notez les versions de Home Assistant et de l’intégration
+MeshCore, puis demandez de l’aide.
+
+## Quoi transmettre lorsque vous demandez de l’aide { #what-to-share-when-asking-for-help }
+
+Copiez ce modèle et remplissez les renseignements que vous connaissez. Conservez
+les libellés anglais pour faciliter le triage :
+
+```text
+Observer method:
+Device or board:
+Operating system / Home Assistant version:
+Observer app, integration, or firmware version:
+Location code:
+Time checked (with time zone):
+First thing that failed: radio / observer / broker / viewer
+Primary connected: yes / no / unknown
+Backup connected: yes / no / not supported
+Observer visible: yes / no
+Recent packet visible: yes / no
+Exact error after redaction:
+Steps already tried:
+```
+
+Avant de le publier, retirez :
+
+- le nom du réseau Wi-Fi et son mot de passe;
+- les mots de passe MQTT, JWT, jetons, témoins et en-têtes d’autorisation;
+- les clés privées MeshCore;
+- le courriel du responsable et ses coordonnées personnelles;
+- les adresses résidentielles ou coordonnées exactes;
+- les lignes sans rapport provenant de fichiers de configuration complets.
+
+Publiez le tout sur le [forum de MeshCore Canada](https://forum.meshcore.ca/) ou
+le canal de soutien de votre communauté locale. N’incluez que le court extrait
+du journal qui montre le problème.
+
+Retournez à [Vérifier votre observateur](verify.md) après chaque correction.
diff --git a/docs/analyzer/troubleshooting.md b/docs/analyzer/troubleshooting.md
index 75496ef..2e3ed6d 100644
--- a/docs/analyzer/troubleshooting.md
+++ b/docs/analyzer/troubleshooting.md
@@ -1,119 +1,158 @@
-# Troubleshooting
-
-If your observer doesn't show up on [CoreScope](https://live.meshcore.ca/#/observers), work through these checks based on your setup path.
-
-## MQTT Firmware
-
-Run these in the device's admin CLI:
-
-| Command | What to check |
-|---------|---------------|
-| `get wifi.status` | Should show connected to your 2.4 GHz network |
-| `get mqtt.status` | Should show an active broker connection |
-| `get mqtt.iata` | Should return your real 3-letter IATA airport code |
-| `get mqtt.packets` | Should be `on` for packet publishing |
-| `get bridge.enabled` | Should be `on` for bridge publishing |
-| `get mqtt.rx` / `get mqtt.tx` | Should match the firmware guide setup |
-| `get mqtt1.preset` | Should show `meshcore-ca-1` |
-| `get mqtt2.preset` | Should show `meshcore-ca-2` |
-| `get name` | Should return the node name you set |
-
-??? note "Full verify command block"
-
- ```text
- get name
- get mqtt.origin
- get mqtt.iata
- get wifi.status
- get mqtt.packets
- get bridge.enabled
- get mqtt.rx
- get mqtt.tx
- get mqtt.status
- get mqtt1.preset
- get mqtt2.preset
- get mqtt3.preset
+---
+title: Troubleshoot an observer
+description: Find where your observer stopped working and ask for help without exposing secrets.
+audience:
+ - observer-operators
+task: troubleshoot-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: intermediate
+estimated_time: 15 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Troubleshoot an observer
+
+Start with what you can see. Change one part at a time so you know what fixed it.
+
+## Observer never appears
+
+Check these in order:
+
+1. **Radio:** confirm it is powered, connected to the observer method, and on the local mesh settings.
+2. **Device or service:** confirm the observer process is running.
+3. **Broker:** confirm the primary endpoint reports connected with TLS verification.
+4. **Viewer:** wait a few minutes, refresh [CoreScope Observers](https://live.meshcore.ca/#/observers), and search the exact observer name.
+
+These commands only read service status:
+
+=== "MCtoMQTT"
+
+ ```bash
+ sudo systemctl status mctomqtt --no-pager
+ sudo journalctl -u mctomqtt -n 80 --no-pager
```
-## MCtoMQTT / Companion (USB Host)
+=== "Companion capture"
-Check that the systemd service is running:
+ ```bash
+ sudo systemctl status meshcore-capture --no-pager
+ sudo journalctl -u meshcore-capture -n 80 --no-pager
+ ```
-```bash
-# Serial host
-sudo systemctl status mctomqtt
+=== "PyMC"
-# Companion
-sudo systemctl status meshcore-capture
-```
+ ```bash
+ sudo systemctl status pymc-repeater --no-pager
+ sudo journalctl -u pymc-repeater -n 80 --no-pager
+ ```
-If the service is running but nothing appears, check the config drop-in:
+Before sharing any output, follow [What to share when asking for
+help](#what-to-share-when-asking-for-help).
-```bash
-# Serial host
-cat /etc/mctomqtt/config.d/20-meshcore-ca.toml
+For standalone firmware, run only these read commands in the device CLI:
-# Companion
-cat ~/.meshcore-packet-capture/.env.local
+```text
+get name
+get wifi.status
+get mqtt.iata
+get mqtt.status
+get mqtt1.preset
+get mqtt2.preset
+get path.hash.mode
```
-Confirm the broker hosts are `mqtt1.meshcore.ca` and `mqtt2.meshcore.ca`.
+You should see Wi-Fi and at least the primary broker connected, a three-letter location code, and the `meshcore-ca-1` and `meshcore-ca-2` presets.
-Also confirm the observer path is publishing packet payloads, not only status. For companion capture, `PACKETCAPTURE_MQTT1_TOPIC_PACKETS` and `PACKETCAPTURE_MQTT2_TOPIC_PACKETS` should use `meshcore/{IATA}/{PUBLIC_KEY}/packets`.
+## Observer appears but no packets arrive
-## IATA Code Problems
+A connected broker is not packet proof.
-Use a real 3-letter IATA airport code such as `YOW`, `YKF`, or `YYZ`. The firmware may accept any text, but the public broker rejects placeholders and made-up region names such as `XXX` or `HOME`. Do not use `CAN` as shorthand for Canada; it is a real airport code for Guangzhou and will tag your observer to the wrong region.
+1. Confirm the radio can hear normal nearby mesh activity.
+2. Confirm packet publishing is enabled for the selected method.
+3. Confirm the location code and packet topic agree.
+4. Check [CoreScope Packets](https://live.meshcore.ca/#/packets) after nearby activity.
-If your code is not on the quick list, that does not automatically mean it is unsupported. It can still work if it is a real IATA airport code. Make sure every component uses the same code:
+| Method | Packet setting |
+|---|---|
+| MQTT firmware | `get mqtt.packets` is `on`, `get bridge.enabled` is `on`, and `get mqtt.rx` is `on` |
+| MCtoMQTT / companion capture | Packet topic ends in `/packets`, not only `/status` |
+| PyMC | Broker `format` is `letsmesh` |
+| Home Assistant | **Payload Mode** is `packet`, or older **Packets (Lets Mesh)** is enabled |
+| RemoteTerm | Community MQTT packet topic is enabled |
-| Component | Where to check |
-|-----------|----------------|
-| MQTT firmware | `get mqtt.iata` |
-| MCtoMQTT | `/etc/mctomqtt/config.d/20-meshcore-ca.toml` |
-| Companion capture | `PACKETCAPTURE_IATA` in `~/.meshcore-packet-capture/.env.local` |
-| Home Assistant | MeshCore integration region/IATA field |
-| RemoteTerm | Community MQTT region/IATA field |
+If the radio hears nothing, resolve the radio preset, antenna, connection, or local activity before changing broker values.
-## PyMC
+## Only the backup connection fails
-```bash
-sudo systemctl status pymc-repeater
-```
+Compare the two entries. The backup host and token audience must both be `mqtt2.meshcore.ca`. A token for the primary host cannot authenticate to the backup.
+
+Do not disable TLS verification to make the connection succeed.
+
+## Observer appears in the wrong place
+
+Check every configured location field. Use the same real three-letter [location code](iata-codes.md) in both broker entries and in the observer method.
-Check that your `mqtt.iata_code` is set and the broker block is present in `/etc/pymc_repeater/config.yaml`.
+Do not use:
-## Home Assistant
+- `CAN` as shorthand for Canada;
+- `XXX` or `HOME`; or
+- a neighbouring code merely because an older picker does not list the correct code.
-Go to **Settings** > **Devices & Services** > **MeshCore** > **Configure** > **Manage MQTT Brokers** and confirm both brokers show as connected. Make sure your IATA code is set in the integration.
+Update the integration if it cannot accept the correct code.
-If the brokers connect but packets never appear, check the packet payload setting. Some Home Assistant MeshCore versions label this as **Packets (Lets Mesh)**; newer versions expose it as **Payload Mode**. It must be enabled/set to `packet` for MeshCore.ca packet visibility.
+## Observer connects and disconnects repeatedly
-If a code such as `YTR` is missing from a picker, update the MeshCore Home Assistant integration and type the code into **Broker IATA Code**. Using a nearby code such as `YGK` can make data visible, but it tags the observer to the wrong region.
+Check in this order:
-| HA symptom | Check |
-|------------|-------|
-| Broker connected, no packets | **Packets (Lets Mesh)** enabled or **Payload Mode** = `packet` |
-| Cannot enter a real IATA code | Update MeshCore-HA; current versions use free text |
-| Backup broker fails | `Token Audience` must match the broker host (`mqtt2.meshcore.ca`) |
-| Observer appears under the wrong city | Both broker entries use the same nearest real IATA code |
+1. stable power and USB connection;
+2. host sleep or service restarts;
+3. internet and DNS stability;
+4. system clock accuracy;
+5. repeated token, TLS, or WebSocket errors in local logs.
-## RemoteTerm
+Record the time and time zone of one disconnect. That lets an administrator compare your report with infrastructure logs without exposing credentials.
-Open **Settings** -> **MQTT & Automation** and confirm each Community MQTT entry:
+## Home Assistant screen does not match the guide
+
+Current screens use **Payload Mode** and a free-text location field. Older screens may use **Packets (Lets Mesh)** and a picker.
+
+Update the integration before substituting a wrong location. If the current screen still differs, record the Home Assistant and MeshCore integration versions and ask for help.
+
+## What to share when asking for help
+
+Copy this template and fill in what you know:
+
+```text
+Observer method:
+Device or board:
+Operating system / Home Assistant version:
+Observer app, integration, or firmware version:
+Location code:
+Time checked (with time zone):
+First thing that failed: radio / observer / broker / viewer
+Primary connected: yes / no / unknown
+Backup connected: yes / no / not supported
+Observer visible: yes / no
+Recent packet visible: yes / no
+Exact error after redaction:
+Steps already tried:
+```
-| Field | Expected value |
-|-------|----------------|
-| Broker Host | `mqtt1.meshcore.ca` or `mqtt2.meshcore.ca` |
-| Broker Port | `443` |
-| Transport | `WebSockets` |
-| Authentication | `Token` |
-| TLS | Enabled and verified |
-| Region Code | Your nearest real IATA code |
-| Packet Topic Template | `meshcore/{IATA}/{PUBLIC_KEY}/packets` |
+Before posting, remove:
-If the primary entry works and the backup does not, check the second entry's token audience. It must be `mqtt2.meshcore.ca`.
+- Wi-Fi SSID and password;
+- MQTT passwords, JWTs, tokens, cookies, and authorization headers;
+- MeshCore private keys;
+- owner email and personal contact details;
+- exact home addresses or coordinates; and
+- unrelated lines from full config files.
-## Still Not Working?
+Post it in the [MeshCore Canada forum](https://forum.meshcore.ca/) or your local community's support channel. Include only the small part of the log that shows the problem.
-If everything looks correct but your observer still doesn't appear, double check that your device has internet access and can reach `mqtt1.meshcore.ca` on port 443. Firewalls or network restrictions on outbound WebSocket connections are the most common blocker.
+Return to [Check your observer](verify.md) after each fix.
diff --git a/docs/analyzer/verify.fr.md b/docs/analyzer/verify.fr.md
new file mode 100644
index 0000000..72c8644
--- /dev/null
+++ b/docs/analyzer/verify.fr.md
@@ -0,0 +1,94 @@
+---
+title: Vérifier votre observateur
+description: Vérifiez que la radio capte le trafic, que l’observateur le transmet et que CoreScope le reçoit.
+audience:
+ - observer-operators
+task: verify-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 10 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Vérifier votre observateur
+
+Une connexion au courtier confirme seulement l’accès Internet. Votre
+observateur fonctionne lorsqu’un véritable paquet radio se rend jusqu’à
+CoreScope.
+
+## Suivre un paquet en quatre étapes
+
+
+
+ Radio : confirmez que l’appareil utilise les paramètres du réseau local et qu’il capte l’activité à proximité.
+ Un compteur de paquets, un journal ou une application connectée devrait changer lorsqu’un nœud à proximité transmet.
+
+
+ Observateur : vérifiez l’état du service, de l’intégration ou de l’appareil utilisé par votre méthode.
+ Il devrait indiquer une connexion chiffrée à au moins un point de terminaison principal, sans erreurs d’authentification répétées.
+
+
+ Vue des observateurs : ouvrez CoreScope Observers.
+ Le nom de l’observateur et son code d’emplacement à trois lettres devraient apparaître avec une heure récente.
+
+
+ Vue des paquets : produisez de l’activité MeshCore à proximité, puis ouvrez CoreScope Packets.
+ Un paquet récent attribué à l’observateur devrait apparaître en quelques minutes.
+
+
+
+Ne produisez pas de trafic inutile sur un réseau maillé occupé. Une annonce
+normale ou l’activité déjà présente à proximité suffit.
+
+## Vérifier les détails
+
+| Vérification | Ce que vous devriez voir |
+|---|---|
+| Nom | Un nom de service clair comme `YOW-Repeater-01`, sans adresse résidentielle |
+| Emplacement | Le véritable code d’aéroport à trois lettres le plus près, et non `CAN`, `XXX` ou `HOME` |
+| Parcours principal | Connexion à `mqtt1.meshcore.ca` avec validation du certificat TLS |
+| Parcours de secours | Connexion à `mqtt2.meshcore.ca` si la méthode accepte deux entrées |
+| Mode de paquets | Publication des paquets activée, et non seulement celle de l’état |
+| Heure du paquet | Un paquet récent apparaît après que la radio a capté de l’activité à proximité |
+| Radio | Les paramètres locaux sont utilisés; les paramètres par défaut du Canada s’appliquent seulement en l’absence d’une configuration locale |
+
+## Conserver une note d’entretien
+
+Conservez une courte note d’entretien privée. Gardez les libellés du modèle
+ci-dessous pour faciliter le soutien :
+
+```text
+Observer:
+Method and version:
+Location code:
+Checked at (include time zone):
+Radio heard a packet: yes / no
+Primary connected: yes / no
+Backup connected: yes / no / not supported
+Observer visible: yes / no
+Packet visible: yes / no
+```
+
+N’y inscrivez pas d’identifiants, de clés privées, de noms de réseaux Wi-Fi ou
+de mots de passe Wi-Fi.
+
+## Commencer par le premier échec
+
+Consultez le guide qui correspond au premier élément qui a échoué :
+
+| Premier échec | Point de départ |
+|---|---|
+| La radio ne capte aucune activité | [L’observateur apparaît, mais aucun paquet n’arrive](troubleshooting.md#observer-appears-but-no-packets-arrive) |
+| L’observateur ne peut pas se connecter | [L’observateur n’apparaît jamais](troubleshooting.md#observer-never-appears) |
+| L’observateur est visible, mais la vue des paquets reste vide | [L’observateur apparaît, mais aucun paquet n’arrive](troubleshooting.md#observer-appears-but-no-packets-arrive) |
+| Seule la connexion de secours échoue | [Seule la connexion de secours échoue](troubleshooting.md#only-the-backup-connection-fails) |
+| Le lieu ou le nom est incorrect | [L’observateur apparaît au mauvais endroit](troubleshooting.md#observer-appears-in-the-wrong-place) |
+
+Pour obtenir des commandes sûres et un modèle de demande d’aide caviardée,
+consultez le [dépannage](troubleshooting.md).
diff --git a/docs/analyzer/verify.md b/docs/analyzer/verify.md
index 7681c9c..fa4ed95 100644
--- a/docs/analyzer/verify.md
+++ b/docs/analyzer/verify.md
@@ -1,64 +1,89 @@
-# Check Your Observer
-
-After setting up your observer using any supported path ([MQTT Firmware](builds/mqtt-firmware.md), [MCtoMQTT](builds/mctomqtt.md), [PyMC](builds/pymc.md), [Home Assistant](builds/meshcore-ha.md), or [RemoteTerm](remoteterm.md)), use the links below to confirm it's online and reporting.
-
-
-
-- :material-eye:{ .lg .middle } **CoreScope Observers**
-
- ---
-
- See all connected observers and their current status.
-
- [:octicons-arrow-right-24: View Observers](https://live.meshcore.ca/#/observers)
-
-- :material-swap-horizontal:{ .lg .middle } **CoreScope Packets**
-
- ---
-
- Watch live packet traffic flowing through observers in real time.
-
- [:octicons-arrow-right-24: View Packets](https://live.meshcore.ca/#/packets)
-
-- :material-map-marker:{ .lg .middle } **MeshCore Map**
-
- ---
-
- See observers and nodes plotted on the map.
-
- [:octicons-arrow-right-24: View Map](https://live.meshcore.ca/#/map)
-
-- :material-wrench:{ .lg .middle } **Troubleshooting**
-
- ---
-
- Not showing up? Path-specific diagnostics for all observer types.
-
- [:octicons-arrow-right-24: Troubleshooting](troubleshooting.md)
-
-
-
-Your observer should appear within a few minutes of coming online.
-
-## Healthy Observer Checklist
-
-| Check | Expected result |
-|-------|-----------------|
-| Observer name | Clear node name such as `YOW-Repeater-01` |
-| Region | Nearest real IATA code, not `CAN`, `XXX`, or `HOME` |
-| Broker coverage | Primary and backup broker configured where the path supports both |
-| Packet activity | Recent packet timestamps on CoreScope after nearby mesh activity |
-| Radio settings | `USA/Canada (Recommended)` and 3-byte path hashes unless your local page differs |
-
-## First Checks
-
-If it does not appear, start with the checks that match the symptom:
-
-| Symptom | First check |
-|---------|-------------|
-| No observer entry | MQTT is connected and the IATA code is a real airport code |
-| Observer appears, but no packets | Packet publishing is enabled: firmware `mqtt.packets`, HA **Payload Mode** = `packet`, or pyMC `format: letsmesh` |
-| Backup broker does not connect | The token audience matches the broker host (`mqtt2.meshcore.ca` for broker 2) |
-| Observer appears under the wrong city | Every broker/integration entry uses the nearest real IATA code; do not use `CAN` for Canada |
-
-For path-specific commands and Home Assistant settings, use [Troubleshooting](troubleshooting.md).
+---
+title: Check your observer
+description: Check that the radio hears traffic, the observer sends it, and CoreScope receives it.
+audience:
+ - observer-operators
+task: verify-observer
+scope: canada-baseline
+status: draft
+owner: meshcore-canada
+last_reviewed: 2026-07-22
+review_by: 2026-10-19
+difficulty: beginner
+estimated_time: 10 minutes
+destructive: false
+page_styles:
+ - assets/styles/analyzer.css?v=20260722-2
+---
+
+# Check your observer
+
+A broker connection proves only internet access. Your observer is working when
+a real radio packet reaches CoreScope.
+
+## Follow a packet through four stages
+
+
+
+ Radio: confirm the device is on the local mesh settings and can hear nearby activity.
+ A packet counter, log, or connected application should change when a nearby node transmits.
+
+
+ Observer: check the method's service, integration, or device status.
+ It should report an encrypted connection to at least the primary endpoint without repeating authentication errors.
+
+
+ Observer view: open CoreScope Observers.
+ The observer name and three-letter location code should appear with a recent timestamp.
+
+
+ Packet view: create nearby MeshCore activity, then open CoreScope Packets.
+ A recent packet attributed to the observer should appear within a few minutes.
+
+
+
+Do not generate unnecessary traffic on a busy mesh. A normal advert or existing nearby activity is enough.
+
+## Check the details
+
+| Check | What you should see |
+|---|---|
+| Name | A clear service name such as `YOW-Repeater-01`, without a home address |
+| Location | The nearest real three-letter airport code, not `CAN`, `XXX`, or `HOME` |
+| Primary path | Connected to `mqtt1.meshcore.ca` with TLS certificate verification |
+| Backup path | Connected to `mqtt2.meshcore.ca` where the method supports two entries |
+| Packet mode | Packet publishing is enabled, not status-only |
+| Packet time | A recent packet appears after the radio hears nearby traffic |
+| Radio | Local settings are used; the Canada defaults apply only when no local override exists |
+
+## Keep a maintenance note
+
+Keep a short private maintenance note:
+
+```text
+Observer:
+Method and version:
+Location code:
+Checked at (include time zone):
+Radio heard a packet: yes / no
+Primary connected: yes / no
+Backup connected: yes / no / not supported
+Observer visible: yes / no
+Packet visible: yes / no
+```
+
+Do not include credentials, private keys, Wi-Fi names, or Wi-Fi passwords.
+
+## Start with the first thing that failed
+
+Use the matching guide for the first thing that failed:
+
+| What failed first | Where to start |
+|---|---|
+| Radio does not hear activity | [Observer appears but no packets](troubleshooting.md#observer-appears-but-no-packets-arrive) |
+| Observer cannot connect | [Observer never appears](troubleshooting.md#observer-never-appears) |
+| Observer is visible but packet view stays empty | [Observer appears but no packets](troubleshooting.md#observer-appears-but-no-packets-arrive) |
+| Backup alone fails | [Only the backup connection fails](troubleshooting.md#only-the-backup-connection-fails) |
+| Place or name is wrong | [Observer appears in the wrong place](troubleshooting.md#observer-appears-in-the-wrong-place) |
+
+For safe commands and a redacted support note, use [Troubleshooting](troubleshooting.md).
diff --git a/docs/assets/javascripts/analyzer-broker-reference.js b/docs/assets/javascripts/analyzer-broker-reference.js
new file mode 100644
index 0000000..3be0bda
--- /dev/null
+++ b/docs/assets/javascripts/analyzer-broker-reference.js
@@ -0,0 +1,82 @@
+(function () {
+ "use strict";
+
+ var isFrench = /^fr(?:-|$)/i.test(
+ document.documentElement ? document.documentElement.lang || "" : ""
+ );
+
+ function tr(english, french) {
+ return isFrench ? french : english;
+ }
+
+ function cell(row, value) {
+ var item = document.createElement("td");
+ item.textContent = String(value);
+ row.appendChild(item);
+ }
+
+ function validConfig(config) {
+ return config &&
+ config.schema_version === 1 &&
+ Array.isArray(config.brokers) &&
+ config.brokers.length === 2 &&
+ config.brokers.every(function (broker) {
+ return /^mqtt[12]\.meshcore\.ca$/.test(broker.host) &&
+ broker.port === 443 &&
+ broker.transport === "websockets" &&
+ broker.tls === true &&
+ broker.verify_tls === true &&
+ broker.token_audience === broker.host;
+ });
+ }
+
+ async function init() {
+ var root = document.getElementById("broker-reference");
+ if (!root || root.dataset.ready === "true") return;
+ var body = document.getElementById("broker-reference-body");
+ var status = document.getElementById("broker-reference-status");
+ if (!body || !status || !root.dataset.source) return;
+
+ root.dataset.ready = "true";
+ try {
+ var response = await fetch(root.dataset.source, {credentials: "same-origin"});
+ if (!response.ok) throw new Error("configuration request failed");
+ var config = await response.json();
+ if (!validConfig(config)) throw new Error("configuration is invalid");
+
+ var fragment = document.createDocumentFragment();
+ config.brokers.forEach(function (broker) {
+ var row = document.createElement("tr");
+ cell(row, broker.id === "primary"
+ ? tr("Primary", "Principal")
+ : tr("Backup", "Secours"));
+ cell(row, broker.host);
+ cell(row, broker.port);
+ cell(row, "WebSockets");
+ cell(row, tr(
+ "Required; verify certificates",
+ "Requis; vérifier les certificats"
+ ));
+ cell(row, broker.token_audience);
+ fragment.appendChild(row);
+ });
+ body.replaceChildren(fragment);
+ status.textContent = isFrench
+ ? "Configuration de l’observateur " + config.version +
+ ", vérifiée le " + config.last_reviewed + "."
+ : "Observer configuration " + config.version +
+ ", reviewed " + config.last_reviewed + ".";
+ } catch (_error) {
+ status.textContent = tr(
+ "The generated broker table could not be loaded. Open the canonical JSON below.",
+ "Impossible de charger le tableau des serveurs MQTT. Ouvrez le fichier JSON de référence ci-dessous."
+ );
+ }
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init, {once: true});
+ } else {
+ init();
+ }
+})();
diff --git a/docs/assets/javascripts/analyzer-command-builder.js b/docs/assets/javascripts/analyzer-command-builder.js
new file mode 100644
index 0000000..04ef2ca
--- /dev/null
+++ b/docs/assets/javascripts/analyzer-command-builder.js
@@ -0,0 +1,306 @@
+(function () {
+ "use strict";
+
+ var isFrench = /^fr(?:-|$)/i.test(
+ document.documentElement ? document.documentElement.lang || "" : ""
+ );
+
+ function tr(english, french) {
+ return isFrench ? french : english;
+ }
+
+ function utf8Length(text) {
+ if (window.TextEncoder) return new TextEncoder().encode(text).length;
+ return unescape(encodeURIComponent(text)).length;
+ }
+
+ function safeCliToken(text) {
+ // MeshCore's device CLI does not publish a general quoting contract.
+ // Reject ambiguous values instead of guessing shell-style escaping.
+ return /^[A-Za-z0-9!#$%&()*+,./:=?@^_~-]+$/.test(text);
+ }
+
+ function markInvalid(field, invalid) {
+ field.setAttribute("aria-invalid", invalid ? "true" : "false");
+ }
+
+ function redactedCommands(commands) {
+ return commands.map(function (line) {
+ if (line.indexOf("set wifi.ssid ") === 0) {
+ return isFrench ? "set wifi.ssid [masqué]" : "set wifi.ssid [hidden]";
+ }
+ if (line.indexOf("set wifi.pwd ") === 0) {
+ return isFrench ? "set wifi.pwd [masqué]" : "set wifi.pwd [hidden]";
+ }
+ return line;
+ });
+ }
+
+ async function populateLocations(root, datalist, status) {
+ var source = root.dataset.locationSource;
+ if (!source || !datalist) return;
+ try {
+ var response = await fetch(source, {credentials: "same-origin"});
+ if (!response.ok) throw new Error("location data request failed");
+ var payload = await response.json();
+ if (!payload || payload.schema_version !== 1 || !Array.isArray(payload.locations)) {
+ throw new Error("location data is invalid");
+ }
+ var fragment = document.createDocumentFragment();
+ payload.locations.forEach(function (location) {
+ if (!/^[A-Z]{3}$/.test(location.code)) return;
+ var option = document.createElement("option");
+ option.value = location.code;
+ option.label = location.name + ", " + location.province;
+ fragment.appendChild(option);
+ });
+ datalist.replaceChildren(fragment);
+ status.textContent = payload.locations.length + tr(
+ " Canadian quick-list codes loaded.",
+ " codes canadiens chargés dans la liste de suggestions."
+ );
+ } catch (_error) {
+ status.textContent = tr(
+ "Location suggestions are unavailable. You can still enter a real 3-letter airport code.",
+ "Les suggestions d’emplacement ne sont pas disponibles. Vous pouvez tout de même entrer un vrai code d’aéroport à 3 lettres."
+ );
+ }
+ }
+
+ function init() {
+ var root = document.getElementById("observer-command-builder");
+ if (!root || root.dataset.ready === "true") return;
+
+ var fields = {
+ board: document.getElementById("observer-board"),
+ iata: document.getElementById("observer-iata"),
+ role: document.getElementById("observer-role"),
+ number: document.getElementById("observer-number"),
+ ssid: document.getElementById("observer-ssid"),
+ password: document.getElementById("observer-password"),
+ repeat: document.getElementById("observer-repeat")
+ };
+ var output = document.getElementById("observer-command-output");
+ var status = document.getElementById("observer-copy-status");
+ var errors = document.getElementById("observer-command-errors");
+ var summary = document.getElementById("observer-command-summary");
+ var copyButton = document.getElementById("observer-copy-commands");
+ var revealCommandsButton = document.getElementById("observer-reveal-commands");
+ var togglePasswordButton = document.getElementById("observer-toggle-password");
+ var clearButton = document.getElementById("observer-clear-secrets");
+ var datalist = document.getElementById("observer-iata-list");
+ var locationStatus = document.getElementById("observer-location-status");
+ if (
+ Object.values(fields).some(function (field) { return !field; }) ||
+ !output || !status || !errors || !summary || !copyButton ||
+ !revealCommandsButton || !togglePasswordButton || !clearButton
+ ) {
+ return;
+ }
+
+ root.dataset.ready = "true";
+ var exactCommands = [];
+ var commandsRevealed = false;
+
+ function hideExactCommands() {
+ commandsRevealed = false;
+ revealCommandsButton.textContent = tr(
+ "Reveal sensitive commands",
+ "Afficher les commandes sensibles"
+ );
+ revealCommandsButton.setAttribute("aria-pressed", "false");
+ copyButton.disabled = true;
+ }
+
+ function showCurrentCommands() {
+ output.textContent = (commandsRevealed ? exactCommands : redactedCommands(exactCommands)).join("\n");
+ revealCommandsButton.textContent = commandsRevealed
+ ? tr("Hide sensitive commands", "Masquer les commandes sensibles")
+ : tr("Reveal sensitive commands", "Afficher les commandes sensibles");
+ revealCommandsButton.setAttribute("aria-pressed", commandsRevealed ? "true" : "false");
+ copyButton.disabled = !commandsRevealed || !exactCommands.length;
+ }
+
+ function renderSummary(board, iata, nodeName, repeat) {
+ var values = [
+ [tr("Board", "Carte"), fields.board.options[fields.board.selectedIndex].text],
+ [tr("Location", "Emplacement"), iata || tr("Not set", "Non défini")],
+ [tr("Node name", "Nom du nœud"), nodeName || tr("Not set", "Non défini")],
+ [
+ tr("Mesh traffic", "Trafic du réseau maillé"),
+ repeat === "on"
+ ? tr("Observe and repeat", "Observer et relayer")
+ : tr("Observe only", "Observer seulement")
+ ]
+ ];
+ var fragment = document.createDocumentFragment();
+ values.forEach(function (entry) {
+ var wrapper = document.createElement("div");
+ var term = document.createElement("dt");
+ var detail = document.createElement("dd");
+ term.textContent = entry[0];
+ detail.textContent = entry[1];
+ wrapper.appendChild(term);
+ wrapper.appendChild(detail);
+ fragment.appendChild(wrapper);
+ });
+ summary.replaceChildren(fragment);
+ summary.dataset.board = board;
+ }
+
+ function render() {
+ var messages = [];
+ var board = fields.board.value;
+ var iata = (fields.iata.value || "").trim().toUpperCase();
+ var role = fields.role.value;
+ var number = (fields.number.value || "").trim();
+ var ssid = fields.ssid.value || "";
+ var password = fields.password.value || "";
+ var repeat = fields.repeat.value;
+ var nodeName = iata && number ? iata + "-" + role + "-" + number : "";
+
+ [fields.iata, fields.number, fields.ssid, fields.password].forEach(function (field) {
+ markInvalid(field, false);
+ });
+
+ if (!/^[A-Z]{3}$/.test(iata) || iata === "XXX" || iata === "CAN") {
+ messages.push(tr(
+ "Enter a real 3-letter airport code; do not use XXX or CAN.",
+ "Entrez un vrai code d’aéroport à 3 lettres; n’utilisez pas XXX ni CAN."
+ ));
+ markInvalid(fields.iata, true);
+ }
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,15}$/.test(number)) {
+ messages.push(tr(
+ "Node number must use 1–16 letters, numbers, underscores, or hyphens.",
+ "Le numéro du nœud doit comporter de 1 à 16 lettres, chiffres, traits de soulignement ou traits d’union."
+ ));
+ markInvalid(fields.number, true);
+ }
+ if (nodeName && utf8Length(nodeName) > 24) {
+ messages.push(tr(
+ "The generated node name is over 24 bytes; shorten the node number.",
+ "Le nom de nœud généré dépasse 24 octets; raccourcissez le numéro du nœud."
+ ));
+ markInvalid(fields.number, true);
+ }
+ if (!ssid || utf8Length(ssid) > 32 || !safeCliToken(ssid)) {
+ messages.push(tr(
+ "Enter a 1–32 byte SSID without spaces, quotes, backslashes, semicolons, pipes, control characters, or non-ASCII text. Use Configure via USB for other SSIDs.",
+ "Entrez un SSID de 1 à 32 octets, sans espaces, guillemets, barres obliques inverses, points-virgules, barres verticales, caractères de contrôle ni texte non ASCII. Utilisez Configure via USB pour les autres SSID."
+ ));
+ markInvalid(fields.ssid, true);
+ }
+ if (!password || utf8Length(password) > 64 || !safeCliToken(password)) {
+ messages.push(tr(
+ "Enter a 1–64 byte password using the safe CLI character set, or use Configure via USB for this network.",
+ "Entrez un mot de passe de 1 à 64 octets avec les caractères permis par l’interface de commande, ou utilisez Configure via USB pour ce réseau."
+ ));
+ markInvalid(fields.password, true);
+ }
+
+ hideExactCommands();
+ errors.textContent = messages.join(" ");
+ renderSummary(board, iata, nodeName, repeat);
+
+ if (messages.length) {
+ exactCommands = [];
+ revealCommandsButton.disabled = true;
+ output.textContent = tr(
+ "Complete the required fields to build commands.",
+ "Remplissez les champs obligatoires pour générer les commandes."
+ );
+ return;
+ }
+
+ exactCommands = [
+ "set name " + nodeName,
+ "set radio 910.525,62.5,7,5",
+ "set path.hash.mode 2",
+ "set mqtt.iata " + iata,
+ "set wifi.ssid " + ssid,
+ "set wifi.pwd " + password,
+ "set wifi.powersave none",
+ "set mqtt1.preset meshcore-ca-1",
+ "set mqtt2.preset meshcore-ca-2",
+ "set mqtt3.preset none",
+ "set mqtt4.preset none",
+ "set mqtt5.preset none",
+ "set mqtt6.preset none",
+ "set mqtt.status on",
+ "set mqtt.packets on",
+ "set mqtt.raw off",
+ "set mqtt.rx on",
+ "set mqtt.tx advert",
+ "set bridge.enabled on",
+ "set repeat " + repeat,
+ "advert",
+ "reboot"
+ ];
+ revealCommandsButton.disabled = false;
+ showCurrentCommands();
+ }
+
+ function clearSecrets() {
+ fields.ssid.value = "";
+ fields.password.value = "";
+ fields.password.type = "password";
+ togglePasswordButton.textContent = tr("Show", "Afficher");
+ togglePasswordButton.setAttribute("aria-pressed", "false");
+ exactCommands = [];
+ hideExactCommands();
+ output.textContent = tr("Wi-Fi fields cleared.", "Champs Wi-Fi effacés.");
+ errors.textContent = "";
+ fields.ssid.focus();
+ }
+
+ root.addEventListener("input", render);
+ root.addEventListener("change", render);
+ togglePasswordButton.addEventListener("click", function () {
+ var showing = fields.password.type === "text";
+ fields.password.type = showing ? "password" : "text";
+ togglePasswordButton.textContent = showing
+ ? tr("Show", "Afficher")
+ : tr("Hide", "Masquer");
+ togglePasswordButton.setAttribute("aria-pressed", showing ? "false" : "true");
+ fields.password.focus();
+ });
+ revealCommandsButton.addEventListener("click", function () {
+ if (!exactCommands.length) return;
+ commandsRevealed = !commandsRevealed;
+ showCurrentCommands();
+ });
+ copyButton.addEventListener("click", function () {
+ if (!commandsRevealed || !exactCommands.length) return;
+ if (!navigator.clipboard || !navigator.clipboard.writeText) {
+ status.textContent = tr(
+ "Clipboard unavailable. Copy the revealed commands manually.",
+ "Le presse-papiers n’est pas disponible. Copiez manuellement les commandes affichées."
+ );
+ return;
+ }
+ navigator.clipboard.writeText(exactCommands.join("\n")).then(function () {
+ status.textContent = tr(
+ "Copied. Your clipboard now contains Wi-Fi credentials.",
+ "Copié. Votre presse-papiers contient maintenant vos identifiants Wi-Fi."
+ );
+ }, function () {
+ status.textContent = tr(
+ "Copy failed. Copy the revealed commands manually.",
+ "Échec de la copie. Copiez manuellement les commandes affichées."
+ );
+ });
+ });
+ clearButton.addEventListener("click", clearSecrets);
+ window.addEventListener("pagehide", clearSecrets);
+
+ populateLocations(root, datalist, locationStatus);
+ render();
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init, {once: true});
+ } else {
+ init();
+ }
+})();
diff --git a/docs/assets/javascripts/analyzer-location-codes.js b/docs/assets/javascripts/analyzer-location-codes.js
new file mode 100644
index 0000000..0173956
--- /dev/null
+++ b/docs/assets/javascripts/analyzer-location-codes.js
@@ -0,0 +1,172 @@
+(function () {
+ "use strict";
+
+ var isFrench = /^fr(?:-|$)/i.test(
+ document.documentElement ? document.documentElement.lang || "" : ""
+ );
+
+ function tr(english, french) {
+ return isFrench ? french : english;
+ }
+
+ var frenchProvinceNames = {
+ BC: "Colombie-Britannique",
+ AB: "Alberta",
+ SK: "Saskatchewan",
+ MB: "Manitoba",
+ ON: "Ontario",
+ QC: "Québec",
+ NB: "Nouveau-Brunswick",
+ NS: "Nouvelle-Écosse",
+ PE: "Île-du-Prince-Édouard",
+ NL: "Terre-Neuve-et-Labrador",
+ YT: "Yukon",
+ NT: "Territoires du Nord-Ouest",
+ NU: "Nunavut"
+ };
+
+ var frenchLocationNames = {
+ YYZ: "Toronto-Pearson",
+ YTZ: "Toronto-Billy Bishop",
+ YUL: "Montréal-Trudeau",
+ YMX: "Montréal-Mirabel",
+ YQB: "Québec",
+ YND: "Gatineau / région d’Ottawa",
+ YHU: "Montréal–Saint-Hubert",
+ YGL: "La Grande Rivière",
+ YZV: "Sept-Îles",
+ YGP: "Gaspé",
+ YRQ: "Trois-Rivières"
+ };
+
+ function localizedProvince(location) {
+ return isFrench && frenchProvinceNames[location.province_code]
+ ? frenchProvinceNames[location.province_code]
+ : location.province;
+ }
+
+ function localizedName(location) {
+ return isFrench && frenchLocationNames[location.code]
+ ? frenchLocationNames[location.code]
+ : location.name;
+ }
+
+ function textCell(text) {
+ var cell = document.createElement("td");
+ cell.textContent = text;
+ return cell;
+ }
+
+ function validPayload(payload) {
+ if (!payload || payload.schema_version !== 1 || !Array.isArray(payload.locations)) return false;
+ var seen = new Set();
+ return payload.locations.every(function (location) {
+ if (
+ !location ||
+ !/^[A-Z]{3}$/.test(location.code) ||
+ !/^[A-Z]{2}$/.test(location.province_code) ||
+ typeof location.name !== "string" ||
+ typeof location.province !== "string" ||
+ seen.has(location.code)
+ ) {
+ return false;
+ }
+ seen.add(location.code);
+ return true;
+ });
+ }
+
+ async function init() {
+ var root = document.getElementById("location-code-tool");
+ if (!root || root.dataset.ready === "true") return;
+
+ var search = document.getElementById("location-code-search");
+ var province = document.getElementById("location-code-province");
+ var body = document.getElementById("location-code-results");
+ var status = document.getElementById("location-code-status");
+ var source = root.dataset.source;
+ if (!search || !province || !body || !status || !source) return;
+
+ root.dataset.ready = "true";
+ status.textContent = tr(
+ "Loading location codes…",
+ "Chargement des codes d’emplacement…"
+ );
+
+ try {
+ var response = await fetch(source, {credentials: "same-origin"});
+ if (!response.ok) throw new Error("location data request failed");
+ var payload = await response.json();
+ if (!validPayload(payload)) throw new Error("location data is invalid");
+
+ var locale = isFrench ? "fr-CA" : "en-CA";
+ var locations = payload.locations.slice().sort(function (left, right) {
+ return localizedProvince(left).localeCompare(localizedProvince(right), locale) ||
+ localizedName(left).localeCompare(localizedName(right), locale);
+ });
+ var provinces = Array.from(new Set(locations.map(function (item) {
+ return item.province;
+ }))).sort(function (left, right) {
+ var leftLocation = locations.find(function (item) { return item.province === left; });
+ var rightLocation = locations.find(function (item) { return item.province === right; });
+ return localizedProvince(leftLocation).localeCompare(localizedProvince(rightLocation), locale);
+ });
+
+ provinces.forEach(function (name) {
+ var option = document.createElement("option");
+ option.value = name;
+ var representative = locations.find(function (item) { return item.province === name; });
+ option.textContent = localizedProvince(representative);
+ province.appendChild(option);
+ });
+
+ function render() {
+ var query = search.value.trim().toLocaleLowerCase(locale);
+ var selectedProvince = province.value;
+ var matches = locations.filter(function (location) {
+ var searchable = [
+ location.code,
+ location.name,
+ location.province,
+ localizedName(location),
+ localizedProvince(location),
+ location.province_code
+ ].join(" ").toLocaleLowerCase(locale);
+ return (!query || searchable.includes(query)) &&
+ (!selectedProvince || location.province === selectedProvince);
+ });
+
+ var fragment = document.createDocumentFragment();
+ matches.forEach(function (location) {
+ var row = document.createElement("tr");
+ row.appendChild(textCell(location.code));
+ row.appendChild(textCell(localizedName(location)));
+ row.appendChild(textCell(localizedProvince(location)));
+ fragment.appendChild(row);
+ });
+ body.replaceChildren(fragment);
+ status.textContent = matches.length === 1
+ ? tr("1 location code shown.", "1 code d’emplacement affiché.")
+ : matches.length + tr(
+ " location codes shown.",
+ " codes d’emplacement affichés."
+ );
+ }
+
+ search.addEventListener("input", render);
+ province.addEventListener("change", render);
+ render();
+ } catch (_error) {
+ status.textContent = tr(
+ "The location list could not be loaded. Use the raw data link below or try again.",
+ "Impossible de charger la liste des emplacements. Utilisez le lien vers les données brutes ci-dessous ou réessayez."
+ );
+ }
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init, {once: true});
+ } else {
+ init();
+ }
+})();
diff --git a/docs/assets/javascripts/analyzer-method-chooser.js b/docs/assets/javascripts/analyzer-method-chooser.js
new file mode 100644
index 0000000..c0460a0
--- /dev/null
+++ b/docs/assets/javascripts/analyzer-method-chooser.js
@@ -0,0 +1,91 @@
+(function () {
+ "use strict";
+
+ var isFrench = /^fr(?:-|$)/i.test(
+ document.documentElement ? document.documentElement.lang || "" : ""
+ );
+
+ function tr(english, french) {
+ return isFrench ? french : english;
+ }
+
+ var methods = {
+ "remote-term": {
+ title: tr("Use RemoteTerm", "Utiliser RemoteTerm"),
+ description: tr(
+ "Keep using the radio connection and management screen you already have.",
+ "Continuez d’utiliser la connexion radio et l’écran de gestion dont vous disposez déjà."
+ ),
+ href: "../remoteterm/"
+ },
+ "home-assistant": {
+ title: tr("Use Home Assistant", "Utiliser Home Assistant"),
+ description: tr(
+ "Add the MeshCore Canada broker pair inside your existing MeshCore integration.",
+ "Ajoutez les deux serveurs MQTT de MeshCore Canada à votre intégration MeshCore existante."
+ ),
+ href: "../builds/meshcore-ha/"
+ },
+ "pymc": {
+ title: tr("Use PyMC", "Utiliser PyMC"),
+ description: tr(
+ "Add the broker pair to the PyMC service you already operate.",
+ "Ajoutez les deux serveurs MQTT au service PyMC que vous exploitez déjà."
+ ),
+ href: "../builds/pymc/"
+ },
+ "usb-host": {
+ title: tr("Use MCtoMQTT", "Utiliser MCtoMQTT"),
+ description: tr(
+ "Run a small bridge on the Linux or macOS computer connected to the radio by USB.",
+ "Exécutez une petite passerelle sur l’ordinateur Linux ou macOS relié à la radio par USB."
+ ),
+ href: "../builds/mctomqtt/"
+ },
+ "wifi-board": {
+ title: tr("Use standalone MQTT firmware", "Utiliser un micrologiciel MQTT autonome"),
+ description: tr(
+ "A supported Wi-Fi LoRa board can listen and publish without a separate computer.",
+ "Une carte LoRa Wi-Fi compatible peut écouter et publier sans ordinateur distinct."
+ ),
+ href: "../builds/mqtt-firmware/"
+ }
+ };
+
+ function init() {
+ var root = document.getElementById("observer-method-chooser");
+ if (!root || root.dataset.ready === "true") return;
+
+ var select = root.querySelector("select");
+ var result = document.getElementById("observer-method-result");
+ if (!select || !result) return;
+
+ root.dataset.ready = "true";
+ select.addEventListener("change", function () {
+ var method = methods[select.value];
+ if (!method) {
+ result.hidden = true;
+ result.replaceChildren();
+ return;
+ }
+
+ var heading = document.createElement("strong");
+ heading.textContent = method.title;
+ var detail = document.createElement("span");
+ detail.textContent = method.description + " ";
+ var link = document.createElement("a");
+ link.href = method.href;
+ link.textContent = tr("Open this setup guide", "Ouvrir ce guide de configuration");
+ detail.appendChild(link);
+ result.replaceChildren(heading, detail);
+ result.hidden = false;
+ result.focus();
+ });
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", init, {once: true});
+ } else {
+ init();
+ }
+})();
diff --git a/docs/assets/javascripts/bootstrap.js b/docs/assets/javascripts/bootstrap.js
new file mode 100644
index 0000000..f1f4be6
--- /dev/null
+++ b/docs/assets/javascripts/bootstrap.js
@@ -0,0 +1,157 @@
+(function () {
+ "use strict";
+
+ var storagePrefix = "meshcore-canada:progress:v1:";
+
+ function storageKey(input) {
+ var pageKey = input.closest("[data-mc-progress-page]");
+ var scope = pageKey ? pageKey.getAttribute("data-mc-progress-page") : window.location.pathname;
+ return storagePrefix + scope + ":" + input.id;
+ }
+
+ function readProgress(input) {
+ try {
+ return window.localStorage.getItem(storageKey(input)) === "done";
+ } catch (_error) {
+ return false;
+ }
+ }
+
+ function writeProgress(input) {
+ try {
+ if (input.checked) {
+ window.localStorage.setItem(storageKey(input), "done");
+ } else {
+ window.localStorage.removeItem(storageKey(input));
+ }
+ } catch (_error) {
+ // The journey still works when storage is blocked or unavailable.
+ }
+ }
+
+ function initialiseProgress() {
+ document.querySelectorAll("input[type='checkbox'][data-mc-progress]").forEach(function (input) {
+ if (!input.id || input.dataset.mcProgressReady === "true") return;
+ input.dataset.mcProgressReady = "true";
+ input.checked = readProgress(input);
+ input.addEventListener("change", function () {
+ writeProgress(input);
+ });
+ });
+ }
+
+ function labelExternalLinks() {
+ document.querySelectorAll("a[target='_blank']").forEach(function (link) {
+ if (link.dataset.mcExternalReady === "true") return;
+ link.dataset.mcExternalReady = "true";
+ var current = (link.getAttribute("aria-label") || "").trim();
+ if (!current) {
+ var text = (link.textContent || "").trim();
+ if (!text) text = (link.getAttribute("title") || "External link").trim();
+ link.setAttribute("aria-label", text + " (opens in a new tab)");
+ }
+ });
+ }
+
+ function makeHeaderToggleAccessible(label, checkbox, name, panel) {
+ if (!label || !checkbox || label.dataset.mcToggleReady === "true") return;
+ label.dataset.mcToggleReady = "true";
+ label.setAttribute("role", "button");
+ label.setAttribute("tabindex", "0");
+ if (!label.hasAttribute("aria-label")) label.setAttribute("aria-label", name);
+ if (panel) {
+ if (!panel.id) panel.id = "mc-" + checkbox.id.replace(/^__/, "") + "-panel";
+ label.setAttribute("aria-controls", panel.id);
+ }
+
+ function updateState() {
+ var expanded = checkbox.checked;
+ label.setAttribute("aria-expanded", expanded ? "true" : "false");
+ label.setAttribute("aria-label", expanded ? name.replace(/^Open /, "Close ") : name);
+ }
+
+ var suppressKeyboardClick = false;
+ label.addEventListener("click", function (event) {
+ if (!suppressKeyboardClick) return;
+ event.preventDefault();
+ event.stopPropagation();
+ suppressKeyboardClick = false;
+ });
+ label.addEventListener("keydown", function (event) {
+ if (event.key !== "Enter" && event.key !== " ") return;
+ event.preventDefault();
+ event.stopPropagation();
+ suppressKeyboardClick = true;
+ checkbox.checked = !checkbox.checked;
+ checkbox.dispatchEvent(new Event("change", { bubbles: true }));
+ window.setTimeout(function () { suppressKeyboardClick = false; }, 0);
+ });
+ checkbox.addEventListener("change", updateState);
+ updateState();
+ }
+
+ function labelThemeControls() {
+ var search = document.querySelector(".md-search[role='dialog']");
+ if (search && !search.hasAttribute("aria-label")) {
+ search.setAttribute("aria-label", "Site search");
+ }
+
+ var drawerToggle = document.getElementById("__drawer");
+ var drawer = document.querySelector(".md-sidebar--primary");
+ document.querySelectorAll("label.md-header__button[for='__drawer']").forEach(function (label) {
+ makeHeaderToggleAccessible(label, drawerToggle, "Open navigation", drawer);
+ });
+
+ var searchToggle = document.getElementById("__search");
+ document.querySelectorAll("label.md-header__button[for='__search'], label.md-search__icon[for='__search']").forEach(function (label) {
+ makeHeaderToggleAccessible(label, searchToggle, "Open site search", search);
+ });
+
+ document.querySelectorAll("a[href]").forEach(function (link) {
+ if ((link.textContent || "").trim() !== "Start") return;
+ if (!/\/start\/(?:$|[?#])/.test(link.href)) return;
+ link.setAttribute("aria-label", "Start using MeshCore");
+ });
+ }
+
+ function initialiseSearchInputs() {
+ document.querySelectorAll(".md-search__input").forEach(function (input) {
+ if (input.dataset.mcSearchReady === "true") return;
+ input.dataset.mcSearchReady = "true";
+
+ var pendingKeyup;
+
+ input.addEventListener("input", function () {
+ window.clearTimeout(pendingKeyup);
+ pendingKeyup = window.setTimeout(function () {
+ input.dispatchEvent(new KeyboardEvent("keyup", {
+ bubbles: true,
+ key: "Unidentified",
+ code: "Unidentified"
+ }));
+ }, 60);
+ });
+
+ input.addEventListener("keyup", function (event) {
+ if (event.isTrusted) {
+ window.clearTimeout(pendingKeyup);
+ }
+ });
+ });
+ }
+
+ function initialise() {
+ initialiseProgress();
+ labelExternalLinks();
+ labelThemeControls();
+ initialiseSearchInputs();
+ }
+
+ if (window.document$ && typeof window.document$.subscribe === "function") {
+ window.document$.subscribe(initialise);
+ } else if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initialise, { once: true });
+ } else {
+ initialise();
+ }
+})();
diff --git a/docs/assets/javascripts/communities.js b/docs/assets/javascripts/communities.js
new file mode 100644
index 0000000..5389fdd
--- /dev/null
+++ b/docs/assets/javascripts/communities.js
@@ -0,0 +1,113 @@
+(function () {
+ "use strict";
+
+ function normalize(value) {
+ return String(value || "")
+ .normalize("NFKD")
+ .replace(/[\u0300-\u036f]/g, "")
+ .toLocaleLowerCase()
+ .replace(/\s+/g, " ")
+ .trim();
+ }
+
+ function initializeDirectory(root) {
+ if (!root || root.dataset.communityReady === "true") {
+ return;
+ }
+
+ var search = root.querySelector("#community-search");
+ var status = root.querySelector("#community-status");
+ var override = root.querySelector("#community-override");
+ var count = root.querySelector("[data-community-count]");
+ var page = root.closest(".md-content") || document;
+ var cards = Array.prototype.slice.call(
+ page.querySelectorAll("[data-community-card]")
+ );
+ var empty = page.querySelector("[data-community-empty]");
+ var clearButtons = page.querySelectorAll("[data-community-clear]");
+
+ if (!search || !status || !override || !count || !empty || !cards.length) {
+ return;
+ }
+
+ root.dataset.communityReady = "true";
+
+ function updateUrl(query) {
+ if (!window.history || !window.history.replaceState) {
+ return;
+ }
+ var url = new URL(window.location.href);
+ if (query) {
+ url.searchParams.set("community", query);
+ } else {
+ url.searchParams.delete("community");
+ }
+ window.history.replaceState(null, "", url);
+ }
+
+ function applyFilters(options) {
+ var query = normalize(search.value);
+ var selectedStatus = status.value;
+ var requireOverride = override.checked;
+ var visible = 0;
+
+ cards.forEach(function (card) {
+ var matchesQuery =
+ !query || normalize(card.dataset.communitySearch).indexOf(query) !== -1;
+ var matchesStatus =
+ !selectedStatus || card.dataset.communityStatus === selectedStatus;
+ var matchesOverride =
+ !requireOverride || card.dataset.communityOverride === "true";
+ var show = matchesQuery && matchesStatus && matchesOverride;
+ card.hidden = !show;
+ if (show) {
+ visible += 1;
+ }
+ });
+
+ empty.hidden = visible !== 0;
+ count.textContent =
+ "Showing " + visible + " " + (visible === 1 ? "community" : "communities");
+ if (!options || options.updateUrl !== false) {
+ updateUrl(search.value.trim());
+ }
+ }
+
+ function clearFilters() {
+ search.value = "";
+ status.value = "";
+ override.checked = false;
+ applyFilters();
+ search.focus();
+ }
+
+ search.addEventListener("input", applyFilters);
+ status.addEventListener("change", applyFilters);
+ override.addEventListener("change", applyFilters);
+ clearButtons.forEach(function (button) {
+ button.addEventListener("click", clearFilters);
+ });
+
+ var initialQuery = new URL(window.location.href).searchParams.get("community");
+ if (initialQuery) {
+ search.value = initialQuery;
+ }
+ applyFilters({ updateUrl: false });
+ }
+
+ function initialize() {
+ document
+ .querySelectorAll("[data-community-directory]")
+ .forEach(initializeDirectory);
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initialize, { once: true });
+ } else {
+ initialize();
+ }
+
+ if (typeof window.document$ !== "undefined") {
+ window.document$.subscribe(initialize);
+ }
+})();
diff --git a/docs/assets/javascripts/i18n-runtime.js b/docs/assets/javascripts/i18n-runtime.js
new file mode 100644
index 0000000..cb8cf10
--- /dev/null
+++ b/docs/assets/javascripts/i18n-runtime.js
@@ -0,0 +1,453 @@
+(function () {
+ "use strict";
+
+ var pathIsFrench = /(?:^|\/)fr(?:\/|$)/.test(window.location.pathname);
+ var declaredLanguage = (document.documentElement.lang || "").toLowerCase();
+ var isFrench = pathIsFrench || declaredLanguage === "fr" || declaredLanguage.indexOf("fr-") === 0;
+
+ var dictionary = {
+ "External link": "Lien externe",
+ "Site search": "Recherche sur le site",
+ "Open navigation": "Ouvrir la navigation",
+ "Close navigation": "Fermer la navigation",
+ "Open site search": "Ouvrir la recherche sur le site",
+ "Close site search": "Fermer la recherche sur le site",
+ "Zoom in": "Zoom avant",
+ "Zoom out": "Zoom arrière",
+ "Start using MeshCore": "Commencer avec MeshCore",
+ "Region boundary editor · MeshCore Canada": "Éditeur des limites régionales · MeshCore Canada",
+ "Skip the map and choose a municipality": "Passer la carte et choisir une municipalité",
+ "Region boundary editor": "Éditeur des limites régionales",
+ "Editor": "Éditeur",
+ "Home": "Accueil",
+ "Standard": "Norme",
+ "Help": "Aide",
+ "Exit editor": "Quitter l’éditeur",
+ "Boundary editing controls": "Commandes de modification des limites",
+ "Region proposal": "Proposition régionale",
+ "Suggest a region change": "Proposer une modification régionale",
+ "Submitting creates a public proposal for review; it does not change the map.": "La soumission crée une proposition publique à examiner; elle ne modifie pas la carte.",
+ "Read the region rules": "Lire les règles régionales",
+ "1. Choose proposal type": "1. Choisir le type de proposition",
+ "Proposal type": "Type de proposition",
+ "Adjust an existing boundary": "Modifier une limite existante",
+ "Move official census areas to an existing region.": "Déplacer des zones de recensement officielles vers une région existante.",
+ "Advanced: propose a new region/subregion": "Avancé : proposer une nouvelle région ou sous-région",
+ "Define a new public name, tag, cells, and anchor for review.": "Définir un nouveau nom public, un identifiant, des cellules et un point d’ancrage à examiner.",
+ "2. Choose an area": "2. Choisir une zone",
+ "Province or territory": "Province ou territoire",
+ "Loading editor…": "Chargement de l’éditeur…",
+ "Choose a proposal type to begin.": "Choisissez un type de proposition pour commencer.",
+ "Discard saved draft": "Supprimer le brouillon enregistré",
+ "3. Build the proposal": "3. Préparer la proposition",
+ "Move selected areas to": "Déplacer les zones sélectionnées vers",
+ "Choose a target region": "Choisir une région de destination",
+ "New region name": "Nom de la nouvelle région",
+ "Short tag": "Identifiant court",
+ "Lowercase letters, numbers and hyphens; up to 29 characters.": "Lettres minuscules, chiffres et traits d’union; 29 caractères au maximum.",
+ "Suggested tag:": "Identifiant proposé :",
+ "Use suggestion": "Utiliser la suggestion",
+ "Proposed hierarchy": "Hiérarchie proposée",
+ "Add cells to derive the parent.": "Ajoutez des cellules pour déterminer la région parente.",
+ "Canada › Province or territory › Proposed region": "Canada › Province ou territoire › Région proposée",
+ "The parent comes from the shared ancestry of the selected cells and is verified by the server.": "La région parente vient de la hiérarchie commune des cellules sélectionnées et sera vérifiée par le serveur.",
+ "Municipality or census subdivision": "Municipalité ou subdivision de recensement",
+ "Choose a municipality": "Choisir une municipalité",
+ "Add whole municipality": "Ajouter toute la municipalité",
+ "Start with a whole municipality. You do not need the map to submit a proposal.": "Commencez par une municipalité entière. La carte n’est pas nécessaire pour soumettre une proposition.",
+ "Repeater paths": "Chemins des répéteurs",
+ "This editor changes Canadian map cells only. Set cross-province and U.S. paths in the configurator.": "Cet éditeur modifie seulement les cellules canadiennes. Réglez les chemins interprovinciaux et américains dans le configurateur.",
+ "Read the rule": "Lire la règle",
+ "Advanced: split a municipality by census cell": "Avancé : diviser une municipalité par cellule de recensement",
+ "Use individual cells only when a full municipality does not fit the boundary.": "Utilisez des cellules individuelles seulement si la municipalité entière ne correspond pas à la limite.",
+ "This creates a finer boundary for review.": "Cela crée une limite plus précise à examiner.",
+ "Map editing mode": "Mode de modification de la carte",
+ "Pan": "Déplacer",
+ "Inspect": "Examiner",
+ "Paint": "Peindre",
+ "Map inspection": "Examen de la carte",
+ "Area": "Zone",
+ "Select a cell on the map": "Sélectionnez une cellule sur la carte",
+ "Municipality": "Municipalité",
+ "Current region": "Région actuelle",
+ "Region anchor": "Point d’ancrage régional",
+ "Move entire municipality": "Déplacer toute la municipalité",
+ "New region anchor": "Point d’ancrage de la nouvelle région",
+ "Choose a changed cell": "Choisir une cellule modifiée",
+ "Confirm this anchor": "Confirmer ce point d’ancrage",
+ "Choose a changed cell near the centre, then confirm it. Existing fixed anchors cannot be used.": "Choisissez une cellule modifiée près du centre, puis confirmez-la. Les points d’ancrage fixes existants ne peuvent pas être utilisés.",
+ "4. Review": "4. Relire",
+ "0 changes": "0 modification",
+ "Undo": "Annuler",
+ "Redo": "Rétablir",
+ "Clear": "Effacer",
+ "Boundary comparison": "Comparaison des limites",
+ "Before": "Avant",
+ "After": "Après",
+ "Proposal summary": "Résumé de la proposition",
+ "Choose a proposal type and geography to build a text review.": "Choisissez un type de proposition et une zone géographique pour produire le résumé.",
+ "Proposed ownership changes": "Modifications proposées de l’attribution",
+ "Before and after by official census area": "Avant et après par zone de recensement officielle",
+ "Municipality / area": "Municipalité ou zone",
+ "Action": "Action",
+ "No proposed changes.": "Aucune modification proposée.",
+ "Submitted by": "Soumis par",
+ "optional": "facultatif",
+ "This field is never stored in the local draft.": "Ce champ n’est jamais enregistré dans le brouillon local.",
+ "Reason": "Raison",
+ "Why should this cell move?": "Pourquoi cette cellule devrait-elle être déplacée?",
+ "Ready to submit?": "Prêt à soumettre?",
+ "Choose a proposal type": "Choisir un type de proposition",
+ "Choose a province or territory": "Choisir une province ou un territoire",
+ "Choose or define the destination region": "Choisir ou définir la région de destination",
+ "Add at least one official census cell": "Ajouter au moins une cellule de recensement officielle",
+ "Confirm an anchor for a new region": "Confirmer un point d’ancrage pour la nouvelle région",
+ "Explain the reason": "Expliquer la raison",
+ "Complete the anti-spam check": "Effectuer la vérification antipourriel",
+ "Website": "Site Web",
+ "Anti-spam check": "Vérification antipourriel",
+ "Loading check…": "Chargement de la vérification…",
+ "Retry check": "Réessayer la vérification",
+ "Submit for review": "Soumettre aux fins d’examen",
+ "Download proposal": "Télécharger la proposition",
+ "No account needed. Submission starts public review; it does not change the map.": "Aucun compte n’est nécessaire. La soumission lance l’examen public; elle ne modifie pas la carte.",
+ "Region map editor": "Éditeur de la carte des régions",
+ "Inspect cells or use the municipality list; yellow means changed": "Examinez les cellules ou utilisez la liste des municipalités; le jaune indique une modification",
+ "Optional interactive region boundary map": "Carte interactive facultative des limites régionales",
+ "The map is optional. Use the municipality list in step three for a full non-map flow. In Inspect mode, select a cell to read its details. Enable the advanced acknowledgement before painting individual cells.": "La carte est facultative. Utilisez la liste des municipalités à l’étape trois pour travailler sans carte. En mode Examiner, sélectionnez une cellule pour voir ses détails. Confirmez l’option avancée avant de peindre des cellules individuelles.",
+ "Proposed change": "Modification proposée",
+ "Discard this draft?": "Supprimer ce brouillon?",
+ "Keep editing": "Continuer la modification",
+ "Discard draft": "Supprimer le brouillon",
+ "British Columbia": "Colombie-Britannique",
+ "Alberta": "Alberta",
+ "Saskatchewan": "Saskatchewan",
+ "Manitoba": "Manitoba",
+ "Ontario": "Ontario",
+ "Quebec": "Québec",
+ "New Brunswick": "Nouveau-Brunswick",
+ "Nova Scotia": "Nouvelle-Écosse",
+ "Prince Edward Island": "Île-du-Prince-Édouard",
+ "Newfoundland and Labrador": "Terre-Neuve-et-Labrador",
+ "Yukon": "Yukon",
+ "Northwest Territories": "Territoires du Nord-Ouest",
+ "Nunavut": "Nunavut",
+ "Local draft storage is unavailable. Download before leaving.": "Le stockage local des brouillons n’est pas disponible. Téléchargez la proposition avant de partir.",
+ "No saved draft.": "Aucun brouillon enregistré.",
+ "Saved locally.": "Brouillon enregistré sur cet appareil.",
+ "Draft could not be saved locally. Download before leaving.": "Le brouillon n’a pas pu être enregistré sur cet appareil. Téléchargez la proposition avant de partir.",
+ "Unsaved changes…": "Modifications non enregistrées…",
+ "An earlier draft was submitted": "Une version antérieure du brouillon a été soumise",
+ "This proposal was already submitted": "Cette proposition a déjà été soumise",
+ "Public review created": "Examen public créé",
+ "Open": "Ouvrir",
+ "Schema": "Schéma",
+ "Proposal hash": "Hachage de la proposition",
+ "Submitted version": "Version soumise",
+ "Next: community review, maintainer approval, then independent repository validation. Submission alone does not change the map.": "Ensuite : examen par la communauté, approbation par l’équipe de maintenance, puis validation indépendante dans le dépôt. La soumission seule ne modifie pas la carte.",
+ "That public region name is already used or reserved.": "Ce nom de région public est déjà utilisé ou réservé.",
+ "Use lowercase letters, numbers and single hyphens; do not start with a hyphen.": "Utilisez des lettres minuscules, des chiffres et des traits d’union simples; ne commencez pas par un trait d’union.",
+ "That canonical tag is already used or reserved.": "Cet identifiant canonique est déjà utilisé ou réservé.",
+ "Not derived yet": "Pas encore déterminée",
+ "Proposed region": "Région proposée",
+ "Add cells before choosing an anchor": "Ajoutez des cellules avant de choisir un point d’ancrage",
+ "Census area": "Zone de recensement",
+ "Choose a changed cell near the centre, then confirm it. No anchor is selected automatically.": "Choisissez une cellule modifiée près du centre, puis confirmez-la. Aucun point d’ancrage n’est sélectionné automatiquement.",
+ "This editor changes Canadian map cells only. Choose cross-province and U.S. paths in the configurator.": "Cet éditeur modifie seulement les cellules canadiennes. Choisissez les chemins interprovinciaux et américains dans le configurateur.",
+ "Edit each province separately; repeater setup keeps the paths together.": "Modifiez chaque province séparément; la configuration du répéteur conserve les chemins ensemble.",
+ "Unnamed census area": "Zone de recensement sans nom",
+ "Remove": "Retirer",
+ "Proposal type": "Type de proposition",
+ "New region/subregion (v2)": "Nouvelle région ou sous-région (v2)",
+ "Existing boundary adjustment (v1)": "Modification d’une limite existante (v1)",
+ "Jurisdiction and hierarchy": "Champ de compétence et hiérarchie",
+ "Destination": "Destination",
+ "Not chosen": "Non choisie",
+ "Official geography": "Géographie officielle",
+ "Sources": "Sources",
+ "None yet": "Aucune pour le moment",
+ "Anchor": "Point d’ancrage",
+ "Not confirmed": "Non confirmé",
+ "Existing fixed anchors remain protected": "Les points d’ancrage fixes existants restent protégés",
+ "Authority base": "Base faisant autorité",
+ "Loading": "Chargement",
+ "Validation": "Validation",
+ "Client checks run before export. The gateway and repository independently revalidate every authority rule.": "Les vérifications du navigateur s’exécutent avant l’exportation. La passerelle et le dépôt revérifient indépendamment toutes les règles faisant autorité.",
+ "Lifecycle": "Cycle de traitement",
+ "Public review → maintainer approval → national validation → publication": "Examen public → approbation de l’équipe de maintenance → validation nationale → publication",
+ "Client checks are ready.": "Les vérifications du navigateur sont terminées.",
+ "Complete the readiness checklist.": "Terminez la liste de vérification.",
+ "Editor cells contain an unsupported geometry type.": "Les cellules de l’éditeur contiennent un type de géométrie non pris en charge.",
+ "Editor cell topology is invalid.": "La topologie des cellules de l’éditeur est invalide.",
+ "Check failed. Retrying…": "La vérification a échoué. Nouvelle tentative…",
+ "Retrying check…": "Nouvelle tentative de vérification…",
+ "Check expired. Retrying…": "La vérification a expiré. Nouvelle tentative…",
+ "Check timed out. Retrying…": "La vérification a pris trop de temps. Nouvelle tentative…",
+ "Complete the check.": "Effectuez la vérification.",
+ "Anti-spam protection is unavailable.": "La protection antipourriel n’est pas disponible.",
+ "Complete the check first.": "Effectuez d’abord la vérification.",
+ "Submitting…": "Soumission…",
+ "Submitted, but later edits were not included. Submit again to include them.": "La proposition a été soumise, mais les modifications plus récentes n’y figurent pas. Soumettez-la de nouveau pour les inclure.",
+ "Already submitted. Open the issue below.": "Déjà soumise. Ouvrez le billet ci-dessous.",
+ "Submitted for review.": "Soumise aux fins d’examen.",
+ "The proposal could not be submitted.": "La proposition n’a pas pu être soumise.",
+ "Reload and try again.": "Rechargez la page et réessayez.",
+ "Your edits are saved here. Complete the check and try again.": "Vos modifications sont conservées ici. Effectuez la vérification et réessayez.",
+ "Download the proposal to share it.": "Téléchargez la proposition pour la partager.",
+ "Preparing another check…": "Préparation d’une nouvelle vérification…",
+ "No schema": "Aucun schéma",
+ "No province selected": "Aucune province sélectionnée",
+ "Switch proposal type?": "Changer de type de proposition?",
+ "The current on-screen draft will be put away. Its local saved copy will remain under the current schema.": "Le brouillon affiché sera mis de côté. Sa copie locale restera enregistrée sous le schéma actuel.",
+ "Switch type": "Changer de type",
+ "Switch province or territory?": "Changer de province ou de territoire?",
+ "The current on-screen draft will be put away. Its local saved copy will remain under this province and schema.": "Le brouillon affiché sera mis de côté. Sa copie locale restera enregistrée sous cette province et ce schéma.",
+ "Switch area": "Changer de zone",
+ "Confirm the public-content statement before saving a draft.": "Confirmez que le contenu peut être public avant d’enregistrer un brouillon.",
+ "Draft saved on this device. Turnstile and anti-spam values are never stored.": "Brouillon enregistré sur cet appareil. Les valeurs Turnstile et antipourriel ne sont jamais conservées.",
+ "This browser blocked draft storage. Your answers remain in the form.": "Ce navigateur a bloqué l’enregistrement du brouillon. Vos réponses restent dans le formulaire.",
+ "Saved draft updated on this device.": "Brouillon mis à jour sur cet appareil.",
+ "Saved draft cleared.": "Brouillon enregistré supprimé.",
+ "An incompatible saved draft was removed.": "Un brouillon enregistré incompatible a été supprimé.",
+ "Draft restored. Confirm the public-content statement before updating or submitting it.": "Brouillon restauré. Confirmez que le contenu peut être public avant de le mettre à jour ou de le soumettre.",
+ "The saved draft could not be read and was removed.": "Le brouillon enregistré était illisible et a été supprimé.",
+ "You can now save a draft on this device.": "Vous pouvez maintenant enregistrer un brouillon sur cet appareil.",
+ "Update preview": "Mettre l’aperçu à jour",
+ "Review changes again": "Relire les changements",
+ "Review idea": "Relire l’idée",
+ "Earlier version submitted. New changes are not included.": "Une version antérieure a été soumise. Les nouvelles modifications n’y figurent pas.",
+ "Already submitted.": "Déjà soumise.",
+ "Open issue": "Ouvrir le billet",
+ "Maintainers will review the public issue and respond there.": "L’équipe de maintenance examinera le billet public et y répondra.",
+ "Answers changed. Review them again before submitting.": "Les réponses ont changé. Relisez-les avant de soumettre.",
+ "Check failed. Retry.": "La vérification a échoué. Réessayez.",
+ "Check complete.": "Vérification terminée.",
+ "Retrying the anti-spam check…": "Nouvelle tentative de vérification antipourriel…",
+ "Anonymous submission is unavailable. Copy the idea or use GitHub.": "La soumission anonyme n’est pas disponible. Copiez l’idée ou utilisez GitHub.",
+ "Fix the fields listed above, then review again.": "Corrigez les champs indiqués ci-dessus, puis relisez la proposition.",
+ "Too long to prefill. Copy the text, then use GitHub.": "Le texte est trop long pour être prérempli. Copiez-le, puis utilisez GitHub.",
+ "Preview ready. Nothing has been submitted. You can now submit.": "L’aperçu est prêt. Rien n’a été soumis. Vous pouvez maintenant soumettre l’idée.",
+ "Preview ready. Nothing has been submitted. Complete the verification next.": "L’aperçu est prêt. Rien n’a été soumis. Effectuez maintenant la vérification.",
+ "Check the form and try again.": "Vérifiez le formulaire et réessayez.",
+ "Copied.": "Texte copié.",
+ "Copy blocked. Copy the selected preview.": "La copie a été bloquée. Copiez l’aperçu sélectionné.",
+ "Review the latest answers.": "Relisez les réponses les plus récentes.",
+ "Submitted. Saved draft cleared from this device.": "Soumise. Le brouillon enregistré a été supprimé de cet appareil.",
+ "The reviewed version was submitted. Review and submit again to include your newer changes.": "La version relue a été soumise. Relisez et soumettez de nouveau pour inclure les modifications plus récentes.",
+ "The idea could not be submitted.": "L’idée n’a pas pu être soumise.",
+ "Your answers remain in the form. Complete the check and try again.": "Vos réponses restent dans le formulaire. Effectuez la vérification et réessayez.",
+ "Your answers remain in the form. Copy the text or use GitHub.": "Vos réponses restent dans le formulaire. Copiez le texte ou utilisez GitHub.",
+ "Submit idea": "Soumettre l’idée",
+ "Review the idea first.": "Relisez d’abord l’idée.",
+ "The submission request was not accepted.": "La demande de soumission n’a pas été acceptée.",
+ "The submission was not accepted.": "La soumission n’a pas été acceptée.",
+ "The proposal no longer matches the current region data.": "La proposition ne correspond plus aux données régionales actuelles.",
+ "The region map changed after this edit began. Reload the editor and try again.": "La carte des régions a changé depuis le début de cette modification. Rechargez l’éditeur et réessayez.",
+ "This submission is too large to send at once. Save it and contact a maintainer.": "Cette soumission est trop volumineuse pour être envoyée en une fois. Enregistrez-la et communiquez avec l’équipe de maintenance.",
+ "The anti-spam check was not accepted. Let it refresh and try again.": "La vérification antipourriel n’a pas été acceptée. Laissez-la se renouveler, puis réessayez.",
+ "Too many submissions were sent from this connection. Wait a few minutes and try again.": "Trop de soumissions ont été envoyées depuis cette connexion. Attendez quelques minutes, puis réessayez.",
+ "The submission service is temporarily unavailable. Try again shortly.": "Le service de soumission est temporairement indisponible. Réessayez dans quelques instants.",
+ "The submission service address is invalid.": "L’adresse du service de soumission est invalide.",
+ "The submission service must use HTTPS.": "Le service de soumission doit utiliser HTTPS.",
+ "A validated submission is required.": "Une soumission validée est requise.",
+ "This browser cannot verify the submitted data.": "Ce navigateur ne peut pas vérifier les données soumises.",
+ "The submission service returned invalid public configuration.": "Le service de soumission a renvoyé une configuration publique invalide.",
+ "This browser cannot contact the submission service.": "Ce navigateur ne peut pas joindre le service de soumission.",
+ "The submission service took too long to respond. Try again shortly.": "Le service de soumission a pris trop de temps à répondre. Réessayez dans quelques instants.",
+ "The submission service could not be reached. Try again shortly.": "Le service de soumission est inaccessible. Réessayez dans quelques instants.",
+ "Spam protection is temporarily unavailable. Try again shortly.": "La protection antipourriel est temporairement indisponible. Réessayez dans quelques instants.",
+ "The submission service returned an unreadable configuration.": "Le service de soumission a renvoyé une configuration illisible.",
+ "Complete the anti-spam check before submitting.": "Effectuez la vérification antipourriel avant de soumettre.",
+ "The submission could not be sent.": "La soumission n’a pas pu être envoyée.",
+ "Choose a valid contribution type.": "Choisissez un type de contribution valide.",
+ "Choose a valid MeshCore experience level.": "Choisissez un niveau d’expérience MeshCore valide.",
+ "Confirm that this submission can be public.": "Confirmez que cette soumission peut être publique.",
+ "Contribution type": "Type de contribution",
+ "MeshCore experience": "Expérience avec MeshCore",
+ "Short title": "Titre court",
+ "What is difficult now": "Ce qui est difficile en ce moment",
+ "What would help": "Ce qui aiderait",
+ "City or broad region": "Ville ou grande région",
+ "Additional context": "Autres précisions",
+ "Public contact": "Contact public",
+ "This field": "Ce champ",
+ "A draft from older map data remains stored but was not restored.": "Un brouillon fondé sur d’anciennes données cartographiques est toujours enregistré, mais n’a pas été restauré.",
+ "Discard the saved copy?": "Supprimer la copie enregistrée?",
+ "This removes the local saved copy for this schema and province. The current on-screen proposal stays open until it changes or you leave.": "Cela supprime la copie enregistrée sur cet appareil pour ce schéma et cette province. La proposition affichée reste ouverte jusqu’à sa modification ou votre départ.",
+ "Discard saved copy": "Supprimer la copie enregistrée",
+ "Saved copy discarded. Current on-screen work is unchanged.": "La copie enregistrée a été supprimée. Le travail affiché reste inchangé.",
+ "Choose at least one census cell.": "Choisissez au moins une cellule de recensement.",
+ "Choose or define the destination region first.": "Choisissez ou définissez d’abord la région de destination.",
+ "Proposal downloaded. It is not live until merged.": "La proposition a été téléchargée. Elle ne sera publiée qu’après sa fusion.",
+ "Spam protection cannot load in this browser.": "La protection antipourriel ne peut pas se charger dans ce navigateur.",
+ "Spam protection did not start. Reload the page and try again.": "La protection antipourriel n’a pas démarré. Rechargez la page et réessayez.",
+ "Spam protection could not load. Check your connection and try again.": "La protection antipourriel n’a pas pu se charger. Vérifiez votre connexion et réessayez.",
+ "Spam protection is unavailable.": "La protection antipourriel est indisponible.",
+ "The submission service took too long to respond. Your work is still here; try again.": "Le service de soumission a pris trop de temps à répondre. Votre travail est toujours ici; réessayez.",
+ "The submission service could not be reached. Your work is still here; try again.": "Le service de soumission est inaccessible. Votre travail est toujours ici; réessayez.",
+ "The submission service returned an unreadable response.": "Le service de soumission a renvoyé une réponse illisible.",
+ "The proposal format is not supported.": "Le format de la proposition n’est pas pris en charge.",
+ "The proposal contains unsupported fields.": "La proposition contient des champs non pris en charge.",
+ "The map changed after this edit began. Reload and try again.": "La carte a changé depuis le début de cette modification. Rechargez la page et réessayez.",
+ "Choose at least one cell before exporting.": "Choisissez au moins une cellule avant d’exporter.",
+ "Choose fewer cells before exporting.": "Choisissez moins de cellules avant d’exporter.",
+ "The proposal contains an invalid change.": "La proposition contient une modification invalide.",
+ "The proposal contains a duplicate cell.": "La proposition contient une cellule en double.",
+ "One or more cells no longer exist. Reload and try again.": "Une ou plusieurs cellules n’existent plus. Rechargez la page et réessayez.",
+ "A proposal may change cells in only one province or territory.": "Une proposition peut modifier les cellules d’une seule province ou d’un seul territoire.",
+ "Add a valid name, tag, and anchor for the new region.": "Ajoutez un nom, un identifiant et un point d’ancrage valides pour la nouvelle région.",
+ "The new region has the wrong catalogue parent.": "La région parente indiquée pour la nouvelle région ne correspond pas au catalogue.",
+ "That region tag is already in use.": "Cet identifiant régional est déjà utilisé.",
+ "That region already exists. Choose it from the list.": "Cette région existe déjà. Choisissez-la dans la liste.",
+ "Choose one changed cell without an existing region anchor.": "Choisissez une cellule modifiée qui ne sert pas déjà de point d’ancrage régional.",
+ "Every changed cell must move to the new region.": "Chaque cellule modifiée doit être déplacée vers la nouvelle région.",
+ "A target region must belong to the same province or territory.": "La région de destination doit appartenir à la même province ou au même territoire.",
+ "A region anchor cell cannot be moved away from its region.": "Une cellule servant de point d’ancrage régional ne peut pas être déplacée hors de sa région.",
+ "The proposal contains a change that has no effect.": "La proposition contient une modification sans effet.",
+ "The submitted-by value is invalid.": "Le nom de la personne qui soumet la proposition est invalide.",
+ "Add a short reason for this boundary change.": "Ajoutez une courte raison pour cette modification de limite.",
+ "No": "Non",
+ "Target region": "Région de destination",
+ "Unnamed municipality": "Municipalité sans nom",
+ "The submission was received, but the review link was invalid.": "La soumission a été reçue, mais le lien d’examen était invalide."
+ };
+
+ var patterns = [
+ [/^(.*) \(opens in a new tab\)$/, function (match) { return match[1] + " (s’ouvre dans un nouvel onglet)"; }],
+ [/^Loading (.+)…$/, function (match) { return "Chargement de " + translate(match[1]) + "…"; }],
+ [/^(\d[\d\s,.]*) census cells loaded\.$/, function (match) { return match[1] + " cellules de recensement chargées."; }],
+ [/^(\d+) change$/, function (match) { return match[1] + " modification"; }],
+ [/^(\d+) changes$/, function (match) { return match[1] + " modifications"; }],
+ [/^(\d+) changed cell$/, function (match) { return match[1] + " cellule modifiée"; }],
+ [/^(\d+) changed cells$/, function (match) { return match[1] + " cellules modifiées"; }],
+ [/^Showing 200 of (.+) changes\. The downloaded proposal contains all changes\.$/, function (match) { return "Affichage de 200 modifications sur " + match[1] + ". La proposition téléchargée contient toutes les modifications."; }],
+ [/^Remove proposed change for (.+)$/, function (match) { return "Retirer la modification proposée pour " + match[1]; }],
+ [/^Confirmed anchor: (.+)$/, function (match) { return "Point d’ancrage confirmé : " + match[1]; }],
+ [/^Derived parent: (.+)$/, function (match) { return "Région parente déterminée : " + match[1]; }],
+ [/^GitHub issue #(\d+)$/, function (match) { return "Billet GitHub no " + match[1]; }],
+ [/^Open issue #(\d+)$/, function (match) { return "Ouvrir le billet no " + match[1]; }],
+ [/^(.+) — (\d+) cell$/, function (match) { return match[1] + " — " + match[2] + " cellule"; }],
+ [/^(.+) — (\d+) cells$/, function (match) { return match[1] + " — " + match[2] + " cellules"; }],
+ [/^(\d[\d\s,.]*) census cells across (\d[\d\s,.]*) municipalities\/CSDs$/, function (match) { return match[1] + " cellules de recensement dans " + match[2] + " municipalités ou SDR"; }],
+ [/^Draft restored from (.+)\. Contributor identity was not stored\.$/, function (match) { return "Brouillon restauré depuis le " + match[1] + ". L’identité de la personne n’a pas été enregistrée."; }],
+ [/^(.+) is required\.$/, function (match) { return "Le champ « " + translate(match[1]) + " » est obligatoire."; }],
+ [/^(.+) is too long\.$/, function (match) { return "Le champ « " + translate(match[1]) + " » est trop long."; }],
+ [/^(.+) contains invalid text\.$/, function (match) { return "Le champ « " + translate(match[1]) + " » contient du texte invalide."; }],
+ [/^Could not load editor data \((.+)\)\.$/, function (match) { return "Impossible de charger les données de l’éditeur (" + match[1] + ")."; }],
+ [/^That selection contains the fixed anchor for (.+)\.$/, function (match) { return "Cette sélection contient le point d’ancrage fixe de " + match[1] + "."; }],
+ [/^The fixed anchor for (.+) cannot be moved\.$/, function (match) { return "Le point d’ancrage fixe de " + match[1] + " ne peut pas être déplacé."; }],
+ [/^(.+)\. Edit each province separately; repeater setup keeps the paths together\.$/, function (match) { return match[1] + ". Modifiez chaque province séparément; la configuration du répéteur conserve les chemins ensemble."; }],
+ [/^(.+) \(confirmed\)$/, function (match) { return match[1] + " (confirmé)"; }],
+ [/^\. (.+)$/, function (match) { return ". " + translate(match[1]); }],
+ [/^(.+\.) (Reload and try again\.|Your edits are saved here\. Complete the check and try again\.|Download the proposal to share it\.|Your answers remain in the form\. Complete the check and try again\.|Your answers remain in the form\. Copy the text or use GitHub\.)$/, function (match) { return translate(match[1]) + " " + translate(match[2]); }],
+ [/^(.+) \(fixed\)$/, function (match) { return match[1] + " (fixe)"; }]
+ ];
+
+ function translate(value) {
+ if (!isFrench || typeof value !== "string") return value;
+ var leading = value.match(/^\s*/)[0];
+ var trailing = value.match(/\s*$/)[0];
+ var normalized = value.trim().replace(/\s+/g, " ");
+ if (!normalized) return value;
+ var translated = dictionary[normalized];
+ if (!translated) {
+ for (var index = 0; index < patterns.length; index += 1) {
+ var match = normalized.match(patterns[index][0]);
+ if (match) {
+ translated = patterns[index][1](match);
+ break;
+ }
+ }
+ }
+ return translated ? leading + translated + trailing : value;
+ }
+
+ function shouldSkip(node) {
+ var element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement;
+ return Boolean(element && element.closest("script, style, code, pre, kbd, samp, textarea, [data-i18n-ignore]"));
+ }
+
+ function localizeTextNode(node) {
+ if (!node || node.nodeType !== Node.TEXT_NODE || shouldSkip(node)) return;
+ var next = translate(node.nodeValue);
+ if (next !== node.nodeValue) node.nodeValue = next;
+ }
+
+ function localizeElement(element) {
+ if (!element || element.nodeType !== Node.ELEMENT_NODE || shouldSkip(element)) return;
+ ["aria-label", "placeholder", "title"].forEach(function (name) {
+ if (!element.hasAttribute(name)) return;
+ var current = element.getAttribute(name);
+ var next = translate(current);
+ if (next !== current) element.setAttribute(name, next);
+ });
+ }
+
+ function localize(root) {
+ if (!isFrench || !root) return;
+ if (root.nodeType === Node.TEXT_NODE) {
+ localizeTextNode(root);
+ return;
+ }
+ if (root.nodeType !== Node.ELEMENT_NODE && root.nodeType !== Node.DOCUMENT_NODE) return;
+ if (root.nodeType === Node.ELEMENT_NODE) localizeElement(root);
+ var elementWalker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
+ while (elementWalker.nextNode()) localizeElement(elementWalker.currentNode);
+ var textWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
+ while (textWalker.nextNode()) localizeTextNode(textWalker.currentNode);
+ }
+
+ function configureEditorLanguageLinks() {
+ var links = document.querySelectorAll("[data-editor-language]");
+ if (!links.length) return;
+ var pathname = window.location.pathname;
+ var englishPath = pathname.replace(/\/fr\/config\/editor(?:\/index\.html|\/)?$/, "/config/editor/");
+ if (!/\/config\/editor\/$/.test(englishPath)) {
+ englishPath = pathname.replace(/\/config\/editor(?:\/index\.html|\/)?$/, "/config/editor/");
+ }
+ var frenchPath = englishPath.replace(/\/config\/editor\/$/, "/fr/config/editor/");
+ links.forEach(function (link) {
+ var locale = link.getAttribute("data-editor-language");
+ link.href = locale === "fr" ? frenchPath : englishPath;
+ link.lang = locale;
+ link.hreflang = locale;
+ if ((locale === "fr") === isFrench) link.setAttribute("aria-current", "page");
+ else link.removeAttribute("aria-current");
+ });
+ }
+
+ window.MeshCoreCanadaI18n = Object.freeze({
+ locale: isFrench ? "fr" : "en",
+ isFrench: isFrench,
+ t: translate,
+ localize: localize
+ });
+
+ function initialise() {
+ if (isFrench) {
+ document.documentElement.lang = "fr";
+ document.title = translate(document.title);
+ localize(document.body);
+ new MutationObserver(function (records) {
+ records.forEach(function (record) {
+ if (record.type === "characterData") localizeTextNode(record.target);
+ if (record.type === "attributes") localizeElement(record.target);
+ record.addedNodes.forEach(localize);
+ });
+ }).observe(document.body, {
+ subtree: true,
+ childList: true,
+ characterData: true,
+ attributes: true,
+ attributeFilter: ["aria-label", "placeholder", "title"]
+ });
+ }
+ configureEditorLanguageLinks();
+ }
+
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initialise, { once: true });
+ } else {
+ initialise();
+ }
+})();
diff --git a/docs/assets/regions/modules/configurator-support.js b/docs/assets/regions/modules/configurator-support.js
new file mode 100644
index 0000000..1d0977c
--- /dev/null
+++ b/docs/assets/regions/modules/configurator-support.js
@@ -0,0 +1,63 @@
+(function (root, factory) {
+ "use strict";
+ root.MeshCoreRegionConfiguratorSupport = factory();
+}(typeof globalThis !== "undefined" ? globalThis : this, function () {
+ "use strict";
+
+ function finiteCoordinate(value, minimum, maximum) {
+ var number = Number(String(value == null ? "" : value).trim());
+ return Number.isFinite(number) && number >= minimum && number <= maximum ? number : null;
+ }
+
+ function parseCoordinates(latitude, longitude) {
+ var lat = finiteCoordinate(latitude, -90, 90);
+ var lon = finiteCoordinate(longitude, -180, 180);
+ return lat === null || lon === null ? null : { lat: lat, lon: lon };
+ }
+
+ function normalizeText(value) {
+ return String(value == null ? "" : value).replace(/\s+/g, " ").trim();
+ }
+
+ function commissioningRecord(input) {
+ var paths = Array.isArray(input.paths) ? input.paths.map(normalizeText).filter(Boolean) : [];
+ var commands = Array.isArray(input.commands) ? input.commands.map(normalizeText).filter(Boolean) : [];
+ var lines = [
+ "MeshCore Canada repeater commissioning summary",
+ "Generated: " + normalizeText(input.generatedAt),
+ "Location label: " + (normalizeText(input.locationLabel) || "Not recorded"),
+ "Home region: " + normalizeText(input.homeRegion),
+ "Firmware: " + normalizeText(input.firmware),
+ "Region budget: " + normalizeText(input.budget),
+ "",
+ "Forwarding paths:"
+ ];
+ paths.forEach(function (path) { lines.push("- " + path); });
+ lines.push("", "Commands:");
+ commands.forEach(function (command) { lines.push(command); });
+ lines.push(
+ "",
+ "Verification:",
+ "1. Run region before saving and compare every path above.",
+ "2. Run region save only after the paths and flood permissions are correct.",
+ "3. Run region again after saving.",
+ "",
+ "This summary omits exact coordinates, passwords, private keys, and device identifiers."
+ );
+ return lines.join("\n") + "\n";
+ }
+
+ function safeFileStem(value) {
+ var stem = normalizeText(value).toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "")
+ .slice(0, 48);
+ return stem || "repeater";
+ }
+
+ return {
+ commissioningRecord: commissioningRecord,
+ parseCoordinates: parseCoordinates,
+ safeFileStem: safeFileStem
+ };
+}));
diff --git a/docs/assets/regions/regions.css b/docs/assets/regions/regions.css
index 5197615..cb09298 100644
--- a/docs/assets/regions/regions.css
+++ b/docs/assets/regions/regions.css
@@ -3,15 +3,18 @@ body:has([data-mcc-regions="map"]) [data-mcc-regions="map"] {
}
body:has([data-mcc-regions]) {
- background: var(--md-default-bg-color, hsl(232, 15%, 14%));
+ background: var(--md-default-bg-color, #ffffff);
}
+/* Region tools use one deliberate dark workbench in either site palette. This
+ keeps dense maps, command output, and status colours readable after a theme
+ switch while the header, navigation, and surrounding site still follow it. */
body:has([data-mcc-regions]) .md-main,
body:has([data-mcc-regions]) .md-main__inner,
body:has([data-mcc-regions]) .md-content,
body:has([data-mcc-regions]) .md-content__inner {
- color: var(--md-typeset-color, rgba(255, 255, 255, 0.82));
- background: var(--md-default-bg-color, hsl(232, 15%, 14%));
+ color: #dce9e7;
+ background: #0c151c;
}
body:has([data-mcc-regions="map"]) .md-main__inner {
@@ -24,10 +27,22 @@ body:has([data-mcc-regions]) .md-typeset h3 {
color: #ffffff;
}
-body:has([data-mcc-regions]) .md-typeset a {
+body:has([data-mcc-regions]) .md-main .md-typeset a {
color: var(--md-typeset-a-color, #5488e8);
}
+.mcc-visually-hidden {
+ position: absolute !important;
+ width: 1px !important;
+ height: 1px !important;
+ padding: 0 !important;
+ margin: -1px !important;
+ overflow: hidden !important;
+ clip: rect(0, 0, 0, 0) !important;
+ white-space: nowrap !important;
+ border: 0 !important;
+}
+
.mcc-region-hero {
margin: 1.2rem 0 1.5rem;
padding: 1rem 1.1rem;
@@ -77,13 +92,13 @@ body:has([data-mcc-regions]) .md-typeset a {
--mcc-blue: #303fa1;
--mcc-gold: #f4bf5a;
--mcc-red: #ff7373;
- --mcc-border: var(--md-default-fg-color--lightest, rgba(255, 255, 255, 0.12));
- --mcc-soft: var(--md-accent-fg-color--transparent, rgba(66, 135, 255, 0.12));
- --mcc-panel: var(--md-default-bg-color, hsl(232, 15%, 14%));
- --mcc-surface: color-mix(in srgb, var(--md-default-bg-color, hsl(232, 15%, 14%)), #ffffff 5%);
- --mcc-surface-2: color-mix(in srgb, var(--md-default-bg-color, hsl(232, 15%, 14%)), #ffffff 8%);
- --mcc-text: var(--md-typeset-color, rgba(255, 255, 255, 0.82));
- --mcc-muted: var(--md-default-fg-color--light, rgba(255, 255, 255, 0.56));
+ --mcc-border: rgba(159, 216, 189, 0.22);
+ --mcc-soft: rgba(66, 135, 255, 0.15);
+ --mcc-panel: #0c151c;
+ --mcc-surface: #101d26;
+ --mcc-surface-2: #172630;
+ --mcc-text: #dce9e7;
+ --mcc-muted: #a7b9bb;
margin: 1rem 0 2rem;
color: var(--mcc-text);
}
@@ -321,7 +336,8 @@ body:has([data-mcc-regions]) .md-typeset a {
}
.mcc-choice small {
- color: var(--mcc-muted);
+ color: var(--mcc-text);
+ opacity: 1;
}
.mcc-chip-list {
@@ -837,6 +853,59 @@ body:has([data-mcc-regions]) .md-typeset a {
background: var(--mcc-panel);
}
+.mcc-map-experience {
+ display: grid;
+ gap: 1rem;
+}
+
+.mcc-map-product-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: end;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.mcc-map-product-header h2,
+.mcc-map-product-header p {
+ margin-block: 0;
+}
+
+.mcc-map-product-header h2 + p {
+ margin-top: 0.5rem;
+}
+
+.mcc-map-mode-switch {
+ display: inline-flex;
+ width: fit-content;
+ gap: 0.25rem;
+ padding: 0.25rem;
+ border: 1px solid var(--mcc-border);
+ border-radius: 0.5rem;
+ background: var(--mcc-panel);
+}
+
+.mcc-map-mode-switch button {
+ padding: 0.45rem 0.75rem;
+ border: 0;
+ border-radius: 0.32rem;
+ color: var(--mcc-text);
+ background: transparent;
+ font: inherit;
+ font-weight: 750;
+ cursor: pointer;
+}
+
+.mcc-map-mode-switch button:hover,
+.mcc-map-mode-switch button:focus-visible {
+ background: var(--mcc-soft);
+}
+
+.mcc-map-mode-switch button[aria-pressed="true"] {
+ color: #ffffff;
+ background: var(--mcc-green);
+}
+
.mcc-map-panel {
display: grid;
align-content: start;
@@ -846,6 +915,61 @@ body:has([data-mcc-regions]) .md-typeset a {
overflow: auto;
}
+.mcc-map-stage {
+ position: relative;
+ min-width: 0;
+ min-height: 30rem;
+}
+
+.mcc-skip-map {
+ position: absolute;
+ top: 0.5rem;
+ left: 0.5rem;
+ z-index: 1001;
+ padding: 0.45rem 0.65rem;
+ border-radius: 0.3rem;
+ color: #ffffff;
+ background: var(--mcc-green-dark);
+ transform: translateY(calc(-100% - 1rem));
+}
+
+.mcc-skip-map:focus {
+ transform: none;
+}
+
+.mcc-map-loading {
+ display: grid;
+ position: absolute;
+ inset: 0;
+ z-index: 1000;
+ height: 100%;
+ place-items: center;
+ padding: 2rem;
+ color: var(--mcc-text);
+ background:
+ radial-gradient(circle at center, color-mix(in srgb, var(--mcc-green), transparent 86%), transparent 55%),
+ var(--mcc-panel);
+ text-align: center;
+}
+
+.mcc-map-loading > div {
+ display: grid;
+ max-width: 28rem;
+ justify-items: center;
+ gap: 0.65rem;
+}
+
+.mcc-map-loading svg {
+ width: 2.5rem;
+ height: 2.5rem;
+ color: var(--mcc-green);
+}
+
+.mcc-map-loading h3,
+.mcc-map-loading p {
+ margin: 0;
+}
+
.mcc-region-breadcrumbs {
display: flex;
flex-wrap: wrap;
@@ -910,8 +1034,10 @@ body:has([data-mcc-regions]) .md-typeset a {
}
.mcc-region-child small {
- color: var(--mcc-muted);
+ color: var(--mcc-text);
font-size: 0.72rem;
+ font-weight: 600;
+ opacity: 1;
}
.mcc-panel-header {
@@ -954,6 +1080,7 @@ body:has([data-mcc-regions]) .md-typeset a {
.mcc-map-area {
position: relative;
+ height: 100%;
min-height: 0;
background: var(--mcc-panel);
}
@@ -1590,7 +1717,7 @@ body:has([data-mcc-regions]) .md-typeset a {
}
.mcc-region-table td {
- color: var(--mcc-text);
+ color: #eaf3f1;
background: #101d26;
font-size: 0.76rem;
}
@@ -1622,6 +1749,14 @@ body:has([data-mcc-regions]) .md-typeset a {
min-height: 32rem;
}
+ .mcc-map-loading {
+ height: 32rem;
+ }
+
+ .mcc-map-stage {
+ min-height: 32rem;
+ }
+
.mcc-sticky-result,
.mcc-table-detail {
position: static;
diff --git a/docs/assets/regions/regions.js b/docs/assets/regions/regions.js
index 2b3cd9e..62b57d2 100644
--- a/docs/assets/regions/regions.js
+++ b/docs/assets/regions/regions.js
@@ -12,11 +12,497 @@
var lucidePromise = null;
var activeMaps = [];
var LUCIDE_SRC = new URL("vendor/lucide.js", assetBase).href;
+ var configuratorSupport = window.MeshCoreRegionConfiguratorSupport || {};
+ var REQUEST_TIMEOUT_MS = 12000;
+ var frenchRuntime = /^fr(?:-|$)/i.test(
+ document.documentElement ? document.documentElement.lang || "" : ""
+ );
+
+ var FRENCH_RUNTIME_TEXT = {
+ "Unable to load MeshCore Canada region data": "Impossible de charger les données régionales de MeshCore Canada",
+ "Unable to load the Canadian map layer": "Impossible de charger la couche cartographique canadienne",
+ "Unable to load the Canadian location layer": "Impossible de charger la couche canadienne de localisation",
+ "The Canadian region catalog contains an invalid or duplicate local region": "Le catalogue des régions canadiennes contient une région locale invalide ou en double",
+ "The Canadian region catalog contains an invalid shared repeater area": "Le catalogue des régions canadiennes contient une zone partagée de répéteurs invalide",
+ "A shared repeater area must cross a province or territory": "Une zone partagée de répéteurs doit traverser une province ou un territoire",
+ "The region catalog contains an invalid neighbouring network path": "Le catalogue des régions contient un chemin de réseau voisin invalide",
+ "A neighbouring network tag collides with the Canadian hierarchy": "Un identifiant de réseau voisin entre en conflit avec la hiérarchie canadienne",
+ "A neighbouring network tag has no label": "Un identifiant de réseau voisin n’a pas de libellé",
+ "A neighbouring network tag has conflicting parents": "Un identifiant de réseau voisin a des parents incompatibles",
+ "A neighbouring network tag has conflicting labels": "Un identifiant de réseau voisin a des libellés incompatibles",
+ "Canadian map layer is invalid": "La couche cartographique canadienne est invalide",
+ "Canadian map layer contains an invalid or duplicate region": "La couche cartographique canadienne contient une région invalide ou en double",
+ "Canadian map layer does not match the region catalog": "La couche cartographique canadienne ne correspond pas au catalogue des régions",
+ "Canadian location layer is invalid": "La couche canadienne de localisation est invalide",
+ "Canadian location layer contains an invalid or duplicate region": "La couche canadienne de localisation contient une région invalide ou en double",
+ "Canadian location layer does not match the region catalog": "La couche canadienne de localisation ne correspond pas au catalogue des régions",
+ "Geocoding service error": "Erreur du service de géocodage",
+ "No matching Canadian postal code found": "Aucun code postal canadien correspondant n’a été trouvé",
+ "Online place lookup is unavailable. Enter coordinates or browse the region list.": "La recherche de lieux en ligne est indisponible. Entrez des coordonnées ou parcourez la liste des régions.",
+ "Copy": "Copier",
+ "Copied": "Copié",
+ "Copy failed": "Échec de la copie",
+ "Copied to clipboard.": "Copié dans le presse-papiers.",
+ "Copy failed. Select and copy the command manually.": "La copie a échoué. Sélectionnez et copiez la commande manuellement.",
+ "Needs review": "À vérifier",
+ "Draft": "Brouillon",
+ "Reviewed": "Vérifié",
+ "Active": "Actif",
+ "Deprecated": "Obsolète",
+ "Unreviewed": "Non vérifié",
+ "No seed": "Aucun point de départ",
+ "Country": "Pays",
+ "Province / Territory": "Province ou territoire",
+ "Local Region": "Région locale",
+ "Region": "Région",
+ "Area Group": "Groupe de zones",
+ "Shared repeater area": "Zone partagée de répéteurs",
+ "community path": "chemin communautaire",
+ "confirm locally": "à confirmer localement",
+ "Add only the paths this repeater should forward. Different repeaters can carry different paths to spread traffic.": "Ajoutez seulement les chemins que ce répéteur doit relayer. Différents répéteurs peuvent transporter différents chemins pour répartir le trafic.",
+ "Canadian regions": "Régions canadiennes",
+ "Provinces and territories may be mixed.": "Vous pouvez combiner des provinces et des territoires.",
+ "required": "obligatoire",
+ "Add any Canadian region": "Ajouter une région canadienne",
+ "Choose a region": "Choisissez une région",
+ "Neighbouring network paths": "Chemins des réseaux voisins",
+ "Add one only when this repeater should forward traffic for that area. Nothing outside Canada is added to the boundary map.": "Ajoutez-en un seulement si ce répéteur doit relayer le trafic de cette zone. Rien à l’extérieur du Canada n’est ajouté à la carte des limites.",
+ "No region yet": "Aucune région pour l’instant",
+ "Choose a location first.": "Choisissez d’abord un emplacement.",
+ "Too many regions selected": "Trop de régions sélectionnées",
+ "Cross-province repeater setup": "Configuration de répéteur interprovinciale",
+ "Connect to the repeater CLI": "Se connecter à l’interface en ligne de commande du répéteur",
+ "Use USB at the repeater or remote management over LoRa.": "Utilisez l’USB sur place ou la gestion à distance par LoRa.",
+ "USB serial": "Connexion série USB",
+ "At the repeater": "Sur place",
+ "Connect the repeater to a computer with a data-capable USB cable.": "Branchez le répéteur à un ordinateur avec un câble USB qui transmet les données.",
+ "In desktop Chrome or Edge, open the": "Dans Chrome ou Edge sur un ordinateur, ouvrez",
+ ". For Gessaman's MQTT Observer firmware, use the": ". Pour le micrologiciel MQTT Observer de Gessaman, utilisez le",
+ "For Gessaman's MQTT Observer firmware, use the": "Pour le micrologiciel MQTT Observer de Gessaman, utilisez le",
+ "instead.": "plutôt.",
+ "Choose": "Choisissez",
+ ", then approve the repeater's serial or COM port when the browser asks.": ", puis autorisez le port série ou COM du répéteur lorsque le navigateur le demande.",
+ "then approve the repeater's serial or COM port when the browser asks.": "puis autorisez le port série ou COM du répéteur lorsque le navigateur le demande.",
+ "Remote over LoRa": "À distance par LoRa",
+ "Through a companion radio": "Par une radio compagnon",
+ "On a phone or computer, connect the": "Sur un téléphone ou un ordinateur, connectez l’",
+ "official MeshCore app": "application MeshCore officielle",
+ "to your companion radio.": "à votre radio compagnon.",
+ "Open": "Ouvrez",
+ ", select the repeater, then choose": ", sélectionnez le répéteur, puis choisissez",
+ "select the repeater, then choose": "sélectionnez le répéteur, puis choisissez",
+ "from its menu.": "dans son menu.",
+ "Enter the repeater admin password, tap": "Entrez le mot de passe administrateur du répéteur, touchez",
+ ", then open": ", puis ouvrez",
+ "then open": "puis ouvrez",
+ "If the repeater is missing, open Tools → Discover Nearby Nodes. If a wait timer appears, let it finish before logging in.": "Si le répéteur est absent, ouvrez Tools → Discover Nearby Nodes. Si une minuterie apparaît, attendez qu’elle se termine avant de vous connecter.",
+ "Confirm the command line": "Confirmer l’interface en ligne de commande",
+ "Run": "Exécutez",
+ "and check the version.": "et vérifiez la version.",
+ "Apply the settings": "Appliquer les paramètres",
+ "Run each line in order. Wait for a reply.": "Exécutez chaque ligne dans l’ordre. Attendez une réponse.",
+ "Stop on": "Arrêtez-vous en cas de",
+ ". Existing regions are not cleared.": ". Les régions existantes ne sont pas effacées.",
+ "Existing regions are not cleared.": "Les régions existantes ne sont pas effacées.",
+ "Check and save": "Vérifier et enregistrer",
+ "and confirm each path:": "et confirmez chaque chemin :",
+ "Save:": "Enregistrez :",
+ "Restart the device, reconnect, then run these final checks:": "Redémarrez l’appareil, reconnectez-vous, puis effectuez ces dernières vérifications :",
+ "Run this once more to confirm the saved region:": "Exécutez cette commande une dernière fois pour confirmer la région enregistrée :",
+ "MeshCore command help": "Aide sur les commandes MeshCore",
+ "Commands": "Commandes",
+ "Canada-wide region boundary": "Limite régionale pancanadienne",
+ "Boundary unavailable": "Limite indisponible",
+ "Region tags": "Identifiants de région",
+ "Firmware": "Micrologiciel",
+ "Region budget": "Budget régional",
+ "Advanced details": "Détails avancés",
+ "Repeater regions:": "Régions du répéteur :",
+ "Your region:": "Votre région :",
+ "Copy commands": "Copier les commandes",
+ "Commissioning record": "Fiche de mise en service",
+ "Download or print a summary without exact coordinates, credentials, or device identifiers.": "Téléchargez ou imprimez un résumé sans coordonnées exactes, identifiants de connexion ni identifiants d’appareil.",
+ "Download": "Télécharger",
+ "Print": "Imprimer",
+ "Commissioning summary downloaded. Exact coordinates and credentials were omitted.": "La fiche de mise en service a été téléchargée. Les coordonnées exactes et les identifiants de connexion ont été omis.",
+ "The print window was blocked. Download the summary instead.": "La fenêtre d’impression a été bloquée. Téléchargez plutôt le résumé.",
+ "MeshCore Canada commissioning summary": "Fiche de mise en service de MeshCore Canada",
+ "MeshCore Canada repeater commissioning summary": "Fiche de mise en service du répéteur MeshCore Canada",
+ "Generated": "Générée",
+ "Location label": "Libellé de l’emplacement",
+ "Not recorded": "Non indiqué",
+ "Forwarding paths:": "Chemins relayés :",
+ "Commands:": "Commandes :",
+ "Verification:": "Vérification :",
+ "1. Run region before saving and compare every path above.": "1. Exécutez region avant d’enregistrer et comparez chaque chemin ci-dessus.",
+ "2. Run region save only after the paths and flood permissions are correct.": "2. Exécutez region save seulement lorsque les chemins et les autorisations de diffusion sont corrects.",
+ "3. Run region again after saving.": "3. Exécutez region de nouveau après l’enregistrement.",
+ "This summary omits exact coordinates, passwords, private keys, and device identifiers.": "Cette fiche omet les coordonnées exactes, les mots de passe, les clés privées et les identifiants d’appareil.",
+ "Commissioning summary opened for printing. Exact coordinates and credentials were omitted.": "La fiche de mise en service est ouverte pour l’impression. Les coordonnées exactes et les identifiants de connexion ont été omis.",
+ "Review": "Vérifier",
+ "Source": "Source",
+ "Seed": "Point de départ",
+ "No seed point": "Aucun point de départ",
+ "Repeater area": "Zone du répéteur",
+ "Source note": "Note sur la source",
+ "BC coastal seed follows the PNW reference data": "Le point de départ côtier de la C.-B. suit les données de référence du PNW",
+ "Strategy draft v1.1.1 region": "Région de l’ébauche de stratégie v1.1.1",
+ "Copy tag": "Copier l’identifiant",
+ "Open source": "Ouvrir la source",
+ "Setup progress": "Progression de la configuration",
+ "Step 1: Device": "Étape 1 : appareil",
+ "Step 2: Location": "Étape 2 : emplacement",
+ "Step 3: Coverage": "Étape 3 : couverture",
+ "Step 4: Apply": "Étape 4 : appliquer",
+ "Device": "Appareil",
+ "Location": "Emplacement",
+ "Coverage": "Couverture",
+ "Apply": "Appliquer",
+ "Step 1 of 4": "Étape 1 sur 4",
+ "What are you configuring?": "Quel appareil configurez-vous?",
+ "We will recommend forwarding paths, then show how to apply them.": "Nous recommanderons les chemins à relayer, puis nous vous montrerons comment les appliquer.",
+ "Device and experience": "Appareil et niveau d’expérience",
+ "Repeater": "Répéteur",
+ "Recommended for most operators": "Recommandé pour la plupart des exploitants",
+ "Room server with repeating": "Serveur de salon avec relais",
+ "Uses the same region paths": "Utilise les mêmes chemins régionaux",
+ "Advanced operator": "Exploitant expérimenté",
+ "Review wide and cross-border paths": "Examine les chemins étendus et transfrontaliers",
+ "Next": "Suivant",
+ "Step 2 of 4": "Étape 2 sur 4",
+ "Step 4 of 4": "Étape 4 sur 4",
+ "Where is the node?": "Où se trouve le nœud?",
+ "Search a place": "Rechercher un lieu",
+ "City, airport code, postal code, or region name": "Ville, code d’aéroport, code postal ou nom de région",
+ "Find": "Rechercher",
+ "Enter coordinates": "Entrer des coordonnées",
+ "Latitude": "Latitude",
+ "Longitude": "Longitude",
+ "Use coordinates": "Utiliser les coordonnées",
+ "Coordinates are checked against the Canadian region data in this page.": "Les coordonnées sont comparées aux données régionales canadiennes de cette page.",
+ "Use this device": "Utiliser cet appareil",
+ "Use my location": "Utiliser ma position",
+ "Your browser asks first. MeshCore Canada does not receive or store your coordinates.": "Votre navigateur demande d’abord votre permission. MeshCore Canada ne reçoit ni ne conserve vos coordonnées.",
+ "Browse regions without search or a map": "Parcourir les régions sans recherche ni carte",
+ "Back": "Retour",
+ "Explore regions": "Explorer les régions",
+ "Step 3 of 4": "Étape 3 sur 4",
+ "What should this node serve?": "Quelles zones ce nœud doit-il desservir?",
+ "Repeater forwarding coverage": "Couverture de relais du répéteur",
+ "Recommended local area": "Zone locale recommandée",
+ "Use the home region and any registered shared area": "Utilise la région d’attache et toute zone partagée enregistrée",
+ "Add nearby or cross-border paths": "Ajouter des chemins voisins ou transfrontaliers",
+ "For bridge, wide-coverage, mountain, or water-path repeaters": "Pour les répéteurs qui font un pont, couvrent une grande zone, une montagne ou un parcours au-dessus de l’eau",
+ "Radio and firmware options": "Options radio et de micrologiciel",
+ "Recommended radio settings": "Paramètres radio recommandés",
+ "Include recommended radio defaults": "Inclure les paramètres radio recommandés",
+ "For a new setup": "Pour une nouvelle installation",
+ "Keep current radio settings": "Conserver les paramètres radio actuels",
+ "For an existing coordinated network": "Pour un réseau coordonné existant",
+ "Firmware version": "Version du micrologiciel",
+ "Review and apply": "Vérifier et appliquer",
+ "Choose setup instructions": "Choisir les instructions de configuration",
+ "Guide me": "Me guider",
+ "Connect, apply, verify, then save": "Se connecter, appliquer, vérifier, puis enregistrer",
+ "Technical operator flow": "Parcours pour exploitant technique",
+ "Selection": "Sélection",
+ "Node": "Nœud",
+ "Place": "Lieu",
+ "Home region": "Région d’attache",
+ "Budget": "Budget",
+ "Forwarding paths": "Chemins relayés",
+ "Neighbouring paths are included only on this repeater. Confirm provisional paths with the neighbouring operators.": "Les chemins voisins sont inclus uniquement sur ce répéteur. Confirmez les chemins provisoires auprès des exploitants voisins.",
+ "Choose a location or browse to a region first.": "Choisissez d’abord un emplacement ou parcourez les régions.",
+ "Your home region": "Votre région d’attache",
+ "Select this region to use it as the home region.": "Sélectionnez cette région comme région d’attache.",
+ "Region found.": "Région trouvée.",
+ "Checking the Canadian region data…": "Vérification des données régionales canadiennes…",
+ "Unable to load the Canadian location data.": "Impossible de charger les données canadiennes de localisation.",
+ "Enter a city, airport code, postal code, or region name.": "Entrez une ville, un code d’aéroport, un code postal ou un nom de région.",
+ "Finding": "Recherche en cours",
+ "Location lookup failed": "La recherche du lieu a échoué",
+ "Enter a latitude from -90 to 90 and a longitude from -180 to 180.": "Entrez une latitude de -90 à 90 et une longitude de -180 à 180.",
+ "This browser does not provide location access. Enter coordinates or browse regions.": "Ce navigateur ne donne pas accès à la position. Entrez des coordonnées ou parcourez les régions.",
+ "Waiting for browser location permission…": "En attente de l’autorisation de localisation du navigateur…",
+ "Current browser location": "Position actuelle du navigateur",
+ "Location was not available. Enter coordinates or browse regions.": "La position n’était pas disponible. Entrez des coordonnées ou parcourez les régions.",
+ "Leaflet failed to load": "Leaflet n’a pas pu se charger",
+ "MeshCore Canada regions": "Régions de MeshCore Canada",
+ "Explore or audit the region release": "Explorer ou vérifier la publication des régions",
+ "Find a home region on the interactive map, or inspect the published release evidence.": "Trouvez une région d’attache sur la carte interactive ou examinez les preuves de la publication.",
+ "Set up a repeater": "Configurer un répéteur",
+ "Region map mode": "Mode de la carte des régions",
+ "Audit the release": "Vérifier la publication",
+ "Region search and details": "Recherche et détails des régions",
+ "Find a place": "Trouver un lieu",
+ "Enter coordinates instead": "Entrer plutôt des coordonnées",
+ "Browse the hierarchy": "Parcourir la hiérarchie",
+ "Selected region": "Région sélectionnée",
+ "Skip the interactive map": "Passer la carte interactive",
+ "Loading interactive map…": "Chargement de la carte interactive…",
+ "Loading Canadian boundaries and OpenStreetMap tiles.": "Chargement des limites canadiennes et des tuiles OpenStreetMap.",
+ "Retry map": "Réessayer la carte",
+ "Interactive Canadian region map": "Carte interactive des régions canadiennes",
+ "Map legend": "Légende de la carte",
+ "Selected boundary": "Limite sélectionnée",
+ "Browsed group outline": "Contour du groupe parcouru",
+ "List alternative": "Option sous forme de liste",
+ "All regions": "Toutes les régions",
+ "If the map is unavailable, search and select a region from this list.": "Si la carte est indisponible, recherchez et sélectionnez une région dans cette liste.",
+ "Audit data loads when this view is opened.": "Les données de vérification se chargent à l’ouverture de cette vue.",
+ "Unable to load release QA": "Impossible de charger les contrôles de qualité de la publication",
+ "Unable to load the source lock": "Impossible de charger le verrouillage des sources",
+ "Release": "Publication",
+ "Status": "État",
+ "Standard": "Norme",
+ "Connectivity": "Connectivité",
+ "Online": "En ligne",
+ "Offline; showing cached static data when available": "Hors ligne; affichage des données statiques en cache lorsqu’elles sont disponibles",
+ "Digital census cells": "Cellules numériques du recensement",
+ "Census subdivisions": "Subdivisions de recensement",
+ "Positive-area overlaps": "Chevauchements de superficie positive",
+ "Quality checks": "Contrôles de qualité",
+ "Release artifacts": "Artéfacts de publication",
+ "Public partition SHA-256": "SHA-256 de la partition publique",
+ "Resolver partition SHA-256": "SHA-256 de la partition de résolution",
+ "Membership SHA-256": "SHA-256 des appartenances",
+ "Unavailable": "Indisponible",
+ "Download QA JSON": "Télécharger le JSON de contrôle",
+ "Download catalog": "Télécharger le catalogue",
+ "Open standard and change process": "Ouvrir la norme et le processus de modification",
+ "Try again": "Réessayer",
+ "Loading release evidence…": "Chargement des preuves de publication…",
+ "Select this leaf to see its full path.": "Sélectionnez cette région terminale pour voir son chemin complet.",
+ "Province or territory": "Province ou territoire",
+ "resolves to": "correspond à",
+ "Aliases": "Alias",
+ "None recorded": "Aucun",
+ "Planned paths": "Chemins prévus",
+ "Configure this region": "Configurer cette région",
+ "Copy link": "Copier le lien",
+ "This location is outside Canada.": "Cet emplacement se trouve à l’extérieur du Canada.",
+ "No Canadian region contains that point. Browse the region list instead.": "Aucune région canadienne ne contient ce point. Parcourez plutôt la liste des régions.",
+ "No Canadian region contains that point. Browse the list instead.": "Aucune région canadienne ne contient ce point. Parcourez plutôt la liste.",
+ "Loading Canadian boundaries and map tools…": "Chargement des limites canadiennes et des outils cartographiques…",
+ "Loading the interactive region map.": "Chargement de la carte interactive des régions.",
+ "OpenStreetMap tiles did not load. Search and the region list still work.": "Les tuiles OpenStreetMap ne se sont pas chargées. La recherche et la liste des régions fonctionnent toujours.",
+ "OpenStreetMap tiles could not load. Search and the region list still work.": "Impossible de charger les tuiles OpenStreetMap. La recherche et la liste des régions fonctionnent toujours.",
+ "Interactive region map loaded.": "La carte interactive des régions est chargée.",
+ "The map could not load. Search and the region list still work.": "La carte n’a pas pu se charger. La recherche et la liste des régions fonctionnent toujours.",
+ "Search regions": "Rechercher des régions",
+ "Filter by area": "Filtrer par zone",
+ "All areas": "Toutes les zones",
+ "Region directory table": "Tableau du répertoire des régions",
+ "Area": "Zone",
+ "Boundary": "Limite",
+ "Basis": "Fondement",
+ "Canada-wide": "Pancanadienne",
+ "Established": "Établie",
+ "Proposed": "Proposée",
+ "Setup": "Configuration",
+ "Map": "Carte",
+ "Regions": "Régions",
+ "Local regions": "Régions locales",
+ "Provinces & territories": "Provinces et territoires",
+ "Region status summary": "Résumé de l’état des régions",
+ "Loading Canadian regions…": "Chargement des régions canadiennes…",
+ "Newfoundland and Labrador": "Terre-Neuve-et-Labrador",
+ "Prince Edward Island": "Île-du-Prince-Édouard",
+ "Nova Scotia": "Nouvelle-Écosse",
+ "New Brunswick": "Nouveau-Brunswick",
+ "Quebec": "Québec",
+ "British Columbia": "Colombie-Britannique",
+ "Northwest Territories": "Territoires du Nord-Ouest",
+ "Atlantic": "Atlantique",
+ "Prairies": "Prairies",
+ "Northern Canada": "Nord canadien",
+ "Zoom in": "Zoom avant",
+ "Zoom out": "Zoom arrière",
+ "Marker": "Marqueur",
+ "contributors": "contributeurs",
+ "Close popup": "Fermer la fenêtre",
+ "A JavaScript library for interactive maps": "Une bibliothèque JavaScript pour les cartes interactives"
+ };
+
+ var FRENCH_RUNTIME_PATTERNS = [
+ [/^(.+) \(request timed out\)$/, function (match, message) {
+ return translateRuntimeText(message) + " (délai de la requête dépassé)";
+ }],
+ [/^(.+): ([^:]+)$/, function (match, prefix, value) {
+ var translatedPrefix = FRENCH_RUNTIME_TEXT[prefix];
+ return translatedPrefix ? translatedPrefix + " : " + translateRuntimeText(value) : match;
+ }],
+ [/^(\d+) \/ 32 tags, (\d+) \/ 172 bytes$/, function (match, tags, bytes) {
+ return tags + " / 32 identifiants, " + bytes + " / 172 octets";
+ }],
+ [/^That name matches more than one region \((.+)\)\. Add a province or postal code\.$/, function (match, choices) {
+ return "Ce nom correspond à plusieurs régions (" + choices + "). Ajoutez une province ou un code postal.";
+ }],
+ [/^Confirm (.+) tags with neighbouring operators before applying them\.$/, function (match, label) {
+ return "Confirmez les identifiants de " + label + " auprès des exploitants voisins avant de les appliquer.";
+ }],
+ [/^Check locally before using: (.+)\.$/, function (match, tags) {
+ return "Vérifiez localement avant d’utiliser : " + tags + ".";
+ }],
+ [/^Do not use: (.+)\.$/, function (match, tags) {
+ return "N’utilisez pas : " + tags + ".";
+ }],
+ [/^Too many regions: (\d+) tags exceeds the 32-tag limit\.$/, function (match, count) {
+ return "Trop de régions : " + count + " identifiants dépassent la limite de 32.";
+ }],
+ [/^Region names use (\d+) bytes, above the 172-byte response limit\.$/, function (match, bytes) {
+ return "Les noms de régions utilisent " + bytes + " octets, au-delà de la limite de réponse de 172 octets.";
+ }],
+ [/^(.+) keeps (.+) in one repeater configuration\.$/, function (match, area, members) {
+ return area + " regroupe " + members.replace(/ and /g, " et ") + " dans une seule configuration de répéteur.";
+ }],
+ [/^This selection uses (\d+) tags and (\d+) bytes\. Remove regions until it fits the 32-tag and 172-byte limits\.$/, function (match, tags, bytes) {
+ return "Cette sélection utilise " + tags + " identifiants et " + bytes + " octets. Retirez des régions jusqu’à respecter les limites de 32 identifiants et de 172 octets.";
+ }],
+ [/^(.+) combines (.+) in one repeater setup\. All map boundaries remain separate\.$/, function (match, area, members) {
+ return area + " regroupe " + members.replace(/ and /g, " et ") + " dans une seule configuration de répéteur. Toutes les limites cartographiques demeurent distinctes.";
+ }],
+ [/^(.+)\. Each map region keeps its own boundary\.$/, function (match, jurisdictions) {
+ return jurisdictions + ". Chaque région cartographique conserve sa propre limite.";
+ }],
+ [/^(.+) is added to this repeater only\. MeshCore Canada does not own or draw those boundaries\.$/, function (match, paths) {
+ return paths + " est ajouté uniquement à ce répéteur. MeshCore Canada ne possède ni ne trace ces limites.";
+ }],
+ [/^Shared repeater area: (.+)$/, function (match, area) {
+ return "Zone partagée de répéteurs : " + area;
+ }],
+ [/^(\d+) subregions$/, function (match, count) {
+ return count + " sous-régions";
+ }],
+ [/^([A-Z0-9-]+) · region$/, function (match, tag) {
+ return tag + " · région";
+ }],
+ [/^(.+) resolves to (.+)$/, function (match, place, region) {
+ return place + " correspond à " + region;
+ }],
+ [/^Text result: (.+)\.$/, function (match, path) {
+ return "Résultat textuel : " + path + ".";
+ }],
+ [/^(\d+) leaf regions$/, function (match, count) {
+ return count + " régions terminales";
+ }],
+ [/^(\d+) of (\d+) reported invariants pass$/, function (match, passed, total) {
+ return passed + " invariants sur " + total + " sont respectés";
+ }],
+ [/^Source lock: (.+) census vintage · (\d+) locked sources\.$/, function (match, vintage, count) {
+ return "Verrouillage des sources : recensement de " + vintage + " · " + count + " sources verrouillées.";
+ }],
+ [/^(\d+) of (\d+) regions$/, function (match, shown, total) {
+ return shown + " régions sur " + total;
+ }],
+ [/^(\d+) regions$/, function (match, count) {
+ return count + " régions";
+ }],
+ [/^(\d+) \/ 32 tags · (\d+) \/ 172 bytes$/, function (match, tags, bytes) {
+ return tags + " / 32 identifiants · " + bytes + " / 172 octets";
+ }],
+ [/^(.+) region$/, function (match, label) {
+ return "Région de " + label;
+ }]
+ ];
+
+ function translateRuntimeText(value) {
+ var source = String(value == null ? "" : value);
+ if (!frenchRuntime || !source) return source;
+ var leading = (source.match(/^\s*/) || [""])[0];
+ var trailing = (source.match(/\s*$/) || [""])[0];
+ var text = source.slice(leading.length, source.length - trailing.length || source.length);
+ if (!text) return source;
+ if (Object.prototype.hasOwnProperty.call(FRENCH_RUNTIME_TEXT, text)) {
+ var direct = FRENCH_RUNTIME_TEXT[text];
+ var directTrailing = /[’']$/.test(direct) ? "" : trailing;
+ return leading + direct + directTrailing;
+ }
+ for (var index = 0; index < FRENCH_RUNTIME_PATTERNS.length; index += 1) {
+ if (FRENCH_RUNTIME_PATTERNS[index][0].test(text)) {
+ return leading + text.replace(FRENCH_RUNTIME_PATTERNS[index][0], FRENCH_RUNTIME_PATTERNS[index][1]) + trailing;
+ }
+ }
+ return source;
+ }
+
+ function localizeRuntimeNode(node) {
+ if (!frenchRuntime || !node) return;
+ if (node.nodeType === 3) {
+ var parentName = node.parentElement && node.parentElement.tagName;
+ var original = node.nodeValue;
+ var trimmed = String(original || "").trim();
+ if (parentName === "SCRIPT" || parentName === "STYLE" || parentName === "PRE") return;
+ if (parentName === "CODE" && trimmed !== "Unavailable") return;
+ var translated = parentName === "DT" && trimmed === "Review"
+ ? original.replace("Review", "Vérification")
+ : translateRuntimeText(original);
+ if (translated !== node.nodeValue) node.nodeValue = translated;
+ return;
+ }
+ if (node.nodeType !== 1) return;
+ ["aria-label", "placeholder", "title"].forEach(function (attribute) {
+ if (!node.hasAttribute(attribute)) return;
+ var current = node.getAttribute(attribute);
+ var translated = translateRuntimeText(current);
+ if (translated !== current) node.setAttribute(attribute, translated);
+ });
+ Array.prototype.slice.call(node.childNodes || []).forEach(localizeRuntimeNode);
+ }
+
+ function enableRuntimeLocalization(root) {
+ if (!frenchRuntime || !root || root.dataset.mccLocaleReady === "1") return;
+ root.dataset.mccLocaleReady = "1";
+ localizeRuntimeNode(root);
+ if (typeof MutationObserver === "undefined") return;
+ var observer = new MutationObserver(function (records) {
+ records.forEach(function (record) {
+ if (record.type === "characterData") localizeRuntimeNode(record.target);
+ if (record.type === "attributes") localizeRuntimeNode(record.target);
+ Array.prototype.slice.call(record.addedNodes || []).forEach(localizeRuntimeNode);
+ });
+ });
+ observer.observe(root, {
+ subtree: true,
+ childList: true,
+ characterData: true,
+ attributes: true,
+ attributeFilter: ["aria-label", "placeholder", "title"]
+ });
+ root.__mccLocaleObserver = observer;
+ }
+
+ function fetchWithTimeout(url, options, timeoutMs) {
+ var requestOptions = Object.assign({}, options || {});
+ var upstreamSignal = requestOptions.signal;
+ var controller = typeof AbortController !== "undefined" ? new AbortController() : null;
+ var timer = null;
+ var abortFromUpstream = function () {
+ if (controller) controller.abort();
+ };
+ if (controller) {
+ requestOptions.signal = controller.signal;
+ timer = window.setTimeout(function () { controller.abort(); }, timeoutMs || REQUEST_TIMEOUT_MS);
+ if (upstreamSignal) {
+ if (upstreamSignal.aborted) controller.abort();
+ else upstreamSignal.addEventListener("abort", abortFromUpstream, { once: true });
+ }
+ }
+ return fetch(url, requestOptions).finally(function () {
+ if (timer) window.clearTimeout(timer);
+ if (upstreamSignal) upstreamSignal.removeEventListener("abort", abortFromUpstream);
+ });
+ }
- function fetchJsonAsset(filename, errorMessage) {
- return fetch(new URL(filename, assetBase)).then(function (response) {
+ function fetchJsonAsset(filename, errorMessage, retrying) {
+ return fetchWithTimeout(new URL(filename, assetBase), {}, REQUEST_TIMEOUT_MS).then(function (response) {
if (!response.ok) throw new Error(errorMessage);
return response.json();
+ }).catch(function (error) {
+ if (!retrying && error && error.name !== "AbortError") {
+ return fetchJsonAsset(filename, errorMessage, true);
+ }
+ throw error && error.name === "AbortError" ? new Error(errorMessage + " (request timed out)") : error;
});
}
@@ -59,19 +545,17 @@
}
function loadData(mode) {
- if (mode === "map") {
- return Promise.all([loadCatalog(), loadDisplayPartition()]).then(function (loaded) {
- return applyGeneratedPartition(loaded[0], loaded[1], null);
- });
- }
- if (mode === "config") {
- return Promise.all([loadCatalog(), loadResolverPartition()]).then(function (loaded) {
- return applyGeneratedPartition(loaded[0], null, loaded[1]);
- });
- }
+ if (mode === "map" || mode === "config") return loadCatalog();
return loadCatalog();
}
+ function ensureResolverData(data) {
+ if (data.resolverRegions) return Promise.resolve(data);
+ return loadResolverPartition().then(function (partition) {
+ return applyGeneratedPartition(data, null, partition);
+ });
+ }
+
function prepareCatalog(data) {
if (data.__mccPrepared) return data;
var suppliedAliases = data.aliases || {};
@@ -281,10 +765,18 @@
function copyText(text, button, resetLabel) {
var feedback = function (copied) {
if (!button) return;
+ var host = button.closest ? button.closest("[data-mcc-regions]") : null;
+ var live = host && host.querySelector("[data-mcc-copy-status]");
var originalHtml = button.dataset.originalHtml || button.innerHTML || resetLabel || "Copy";
button.dataset.originalHtml = originalHtml;
button.classList.toggle("is-copied", copied);
button.innerHTML = copied ? "Copied" : "Copy failed";
+ if (live) {
+ live.textContent = "";
+ window.setTimeout(function () {
+ live.textContent = copied ? "Copied to clipboard." : "Copy failed. Select and copy the command manually.";
+ }, 10);
+ }
window.setTimeout(function () {
button.classList.remove("is-copied");
button.innerHTML = button.dataset.originalHtml || resetLabel || "Copy";
@@ -343,9 +835,10 @@
}
function labelFor(data, tag) {
- if (data.hierarchy[tag]) return data.hierarchy[tag].label;
- if (data.externalTagLabels && data.externalTagLabels[tag]) return data.externalTagLabels[tag];
- return tag;
+ var label = tag;
+ if (data.hierarchy[tag]) label = data.hierarchy[tag].label;
+ else if (data.externalTagLabels && data.externalTagLabels[tag]) label = data.externalTagLabels[tag];
+ return translateRuntimeText(label);
}
function scopeExists(data, tag) {
@@ -501,6 +994,10 @@
return regionPageHref("map") + (params.toString() ? "?" + params.toString() : "");
}
+ function configHrefForState(state) {
+ var mapHref = new URL(mapHrefForState(state));
+ return regionPageHref("config") + mapHref.search;
+ }
function haversineKm(aLat, aLon, bLat, bLon) {
var rad = function (d) { return d * Math.PI / 180; };
var dLat = rad(bLat - aLat);
@@ -870,8 +1367,12 @@
};
}
- function geocoderCaSearch(query) {
- return fetch("https://geocoder.ca/?locate=" + encodeURIComponent(query) + "&json=1")
+ function geocoderCaSearch(query, signal) {
+ return fetchWithTimeout(
+ "https://geocoder.ca/?locate=" + encodeURIComponent(query) + "&json=1",
+ { signal: signal },
+ REQUEST_TIMEOUT_MS
+ )
.then(function (res) {
if (!res.ok) throw new Error("Geocoding service error");
return res.json();
@@ -881,13 +1382,16 @@
});
}
- function nominatimSearch(params) {
+ function nominatimSearch(params, signal) {
var url = "https://nominatim.openstreetmap.org/search?" + new URLSearchParams(Object.assign({
format: "json",
limit: "1",
addressdetails: "1"
}, params));
- return fetch(url, { headers: { "Accept-Language": "en-CA,en" } })
+ return fetchWithTimeout(url, {
+ headers: { "Accept-Language": frenchRuntime ? "fr-CA,fr,en" : "en-CA,en" },
+ signal: signal
+ }, REQUEST_TIMEOUT_MS)
.then(function (res) {
if (!res.ok) throw new Error("Geocoding service error");
return res.json();
@@ -897,13 +1401,17 @@
});
}
- function geocodeCanadianPostal(postal) {
- return nominatimSearch({ postalcode: postal.formatted, country: "ca" }).then(function (hit) {
+ function geocodeCanadianPostal(postal, signal) {
+ return nominatimSearch({ postalcode: postal.formatted, country: "ca" }, signal).then(function (hit) {
if (hit) return hit;
- return nominatimSearch({ q: postal.formatted, countrycodes: "ca" });
+ return nominatimSearch({ q: postal.formatted, countrycodes: "ca" }, signal);
}).then(function (hit) {
if (hit) return hit;
- return fetch("https://geocoder.ca/?locate=" + encodeURIComponent(postal.compact) + "&json=1")
+ return fetchWithTimeout(
+ "https://geocoder.ca/?locate=" + encodeURIComponent(postal.compact) + "&json=1",
+ { signal: signal },
+ REQUEST_TIMEOUT_MS
+ )
.then(function (res) {
if (!res.ok) throw new Error("Geocoding service error");
return res.json();
@@ -917,7 +1425,7 @@
});
}
- function geocode(data, query) {
+ function geocode(data, query, signal) {
var localMatch = localGeocode(data, query);
if (localMatch && localMatch.ambiguous) {
return Promise.reject(new Error("That name matches more than one region (" + localMatch.choices.join("; ") + "). Add a province or postal code."));
@@ -925,18 +1433,19 @@
if (localMatch && localMatch.exactLocalMatch) return Promise.resolve(localMatch);
var postal = parseCanadianPostalCode(query);
var primaryLookup = postal
- ? geocodeCanadianPostal(postal)
- : nominatimSearch({ q: query, countrycodes: "ca" }).then(function (hit) {
+ ? geocodeCanadianPostal(postal, signal)
+ : nominatimSearch({ q: query, countrycodes: "ca" }, signal).then(function (hit) {
if (hit) return hit;
- return nominatimSearch({ q: query });
+ return nominatimSearch({ q: query }, signal);
});
return primaryLookup.catch(function () {
- return geocoderCaSearch(postal ? postal.formatted : query).catch(function () { return null; });
+ if (signal && signal.aborted) throw new DOMException("Request cancelled", "AbortError");
+ return geocoderCaSearch(postal ? postal.formatted : query, signal).catch(function () { return null; });
}).then(function (hit) {
if (hit) return hit;
if (localMatch) return localMatch;
- throw new Error("Location lookup is temporarily unavailable. Try an airport code or use the map.");
+ throw new Error("Online place lookup is unavailable. Enter coordinates or browse the region list.");
});
}
@@ -1160,10 +1669,10 @@
'
' +
'
' + icon("usb") + '
USB serial
At the repeater
' +
'
Connect the repeater to a computer with a data-capable USB cable.
We will recommend forwarding paths, then show how to apply them.
' +
+ '
' +
+ 'RepeaterRecommended for most operators' +
+ 'Room server with repeatingUses the same region paths' +
+ 'Advanced operatorReview wide and cross-border paths' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
Step 2 of 4
' +
+ '
Where is the node?
' +
+ '
' +
+ '
Search a place
' +
+ 'City, airport code, postal code, or region name' +
'
' +
'' +
'' +
'
' +
- '
Search uses OpenStreetMap and geocoder.ca. Or click the map.
' +
+ '
' +
+ '
' +
+ '' +
+ '
Enter coordinates
' +
+ '
' +
+ 'Latitude' +
+ 'Longitude' +
+ '
' +
+ '' +
+ '
Coordinates are checked against the Canadian region data in this page.
' +
+ '' +
+ '' +
+ '
Use this device
' +
+ '' +
+ '
Your browser asks first. MeshCore Canada does not receive or store your coordinates.
' +
+ '' +
+ '
' +
'' +
'' +
- '' +
- 'Choose a different region' +
+ '' +
+ 'Browse regions without search or a map' +
'' +
'' +
'' +
'
' +
+ 'Recommended local areaUse the home region and any registered shared area' +
+ 'Add nearby or cross-border pathsFor bridge, wide-coverage, mountain, or water-path repeaters' +
+ '
No account needed. Submission starts public review; it does not change the map.
Canada
- Census cells · yellow means changed
+ Inspect cells or use the municipality list; yellow means changed
-
+
+
The map is optional. Use the municipality list in step three for a full non-map flow. In Inspect mode, select a cell to read its details. Enable the advanced acknowledgement before painting individual cells.