diff --git a/.github/workflows/update-download-metrics.yml b/.github/workflows/update-download-metrics.yml index 4a5ae24..720c63b 100644 --- a/.github/workflows/update-download-metrics.yml +++ b/.github/workflows/update-download-metrics.yml @@ -1,4 +1,4 @@ -name: Update Download Metrics +name: Update Download & Traffic Metrics on: release: @@ -25,11 +25,11 @@ jobs: with: python-version: "3.12" - - name: Update README metrics and chart + - name: Update README metrics and traffic chart env: GH_TOKEN: ${{ secrets.METRIC_TOKEN }} GITHUB_TOKEN: ${{ github.token }} - run: scripts/update_download_metrics.py --require-clone-api + run: scripts/update_download_metrics.py --require-traffic-api - name: Commit and push if changed run: | diff --git a/Neon Vision Editor.xcodeproj/project.pbxproj b/Neon Vision Editor.xcodeproj/project.pbxproj index ccb7e85..024373f 100644 --- a/Neon Vision Editor.xcodeproj/project.pbxproj +++ b/Neon Vision Editor.xcodeproj/project.pbxproj @@ -361,7 +361,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 497; + CURRENT_PROJECT_VERSION = 498; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; @@ -444,7 +444,7 @@ CODE_SIGNING_ALLOWED = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 497; + CURRENT_PROJECT_VERSION = 498; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = CS727NF72U; ENABLE_APP_SANDBOX = YES; diff --git a/README.md b/README.md index deac524..21a0236 100644 --- a/README.md +++ b/README.md @@ -151,14 +151,13 @@

Styled line chart shows per-release totals with 14-day traffic counters for clones and views.

- Git clones (14d) - GitHub views (14d) + Unique cloners (14d) + Unique visitors (14d)

- Clone snapshot (UTC) - View snapshot (UTC) + Clone snapshot (UTC) + View snapshot (UTC)

- ## Project Docs - Release history: [`CHANGELOG.md`](CHANGELOG.md) diff --git a/docs/images/release-download-trend.svg b/docs/images/release-download-trend.svg index 9a0cadf..ada80aa 100644 --- a/docs/images/release-download-trend.svg +++ b/docs/images/release-download-trend.svg @@ -83,17 +83,17 @@ Release trend line with highlighted points Repository Traffic (last 14 days) - Clones: 2624 + Unique cloners: 413 - - Views: 865 + + Unique visitors: 159 - + 0 - 1400 - 2800 - Shared scale: 0 to 2800 events in the last 14 days. + 400 + 800 + Shared scale: 0 to 800 events in the last 14 days. diff --git a/scripts/ci/release_preflight.sh b/scripts/ci/release_preflight.sh index bf01880..2c147cf 100755 --- a/scripts/ci/release_preflight.sh +++ b/scripts/ci/release_preflight.sh @@ -29,13 +29,13 @@ grep -nE "^> Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null grep -nE "^- Latest release: \\*\\*${TAG}\\*\\*\\r?$" README.md >/dev/null grep -nE "^\\| .*\\(https://github\\.com/h3pdesign/Neon-Vision-Editor/releases/tag/${TAG}\\) \\|" README.md >/dev/null -echo "Validating README download metrics freshness..." +echo "Validating README download + traffic metrics freshness..." if gh release view "$TAG" >/dev/null 2>&1; then is_draft="$(gh release view "$TAG" --json isDraft --jq '.isDraft' 2>/dev/null || echo "false")" if [[ "$is_draft" == "true" ]]; then echo "Skipping metrics freshness check: ${TAG} exists as a draft release." else - scripts/update_download_metrics.py --check + scripts/update_download_metrics.py --check --require-traffic-api fi else echo "Skipping metrics freshness check: ${TAG} is not published on GitHub releases yet." diff --git a/scripts/update_download_metrics.py b/scripts/update_download_metrics.py index b0ec114..423b2fb 100755 --- a/scripts/update_download_metrics.py +++ b/scripts/update_download_metrics.py @@ -154,8 +154,11 @@ def fetch_clone_traffic() -> tuple[list[ClonePoint], int | None, dt.datetime | N points.append(ClonePoint(timestamp=ts, count=count)) points.sort(key=lambda p: p.timestamp) + unique_total = payload.get("uniques") total_count = payload.get("count") latest_timestamp = points[-1].timestamp if points else None + if isinstance(unique_total, int): + return points, unique_total, latest_timestamp if isinstance(total_count, int): return points, total_count, latest_timestamp return points, None, latest_timestamp @@ -201,8 +204,11 @@ def fetch_view_traffic() -> tuple[list[ViewPoint], int | None, dt.datetime | Non points.append(ViewPoint(timestamp=ts, count=count)) points.sort(key=lambda p: p.timestamp) + unique_total = payload.get("uniques") total_count = payload.get("count") latest_timestamp = points[-1].timestamp if points else None + if isinstance(unique_total, int): + return points, unique_total, latest_timestamp if isinstance(total_count, int): return points, total_count, latest_timestamp return points, None, latest_timestamp @@ -299,10 +305,10 @@ def generate_svg(points: list[ReleasePoint], clone_total: int, view_total: int, mid_x = panel_left + (track_width * 0.5) clone_panel.extend( [ - f' Clones: {clone_total}', + f' Unique cloners: {clone_total}', f' ', f' ', - f' Views: {view_total}', + f' Unique visitors: {view_total}', f' ', f' ', f' ', @@ -398,14 +404,6 @@ def parse_existing_clone_total(content: str) -> int | None: return None -def parse_existing_clone_snapshot(content: str) -> str | None: - match = re.search(r"Clone data snapshot \(UTC\): ([^<]+)\.", content) - if not match: - return None - value = match.group(1).strip() - return value if value else None - - def parse_existing_view_total(content: str) -> int | None: match = re.search(r"GitHub views \(last \d+ days\): (\d+)\.", content) if not match: @@ -416,14 +414,6 @@ def parse_existing_view_total(content: str) -> int | None: return None -def parse_existing_view_snapshot(content: str) -> str | None: - match = re.search(r"View data snapshot \(UTC\): ([^<]+)\.", content) - if not match: - return None - value = match.group(1).strip() - return value if value else None - - def shields_badge(label: str, message: str, color: str, style: str = "for-the-badge") -> str: query = urllib.parse.urlencode( { @@ -511,29 +501,28 @@ def update_readme( '

Styled line chart shows per-release totals with 14-day traffic counters for clones and views.

', content, ) + content = re.sub( + r'(?s)

\s*(?:Git clones|Unique cloners) \(14d\)\s*' + r'

\s*Clone snapshot \(UTC\)\s*', + "", + content, + ) traffic_badges = ( "

\n" - f" \"Git\n" - f" \"GitHub\n" + f" \"Unique\n" + f" \"Unique\n" "

\n" "

\n" f" \"Clone\n" f" \"View\n" "

" ) - old_badges_pattern = ( - r'(?s)

\s*Git clones \(14d\)\s*' - r'

\s*Clone snapshot \(UTC\)' + content = content.replace( + '

Styled line chart shows per-release totals with 14-day traffic counters for clones and views.

', + '

Styled line chart shows per-release totals with 14-day traffic counters for clones and views.

\n' + + traffic_badges, + 1, ) - if re.search(old_badges_pattern, content): - content = re.sub(old_badges_pattern, traffic_badges, content) - else: - content = content.replace( - '

Styled line chart shows per-release totals with 14-day traffic counters for clones and views.

', - '

Styled line chart shows per-release totals with 14-day traffic counters for clones and views.

\n' - + traffic_badges, - 1, - ) content = re.sub( r"(?m)^> Latest release: \*\*.*\*\*$", f"> Latest release: **{latest_tag}**", @@ -562,16 +551,23 @@ def total_downloads_for_scale(points: list[ReleasePoint]) -> int: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Refresh README download metrics and chart.") parser.add_argument("--check", action="store_true", help="Fail if files are not up-to-date.") + parser.add_argument( + "--require-traffic-api", + action="store_true", + help="Fail if clone/view traffic API data cannot be fetched.", + ) parser.add_argument( "--require-clone-api", action="store_true", - help="Fail if clone traffic API data cannot be fetched.", + help=argparse.SUPPRESS, ) return parser.parse_args() def main() -> int: args = parse_args() + require_traffic_api = args.require_traffic_api or args.require_clone_api + update_timestamp_utc = dt.datetime.now(dt.UTC).strftime("%Y-%m-%d %H:%M") releases = fetch_releases() clone_points, clone_total_api, clone_latest_timestamp = fetch_clone_traffic() view_points, view_total_api, view_latest_timestamp = fetch_view_traffic() @@ -584,16 +580,14 @@ def main() -> int: readme_before = README.read_text(encoding="utf-8") existing_clone_total = parse_existing_clone_total(readme_before) - existing_clone_snapshot = parse_existing_clone_snapshot(readme_before) existing_view_total = parse_existing_view_total(readme_before) - existing_view_snapshot = parse_existing_view_snapshot(readme_before) - if args.require_clone_api and (clone_total_api is None or clone_latest_timestamp is None): + if require_traffic_api and (clone_total_api is None or clone_latest_timestamp is None): print( "Clone traffic API unavailable. Refusing to reuse stale README clone metrics.", file=sys.stderr, ) return 1 - if args.require_clone_api and (view_total_api is None or view_latest_timestamp is None): + if require_traffic_api and (view_total_api is None or view_latest_timestamp is None): print( "View traffic API unavailable. Refusing to reuse stale README view metrics.", file=sys.stderr, @@ -605,20 +599,14 @@ def main() -> int: file=sys.stderr, ) clone_total = clone_total_api if clone_total_api is not None else (existing_clone_total or 0) - if clone_latest_timestamp is not None: - clone_snapshot_utc = clone_latest_timestamp.astimezone(dt.UTC).strftime("%Y-%m-%d %H:%M") - else: - clone_snapshot_utc = existing_clone_snapshot or f"{snapshot_date} 00:00" + clone_snapshot_utc = update_timestamp_utc if view_total_api is None: print( "Warning: view traffic API unavailable; reusing existing README view total.", file=sys.stderr, ) view_total = view_total_api if view_total_api is not None else (existing_view_total or 0) - if view_latest_timestamp is not None: - view_snapshot_utc = view_latest_timestamp.astimezone(dt.UTC).strftime("%Y-%m-%d %H:%M") - else: - view_snapshot_utc = existing_view_snapshot or f"{snapshot_date} 00:00" + view_snapshot_utc = update_timestamp_utc svg = generate_svg(trend_points, clone_total, view_total, snapshot_date) readme_after = update_readme( readme_before,