diff --git a/data/communities.fr.json b/data/communities.fr.json
index daca101..f71c39c 100644
--- a/data/communities.fr.json
+++ b/data/communities.fr.json
@@ -93,6 +93,9 @@
"yqrmesh": {
"service_area": "Regina et sud de la Saskatchewan"
},
+ "winnipeg-meshcore": {
+ "service_area": "Winnipeg (Manitoba)"
+ },
"greater-ottawa-mesh-enthusiasts": {
"service_area": "Est de l’Ontario et ouest du Québec"
},
diff --git a/data/communities.json b/data/communities.json
index 628f285..f53346d 100644
--- a/data/communities.json
+++ b/data/communities.json
@@ -821,16 +821,16 @@
},
"contacts": [
{
- "type": "telegram",
- "label": "Mesh Saskatchewan Telegram",
- "url": "https://t.me/MeshtSaska",
+ "type": "discord",
+ "label": "StoonMesh Discord",
+ "url": "https://discord.gg/StFvPY7BZe",
"health": "verified",
- "last_checked": "2026-08-29"
+ "last_checked": "2026-09-24"
}
],
"owner": null,
- "verified_at": "2026-08-29",
- "verify_by": "2027-02-28",
+ "verified_at": "2026-09-24",
+ "verify_by": "2027-03-24",
"canonical_route": "/provinces/saskatchewan/#community-stoonmesh"
},
{
@@ -862,6 +862,50 @@
"verify_by": "2027-02-28",
"canonical_route": "/provinces/saskatchewan/#community-yqrmesh"
},
+ {
+ "id": "winnipeg-meshcore",
+ "name": "Winnipeg MeshCore",
+ "province": "MB",
+ "service_area": "Winnipeg, Manitoba",
+ "places": [
+ "Winnipeg"
+ ],
+ "aliases": [
+ "WPG",
+ "YWG"
+ ],
+ "status": "forming",
+ "languages": [],
+ "location": {
+ "latitude": null,
+ "longitude": null,
+ "precision": "service-area"
+ },
+ "settings": {
+ "inherit_national": true,
+ "overrides": {}
+ },
+ "contacts": [
+ {
+ "type": "website",
+ "label": "Winnipeg MeshCore website",
+ "url": "https://winnipegmeshcore.neocities.org/",
+ "health": "verified",
+ "last_checked": "2026-09-24"
+ },
+ {
+ "type": "matrix",
+ "label": "Winnipeg MeshCore chat",
+ "url": "https://matrix.to/#/#winnipeg-meshcore:matrix.org",
+ "health": "verified",
+ "last_checked": "2026-09-24"
+ }
+ ],
+ "owner": null,
+ "verified_at": "2026-09-24",
+ "verify_by": "2027-03-24",
+ "canonical_route": "/provinces/manitoba/#community-winnipeg-meshcore"
+ },
{
"id": "greater-ottawa-mesh-enthusiasts",
"name": "Greater Ottawa Mesh Enthusiasts",
diff --git a/data/community-search-anchors.json b/data/community-search-anchors.json
index 0bc6c42..a5acc3c 100644
--- a/data/community-search-anchors.json
+++ b/data/community-search-anchors.json
@@ -14,6 +14,7 @@
"yyc-meshcore-discord": ["cal"],
"stoonmesh": ["stoon"],
"yqrmesh": ["regina"],
+ "winnipeg-meshcore": ["wpg"],
"greater-ottawa-mesh-enthusiasts": ["ott", "gatout"],
"gta-lora-meshes": ["tor"],
"quinte-mesh-network": ["has", "pec"],
diff --git a/docs/provinces/index.fr.md b/docs/provinces/index.fr.md
index 1fea6ff..8212cc2 100644
--- a/docs/provinces/index.fr.md
+++ b/docs/provinces/index.fr.md
@@ -31,9 +31,9 @@ Filtrez la liste par communauté ou province, ou cherchez votre ville pour trouv
Ces renseignements pourraient être périmés !
-24 fiches
+25 fiches
23 fiches actives
-1 fiche en formation
+2 fiches en formation
1 fiche avec des réglages locaux différents
@@ -57,7 +57,7 @@ Filtrez la liste par communauté ou province, ou cherchez votre ville pour trouv
Trouver à proximité
Effacer
- 24 communautés affichées
+ 25 communautés affichées
@@ -274,9 +274,9 @@ Filtrez la liste par communauté ou province, ou cherchez votre ville pour trouv
Province ou territoire : Saskatchewan
Réglages : Réglages par défaut du Canada
-Fiche révisée : 2026-08-29
+Fiche révisée : 2026-09-24
@@ -293,6 +293,21 @@ Filtrez la liste par communauté ou province, ou cherchez votre ville pour trouv
+
diff --git a/schemas/community-directory.schema.json b/schemas/community-directory.schema.json
index d22e569..d6bb4cf 100644
--- a/schemas/community-directory.schema.json
+++ b/schemas/community-directory.schema.json
@@ -186,6 +186,7 @@
"discord",
"facebook",
"instagram",
+ "matrix",
"meshmapper",
"reddit",
"telegram",
diff --git a/scripts/validate-communities.py b/scripts/validate-communities.py
index 1d35c18..ce7fe4b 100644
--- a/scripts/validate-communities.py
+++ b/scripts/validate-communities.py
@@ -46,6 +46,7 @@
"discord",
"facebook",
"instagram",
+ "matrix",
"meshmapper",
"reddit",
"telegram",
@@ -1083,6 +1084,7 @@ def contact_type_label_fr(contact_type: str) -> str:
"discord": "Discord",
"facebook": "Facebook",
"instagram": "Instagram",
+ "matrix": "Matrix",
"meshmapper": "MeshMapper",
"reddit": "Reddit",
"telegram": "Telegram",
@@ -1126,6 +1128,8 @@ def contact_label_fr(label: str) -> str:
"Cardston topic in Mesh Alberta": "Sujet sur Cardston dans Mesh Alberta",
"YYC MeshCore Discord": "Discord de YYC MeshCore",
"StoonMesh Discord": "Discord de StoonMesh",
+ "Winnipeg MeshCore website": "Site Web de Winnipeg MeshCore",
+ "Winnipeg MeshCore chat": "Salon de Winnipeg MeshCore",
"Greater Ottawa Mesh Enthusiasts Discord": "Discord de Greater Ottawa Mesh Enthusiasts",
"Ottawa Mesh website": "Site Web d’Ottawa Mesh",
"GTA+-Lora-Meshes Discord": "Discord de GTA+-Lora-Meshes",
diff --git a/tests/browser/critical-journeys.spec.mjs b/tests/browser/critical-journeys.spec.mjs
index 132a86d..ba6c377 100644
--- a/tests/browser/critical-journeys.spec.mjs
+++ b/tests/browser/critical-journeys.spec.mjs
@@ -293,6 +293,29 @@ test("community directory deep links use their dedicated query parameter", async
expect(url.searchParams.has("q")).toBeFalsy();
});
+for (const locale of ["", "fr/"]) {
+ test(`${locale || "en/"} community updates expose StoonMesh and Winnipeg contacts`, async ({ page }) => {
+ await page.goto(siteRoute(`/${locale}provinces/?community=StoonMesh`));
+ const stoon = page.locator("#directory-stoonmesh");
+ await expect(stoon).toBeVisible();
+ await expect(stoon.locator('a[href="https://discord.gg/StFvPY7BZe"]')).toBeVisible();
+ await expect(stoon.locator('a[href*="t.me"]')).toHaveCount(0);
+ await stoon.locator("h3 a").click();
+ await expect(page.locator('#community-stoonmesh a[href="https://discord.gg/StFvPY7BZe"]')).toBeVisible();
+
+ await page.goto(siteRoute(`/${locale}provinces/?community=Winnipeg`));
+ const winnipeg = page.locator("#directory-winnipeg-meshcore");
+ await expect(page.locator("[data-community-card]:visible")).toHaveCount(1);
+ await expect(winnipeg.locator('[data-status="forming"]')).toHaveText(locale ? "En formation" : "Forming");
+ await expect(winnipeg.locator('a[href="https://matrix.to/#/#winnipeg-meshcore:matrix.org"]')).toBeVisible();
+ await winnipeg.locator("h3 a").click();
+ await expect(page).toHaveURL(new RegExp(`/${locale}provinces/manitoba/#community-winnipeg-meshcore$`));
+ const detail = page.locator("#community-winnipeg-meshcore");
+ await expect(detail.locator('a[href="https://winnipegmeshcore.neocities.org/"]')).toBeVisible();
+ await expect(detail.locator('a[href="https://matrix.to/#/#winnipeg-meshcore:matrix.org"]')).toBeVisible();
+ });
+}
+
test("BC Mesh's local frequency is searchable in both directories", async ({ page }) => {
for (const [route, count] of [
["/provinces/", "Showing 1 community"],
diff --git a/tests/browser/site-feedback.spec.mjs b/tests/browser/site-feedback.spec.mjs
index 2a48495..55f377a 100644
--- a/tests/browser/site-feedback.spec.mjs
+++ b/tests/browser/site-feedback.spec.mjs
@@ -1,7 +1,11 @@
import { expect, test } from "./site-fixtures.mjs";
import AxeBuilder from "@axe-core/playwright";
+import { readFileSync } from "node:fs";
import { siteRoute } from "./site-route.mjs";
+const communityCount = JSON.parse(
+ readFileSync(new URL("../../data/communities.json", import.meta.url), "utf8"),
+).communities.length;
const overview = { activeObservers: 12, activeIatas: 4, totalPackets: 900, windowHours: 24 };
const types = [{ nodeType: 2, count: 50 }, { nodeType: 1, count: 20 }, { nodeType: 3, count: 3 }, { nodeType: 4, count: 2 }];
@@ -39,10 +43,10 @@ for (const locale of ["", "fr/"]) {
expect(query.searchParams.get("keys")).toBe("geonames");
expect(query.searchParams.get("lang")).toBe(locale ? "fr" : "en");
await page.locator("[data-community-show-all]").click();
- await expect(cards).toHaveCount(24);
+ await expect(cards).toHaveCount(communityCount);
await page.locator("[data-community-clear]").first().click();
await expect(page.locator("#community-search")).toHaveValue("");
- await expect(cards).toHaveCount(24);
+ await expect(cards).toHaveCount(communityCount);
await expect(cards.first()).toHaveAttribute("id", "directory-bc-mesh");
expect(lookups).toHaveLength(1);
});
diff --git a/tests/content/browser-feedback.test.mjs b/tests/content/browser-feedback.test.mjs
index 24ab4bd..66838b2 100644
--- a/tests/content/browser-feedback.test.mjs
+++ b/tests/content/browser-feedback.test.mjs
@@ -56,7 +56,7 @@ test("Beacon totals distinguish known devices from the overview time window", ()
assert.throws(() => network.parseStats({ ...overview, windowHours: 0 }, types));
});
-test("all 24 listings have finite geographic search references without changing their source locations", () => {
+test("all listings have finite geographic search references without changing their source locations", () => {
const directory = JSON.parse(readFileSync("data/communities.json", "utf8"));
const anchors = JSON.parse(readFileSync("data/community-search-anchors.json", "utf8"));
assert.deepEqual(Object.keys(anchors.communities).sort(), directory.communities.map(item => item.id).sort());
@@ -64,7 +64,7 @@ test("all 24 listings have finite geographic search references without changing
const page = readFileSync(`docs/provinces/${locale}`, "utf8");
assert.match(page, /status_notice: false/);
const points = [...page.matchAll(/data-community-points="([^"]+)"/g)];
- assert.equal(points.length, 24);
+ assert.equal(points.length, directory.communities.length);
for (const [, encoded] of points) {
const list = JSON.parse(encoded.replaceAll(""", '"').replaceAll("'", "'").replaceAll("&", "&"));
assert.ok(list.length > 0);
diff --git a/tests/content/community-directory.test.mjs b/tests/content/community-directory.test.mjs
index 620e9b9..3750a52 100644
--- a/tests/content/community-directory.test.mjs
+++ b/tests/content/community-directory.test.mjs
@@ -84,8 +84,12 @@ const expectedCommunities = new Map([
"yyc-meshcore-discord",
["https://discord.gg/CznDhsRWnJ", "https://meshmonitoring.com/"],
],
- ["stoonmesh", ["https://t.me/MeshtSaska"]],
+ ["stoonmesh", ["https://discord.gg/StFvPY7BZe"]],
["yqrmesh", []],
+ [
+ "winnipeg-meshcore",
+ ["https://winnipegmeshcore.neocities.org/", "https://matrix.to/#/#winnipeg-meshcore:matrix.org"],
+ ],
[
"greater-ottawa-mesh-enthusiasts",
["https://discord.gg/WSyNd8SfNr", "https://ottawamesh.ca/"],
@@ -148,8 +152,8 @@ function idsMatching(query) {
.map((community) => community.id);
}
-test("all 24 structured listings match the reviewed contact fixture", () => {
- assert.equal(data.communities.length, 24);
+test("all structured listings match the reviewed contact fixture", () => {
+ assert.equal(data.communities.length, expectedCommunities.size);
assert.deepEqual(
new Set(data.communities.map((community) => community.id)),
new Set(expectedCommunities.keys()),
@@ -185,7 +189,7 @@ test("the generated directory cannot drift from structured data", () => {
encoding: "utf8",
},
);
- assert.match(output, /Community directory validated: 24 listings/);
+ assert.match(output, new RegExp(`Community directory validated: ${expectedCommunities.size} listings`));
});
test("search covers reviewed place names and common aliases", () => {
@@ -205,6 +209,9 @@ test("search covers reviewed place names and common aliases", () => {
]);
assert.deepEqual(idsMatching("YQR"), ["yqrmesh"]);
assert.deepEqual(idsMatching("Regina"), ["yqrmesh"]);
+ assert.deepEqual(idsMatching("Winnipeg"), ["winnipeg-meshcore"]);
+ assert.deepEqual(idsMatching("WPG"), ["winnipeg-meshcore"]);
+ assert.deepEqual(idsMatching("YWG"), ["winnipeg-meshcore"]);
assert.deepEqual(idsMatching("Montréal"), ["montreal-mesh", "reseau-libre"]);
assert.deepEqual(idsMatching("Airdrie"), [
"alberta-meshcore-networks",
@@ -377,6 +384,36 @@ test("BC Mesh documents the issue 79 frequency override in both languages", () =
}
});
+test("issues 94 and 95 keep corrected contacts and Winnipeg status in both languages", () => {
+ const winnipeg = data.communities.find(community => community.id === "winnipeg-meshcore");
+ assert.equal(winnipeg.province, "MB");
+ assert.equal(winnipeg.status, "forming");
+ assert.deepEqual(winnipeg.settings, { inherit_national: true, overrides: {} });
+ assert.deepEqual(winnipeg.contacts.map(contact => contact.type), ["website", "matrix"]);
+ const schema = JSON.parse(readFileSync(join(root, "schemas/community-directory.schema.json"), "utf8"));
+ assert.ok(schema.$defs.contact.properties.type.enum.includes("matrix"));
+ const anchors = JSON.parse(readFileSync(join(root, "data/community-search-anchors.json"), "utf8"));
+ assert.deepEqual(anchors.communities[winnipeg.id], ["wpg"]);
+
+ for (const suffix of [".md", ".fr.md"]) {
+ const index = readFileSync(join(provinceDir, `index${suffix}`), "utf8");
+ const saskatchewan = readFileSync(join(provinceDir, `saskatchewan${suffix}`), "utf8");
+ const manitoba = readFileSync(join(provinceDir, `manitoba${suffix}`), "utf8");
+ for (const page of [index, saskatchewan]) {
+ assert.ok(page.includes('href="https://discord.gg/StFvPY7BZe"'));
+ assert.ok(!page.includes("MeshtSaska"));
+ assert.ok(!page.includes("Mesh Saskatchewan Telegram"));
+ }
+ for (const page of [index, manitoba]) {
+ assert.ok(page.includes('href="https://winnipegmeshcore.neocities.org/"'));
+ assert.match(page, /Matrix\s*:<\/strong>/);
+ assert.ok(page.includes('href="https://matrix.to/#/#winnipeg-meshcore:matrix.org"'));
+ }
+ assert.ok(manitoba.includes('id="community-winnipeg-meshcore"'));
+ assert.match(manitoba, /data-status="forming">(?:Forming|En formation));
+ }
+});
+
test("summaries and added social contact types render from structured data", () => {
const yqlmesh = data.communities.find(
(community) => community.id === "yqlmesh",