From 1fe4d495a6c45dfb8526fcb7db2d978204275a8e Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Sun, 22 Feb 2026 21:02:16 +0530 Subject: [PATCH 1/3] Add automated community metrics to community page --- .github/workflows/update-discourse-data.yml | 2 + assets/data/community-metrics.json | 60 +++++++ content/community/community.md | 9 ++ js/community-metrics.js | 166 ++++++++++++++++++++ tools/fetch-community-metrics.py | 154 ++++++++++++++++++ 5 files changed, 391 insertions(+) create mode 100644 assets/data/community-metrics.json create mode 100644 js/community-metrics.js create mode 100644 tools/fetch-community-metrics.py diff --git a/.github/workflows/update-discourse-data.yml b/.github/workflows/update-discourse-data.yml index 1a822541c0b..0931a3d1cc5 100644 --- a/.github/workflows/update-discourse-data.yml +++ b/.github/workflows/update-discourse-data.yml @@ -32,6 +32,7 @@ jobs: run: | python tools/fetch-news.py python tools/fetch-faq.py + python tools/fetch-community-metrics.py - name: Configure Git author run: | @@ -42,6 +43,7 @@ jobs: run: | git add assets/data/news.json git add assets/data/faq.json + git add assets/data/community-metrics.json git commit -m "Update Discourse data data [skip ci]" || echo "No changes to commit" - name: Push commit diff --git a/assets/data/community-metrics.json b/assets/data/community-metrics.json new file mode 100644 index 00000000000..4a0e0582012 --- /dev/null +++ b/assets/data/community-metrics.json @@ -0,0 +1,60 @@ +{ + "generated_at": "2026-02-22T15:31:13.791892+00:00", + "github": { + "repositories": [ + { + "id": "core", + "label": "preCICE core", + "full_name": "precice/precice", + "url": "https://github.com/precice/precice", + "description": "A coupling library and ecosystem for partitioned multi-physics and multi-scale simulations, including surface and volume coupling.", + "stars": 887, + "forks": 224, + "open_issues": 230, + "watchers": 35, + "contributors": 66, + "latest_commit_at": "2026-02-18T13:23:28Z", + "latest_release": { + "name": "v3.3.1", + "tag_name": "v3.3.1", + "published_at": "2026-01-14T15:28:58Z", + "url": "https://github.com/precice/precice/releases/tag/v3.3.1", + "assets_count": 8, + "downloads_count": 345 + } + }, + { + "id": "tutorials", + "label": "Tutorials", + "full_name": "precice/tutorials", + "url": "https://github.com/precice/tutorials", + "description": "Various tutorial cases for the coupling library preCICE with real solvers. These files are meant to be rendered on precice.org, so don't look at the README files here.", + "stars": 131, + "forks": 138, + "open_issues": 110, + "watchers": 10, + "contributors": 48, + "latest_commit_at": "2026-02-20T20:01:02Z", + "latest_release": { + "name": "v202404.0 - Now with preCICE v3", + "tag_name": "v202404.0", + "published_at": "2024-04-16T20:35:04Z", + "url": "https://github.com/precice/tutorials/releases/tag/v202404.0", + "assets_count": 0, + "downloads_count": 0 + } + } + ] + }, + "discourse": { + "url": "https://precice.discourse.group", + "title": "preCICE Forum on Discourse", + "site_creation_date": "2019-09-16T04:29:48.122Z", + "users_count": 676, + "topics_count": 1163, + "posts_count": 8866, + "active_users_30_days": 62, + "topics_30_days": 18, + "posts_30_days": 83 + } +} \ No newline at end of file diff --git a/content/community/community.md b/content/community/community.md index d340619d90d..57a9ef53414 100644 --- a/content/community/community.md +++ b/content/community/community.md @@ -21,6 +21,15 @@ Meet the community online, ask questions, and help others at the [preCICE forum Are you looking for something else? Maybe one of the other [community channels](community-channels.html) is for you. +## Community metrics + +The following metrics are updated automatically and provide a quick snapshot of community activity. + +
+

Loading automatically generated metrics...

+ + + ## Support preCICE There are different ways how to support preCICE and get priority support from the preCICE developers in return. [Find out which options](community-support-precice.html). diff --git a/js/community-metrics.js b/js/community-metrics.js new file mode 100644 index 00000000000..3a2e25e27f7 --- /dev/null +++ b/js/community-metrics.js @@ -0,0 +1,166 @@ +console.log("community-metrics.js loaded"); + +(function () { + function formatNumber(value) { + if (typeof value !== "number") { + return "n/a"; + } + return new Intl.NumberFormat("en-US").format(value); + } + + function formatDate(value) { + if (!value) { + return "n/a"; + } + + var parsed = new Date(value); + if (Number.isNaN(parsed.getTime())) { + return "n/a"; + } + + return parsed.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + } + + function createItem(label, value) { + return "
  • " + label + ": " + value + "
  • "; + } + + function createCardColumn(title, description, items, linkUrl, linkLabel) { + var column = document.createElement("div"); + column.className = "col-md-4 col-sm-6 col-flex"; + + var card = document.createElement("div"); + card.className = "panel panel-primary panel-precice full-height"; + + var list = ""; + var descriptionHtml = description ? "

    " + description + "

    " : ""; + var linkHtml = ""; + if (linkUrl && linkLabel) { + linkHtml = + "

    " + + linkLabel + + "  

    "; + } + + card.innerHTML = + "
    " + + title + + "
    " + + "
    " + + descriptionHtml + + list + + linkHtml + + "
    "; + + column.appendChild(card); + return column; + } + + function createRepositoryCard(repo) { + var latestRelease = repo.latest_release; + var latestReleaseValue = "n/a"; + if (latestRelease && latestRelease.url) { + latestReleaseValue = + "" + + (latestRelease.name || latestRelease.tag_name || "Release") + + " (" + + formatDate(latestRelease.published_at) + + ")"; + } + + var releaseDownloads = latestRelease ? formatNumber(latestRelease.downloads_count) : "n/a"; + + return createCardColumn( + repo.label, + repo.description || "", + [ + createItem("Stars", formatNumber(repo.stars)), + createItem("Contributors", formatNumber(repo.contributors)), + createItem("Forks", formatNumber(repo.forks)), + createItem("Open issues", formatNumber(repo.open_issues)), + createItem("Latest commit", formatDate(repo.latest_commit_at)), + createItem("Latest release", latestReleaseValue), + createItem("Release downloads", releaseDownloads), + ], + repo.url, + "Open repository" + ); + } + + function createDiscourseCard(discourse) { + return createCardColumn( + "Discourse forum", + "Community activity snapshot from the preCICE forum.", + [ + createItem("Users", formatNumber(discourse.users_count)), + createItem("Topics", formatNumber(discourse.topics_count)), + createItem("Posts", formatNumber(discourse.posts_count)), + createItem("Active users (30d)", formatNumber(discourse.active_users_30_days)), + createItem("Topics (30d)", formatNumber(discourse.topics_30_days)), + createItem("Posts (30d)", formatNumber(discourse.posts_30_days)), + ], + discourse.url, + "Open forum" + ); + } + + function showError(status, message) { + if (status) { + status.textContent = message; + } + } + + document.addEventListener("DOMContentLoaded", async function () { + var container = document.getElementById("community-metrics"); + if (!container) { + return; + } + + var status = document.getElementById("community-metrics-status"); + + try { + var response = await fetch("/assets/data/community-metrics.json"); + if (!response.ok) { + throw new Error("HTTP " + response.status); + } + + var metrics = await response.json(); + var repositories = (metrics.github && metrics.github.repositories) || []; + var discourse = metrics.discourse; + + if (!repositories.length || !discourse) { + throw new Error("Missing metrics data"); + } + + var row = document.createElement("div"); + row.className = "row equal"; + + for (var i = 0; i < repositories.length; i += 1) { + row.appendChild(createRepositoryCard(repositories[i])); + } + row.appendChild(createDiscourseCard(discourse)); + + container.innerHTML = ""; + container.appendChild(row); + + if (status) { + status.textContent = + "Automatically generated metrics. Last updated " + formatDate(metrics.generated_at) + "."; + } + } catch (error) { + console.error("Could not load community metrics:", error); + showError( + status, + "Could not load the metrics right now. You can still browse all details on GitHub and in the forum." + ); + } + }); +})(); diff --git a/tools/fetch-community-metrics.py b/tools/fetch-community-metrics.py new file mode 100644 index 00000000000..63b37da8347 --- /dev/null +++ b/tools/fetch-community-metrics.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +import json +import os +import re +import urllib.error +import urllib.request +from datetime import datetime, timezone + +GITHUB_API_BASE = "https://api.github.com" +DISCOURSE_ABOUT_URL = "https://precice.discourse.group/about.json" +OUTPUT_FILE = "./assets/data/community-metrics.json" + +GITHUB_HEADERS = { + "Accept": "application/vnd.github+json", + "User-Agent": "precice-community-metrics-bot", +} + +REPOSITORIES = [ + {"id": "core", "owner": "precice", "repo": "precice", "label": "preCICE core"}, + {"id": "tutorials", "owner": "precice", "repo": "tutorials", "label": "Tutorials"}, +] + + +def http_get_json(url, headers=None): + request = urllib.request.Request(url, headers=headers or {}) + with urllib.request.urlopen(request) as response: + payload = json.load(response) + return payload, response.headers + + +def parse_last_page(link_header): + if not link_header: + return None + + for link_part in link_header.split(","): + if 'rel="last"' not in link_part: + continue + match = re.search(r"[?&]page=(\d+)", link_part) + if match: + return int(match.group(1)) + return None + + +def fetch_contributor_count(owner, repo): + endpoint = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/contributors?anon=1&per_page=1" + try: + contributors, headers = http_get_json(endpoint, GITHUB_HEADERS) + except urllib.error.HTTPError as error: + print(f"Could not fetch contributors for {owner}/{repo}: HTTP {error.code}") + return None + except urllib.error.URLError as error: + print(f"Could not fetch contributors for {owner}/{repo}: {error}") + return None + + if not contributors: + return 0 + + last_page = parse_last_page(headers.get("Link")) + return last_page if last_page is not None else len(contributors) + + +def fetch_latest_release(owner, repo): + endpoint = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/releases/latest" + try: + release, _ = http_get_json(endpoint, GITHUB_HEADERS) + except urllib.error.HTTPError as error: + # Some repositories may not have a latest release. + if error.code == 404: + return None + print(f"Could not fetch latest release for {owner}/{repo}: HTTP {error.code}") + return None + except urllib.error.URLError as error: + print(f"Could not fetch latest release for {owner}/{repo}: {error}") + return None + + assets = release.get("assets", []) + return { + "name": release.get("name") or release.get("tag_name"), + "tag_name": release.get("tag_name"), + "published_at": release.get("published_at"), + "url": release.get("html_url"), + "assets_count": len(assets), + "downloads_count": sum(asset.get("download_count", 0) for asset in assets), + } + + +def fetch_github_repo_metrics(repo_data): + owner = repo_data["owner"] + repo = repo_data["repo"] + endpoint = f"{GITHUB_API_BASE}/repos/{owner}/{repo}" + + try: + repository, _ = http_get_json(endpoint, GITHUB_HEADERS) + except urllib.error.HTTPError as error: + raise RuntimeError(f"Could not fetch repository {owner}/{repo}: HTTP {error.code}") from error + except urllib.error.URLError as error: + raise RuntimeError(f"Could not fetch repository {owner}/{repo}: {error}") from error + + return { + "id": repo_data["id"], + "label": repo_data["label"], + "full_name": repository.get("full_name"), + "url": repository.get("html_url"), + "description": repository.get("description"), + "stars": repository.get("stargazers_count"), + "forks": repository.get("forks_count"), + "open_issues": repository.get("open_issues_count"), + "watchers": repository.get("subscribers_count"), + "contributors": fetch_contributor_count(owner, repo), + "latest_commit_at": repository.get("pushed_at"), + "latest_release": fetch_latest_release(owner, repo), + } + + +def fetch_discourse_metrics(): + data, _ = http_get_json(DISCOURSE_ABOUT_URL) + about = data.get("about", {}) + stats = about.get("stats", {}) + + return { + "url": "https://precice.discourse.group", + "title": about.get("title"), + "site_creation_date": about.get("site_creation_date"), + "users_count": stats.get("users_count"), + "topics_count": stats.get("topics_count"), + "posts_count": stats.get("posts_count"), + "active_users_30_days": stats.get("active_users_30_days"), + "topics_30_days": stats.get("topics_30_days"), + "posts_30_days": stats.get("posts_30_days"), + } + + +def main(): + print("Fetching GitHub metrics...") + github_repositories = [fetch_github_repo_metrics(repo) for repo in REPOSITORIES] + + print("Fetching Discourse metrics...") + discourse = fetch_discourse_metrics() + + payload = { + "generated_at": datetime.now(timezone.utc).isoformat(), + "github": {"repositories": github_repositories}, + "discourse": discourse, + } + + os.makedirs(os.path.dirname(OUTPUT_FILE), exist_ok=True) + with open(OUTPUT_FILE, "w", encoding="utf-8") as file: + json.dump(payload, file, indent=2) + + print(f"Community metrics saved to {OUTPUT_FILE}") + + +if __name__ == "__main__": + main() From 7956bd090523b4bfb2c6aea270114f57b2aafc74 Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Sun, 22 Feb 2026 23:44:23 +0530 Subject: [PATCH 2/3] Refine community metrics UI for readability --- content/community/community.md | 2 +- js/community-metrics.js | 29 ++++++++++++++++------------- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/content/community/community.md b/content/community/community.md index 57a9ef53414..a6e52269655 100644 --- a/content/community/community.md +++ b/content/community/community.md @@ -25,8 +25,8 @@ Are you looking for something else? Maybe one of the other [community channels]( The following metrics are updated automatically and provide a quick snapshot of community activity. +

    Loading automatically generated metrics...

    -

    Loading automatically generated metrics...

    diff --git a/js/community-metrics.js b/js/community-metrics.js index 3a2e25e27f7..48674f36091 100644 --- a/js/community-metrics.js +++ b/js/community-metrics.js @@ -1,6 +1,9 @@ -console.log("community-metrics.js loaded"); - (function () { + var REPO_BLURBS = { + core: "Core coupling library and ecosystem.", + tutorials: "Ready-to-run tutorial cases for users and developers.", + }; + function formatNumber(value) { if (typeof value !== "number") { return "n/a"; @@ -31,17 +34,18 @@ console.log("community-metrics.js loaded"); function createCardColumn(title, description, items, linkUrl, linkLabel) { var column = document.createElement("div"); - column.className = "col-md-4 col-sm-6 col-flex"; + column.className = "col-md-12"; var card = document.createElement("div"); - card.className = "panel panel-primary panel-precice full-height"; + card.className = "panel panel-primary panel-precice"; + card.style.marginBottom = "14px"; - var list = ""; - var descriptionHtml = description ? "

    " + description + "

    " : ""; + var list = ""; + var descriptionHtml = description ? "

    " + description + "

    " : ""; var linkHtml = ""; if (linkUrl && linkLabel) { linkHtml = - "

    " + linkLabel + @@ -52,7 +56,7 @@ console.log("community-metrics.js loaded"); "

    " + title + "
    " + - "
    " + + "
    " + descriptionHtml + list + linkHtml + @@ -70,7 +74,7 @@ console.log("community-metrics.js loaded"); "" + - (latestRelease.name || latestRelease.tag_name || "Release") + + (latestRelease.tag_name || latestRelease.name || "Release") + " (" + formatDate(latestRelease.published_at) + ")"; @@ -80,11 +84,10 @@ console.log("community-metrics.js loaded"); return createCardColumn( repo.label, - repo.description || "", + REPO_BLURBS[repo.id] || "", [ createItem("Stars", formatNumber(repo.stars)), createItem("Contributors", formatNumber(repo.contributors)), - createItem("Forks", formatNumber(repo.forks)), createItem("Open issues", formatNumber(repo.open_issues)), createItem("Latest commit", formatDate(repo.latest_commit_at)), createItem("Latest release", latestReleaseValue), @@ -98,7 +101,7 @@ console.log("community-metrics.js loaded"); function createDiscourseCard(discourse) { return createCardColumn( "Discourse forum", - "Community activity snapshot from the preCICE forum.", + "Community activity snapshot.", [ createItem("Users", formatNumber(discourse.users_count)), createItem("Topics", formatNumber(discourse.topics_count)), @@ -141,7 +144,7 @@ console.log("community-metrics.js loaded"); } var row = document.createElement("div"); - row.className = "row equal"; + row.className = "row"; for (var i = 0; i < repositories.length; i += 1) { row.appendChild(createRepositoryCard(repositories[i])); From 22e2178e79c2e2a81c475505edf77123509035b9 Mon Sep 17 00:00:00 2001 From: Flamki <9833ayush@gmail.com> Date: Mon, 23 Feb 2026 04:25:52 +0530 Subject: [PATCH 3/3] Render community metrics as simple extensible list --- js/community-metrics.js | 113 +++++++++++++++++++++------------------- 1 file changed, 60 insertions(+), 53 deletions(-) diff --git a/js/community-metrics.js b/js/community-metrics.js index 48674f36091..a4d05a41786 100644 --- a/js/community-metrics.js +++ b/js/community-metrics.js @@ -28,45 +28,57 @@ }); } - function createItem(label, value) { - return "
  • " + label + ": " + value + "
  • "; + function createMetricItem(label, value) { + var item = document.createElement("li"); + item.innerHTML = "" + label + ": " + value; + return item; } - function createCardColumn(title, description, items, linkUrl, linkLabel) { - var column = document.createElement("div"); - column.className = "col-md-12"; + function createSection(title, description, metrics, linkUrl, linkLabel) { + var section = document.createElement("section"); + section.style.marginBottom = "14px"; + + var heading = document.createElement("h4"); + heading.textContent = title; + heading.style.marginBottom = "4px"; + section.appendChild(heading); + + if (description) { + var blurb = document.createElement("p"); + blurb.className = "text-muted"; + blurb.style.marginBottom = "6px"; + blurb.style.fontSize = "0.95em"; + blurb.textContent = description; + section.appendChild(blurb); + } + + var list = document.createElement("ul"); + list.className = "list-unstyled"; + list.style.marginBottom = "6px"; - var card = document.createElement("div"); - card.className = "panel panel-primary panel-precice"; - card.style.marginBottom = "14px"; + for (var i = 0; i < metrics.length; i += 1) { + var metric = metrics[i]; + list.appendChild(createMetricItem(metric[0], metric[1])); + } + section.appendChild(list); - var list = ""; - var descriptionHtml = description ? "

    " + description + "

    " : ""; - var linkHtml = ""; if (linkUrl && linkLabel) { - linkHtml = - "

    " + - linkLabel + - "  

    "; + var paragraph = document.createElement("p"); + paragraph.className = "no-margin"; + var link = document.createElement("a"); + link.href = linkUrl; + link.target = "_blank"; + link.rel = "noopener noreferrer"; + link.className = "no-external-marker"; + link.innerHTML = linkLabel + "  "; + paragraph.appendChild(link); + section.appendChild(paragraph); } - card.innerHTML = - "
    " + - title + - "
    " + - "
    " + - descriptionHtml + - list + - linkHtml + - "
    "; - - column.appendChild(card); - return column; + return section; } - function createRepositoryCard(repo) { + function createRepositorySection(repo) { var latestRelease = repo.latest_release; var latestReleaseValue = "n/a"; if (latestRelease && latestRelease.url) { @@ -82,33 +94,33 @@ var releaseDownloads = latestRelease ? formatNumber(latestRelease.downloads_count) : "n/a"; - return createCardColumn( + return createSection( repo.label, REPO_BLURBS[repo.id] || "", [ - createItem("Stars", formatNumber(repo.stars)), - createItem("Contributors", formatNumber(repo.contributors)), - createItem("Open issues", formatNumber(repo.open_issues)), - createItem("Latest commit", formatDate(repo.latest_commit_at)), - createItem("Latest release", latestReleaseValue), - createItem("Release downloads", releaseDownloads), + ["Stars", formatNumber(repo.stars)], + ["Contributors", formatNumber(repo.contributors)], + ["Open issues", formatNumber(repo.open_issues)], + ["Latest commit", formatDate(repo.latest_commit_at)], + ["Latest release", latestReleaseValue], + ["Release downloads", releaseDownloads], ], repo.url, "Open repository" ); } - function createDiscourseCard(discourse) { - return createCardColumn( + function createDiscourseSection(discourse) { + return createSection( "Discourse forum", "Community activity snapshot.", [ - createItem("Users", formatNumber(discourse.users_count)), - createItem("Topics", formatNumber(discourse.topics_count)), - createItem("Posts", formatNumber(discourse.posts_count)), - createItem("Active users (30d)", formatNumber(discourse.active_users_30_days)), - createItem("Topics (30d)", formatNumber(discourse.topics_30_days)), - createItem("Posts (30d)", formatNumber(discourse.posts_30_days)), + ["Users", formatNumber(discourse.users_count)], + ["Topics", formatNumber(discourse.topics_count)], + ["Posts", formatNumber(discourse.posts_count)], + ["Active users (30d)", formatNumber(discourse.active_users_30_days)], + ["Topics (30d)", formatNumber(discourse.topics_30_days)], + ["Posts (30d)", formatNumber(discourse.posts_30_days)], ], discourse.url, "Open forum" @@ -143,16 +155,11 @@ throw new Error("Missing metrics data"); } - var row = document.createElement("div"); - row.className = "row"; - + container.innerHTML = ""; for (var i = 0; i < repositories.length; i += 1) { - row.appendChild(createRepositoryCard(repositories[i])); + container.appendChild(createRepositorySection(repositories[i])); } - row.appendChild(createDiscourseCard(discourse)); - - container.innerHTML = ""; - container.appendChild(row); + container.appendChild(createDiscourseSection(discourse)); if (status) { status.textContent =