docs(metrics): add traffic badges and views-vs-clones dual bar

This commit is contained in:
h3p 2026-03-11 21:59:41 +01:00
parent 20641de5c2
commit e9366c9456
4 changed files with 230 additions and 72 deletions

View file

@ -361,7 +361,7 @@
CODE_SIGNING_ALLOWED = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 495;
CURRENT_PROJECT_VERSION = 496;
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 = 495;
CURRENT_PROJECT_VERSION = 496;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = CS727NF72U;
ENABLE_APP_SANDBOX = YES;

View file

@ -143,16 +143,21 @@
<img alt="v0.5.3 Downloads" src="https://img.shields.io/github/downloads/h3pdesign/Neon-Vision-Editor/v0.5.3/total?style=for-the-badge&label=v0.5.3&color=22C55E">
</p>
<p align="center"><strong>Release Download + Clone Trend</strong></p>
<p align="center"><strong>Release Download + Traffic Trend</strong></p>
<p align="center">
<img src="docs/images/release-download-trend.svg" alt="GitHub release downloads trend chart" width="100%">
</p>
<p align="center"><em>Styled line chart shows per-release totals plus a scaled 14-day git clone volume bar.</em></p>
<p align="center">Git clones (last 14 days): <strong>2624</strong>.</p>
<p align="center">Clone data snapshot (UTC): <strong>2026-03-09 00:00</strong>.</p>
<p align="center">Snapshot total downloads: <strong>623</strong> across releases.</p>
<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>
<p align="center">
<img alt="Git clones (14d)" src="https://img.shields.io/badge/Git%20clones%20%2814d%29-2624-7C3AED?style=for-the-badge">
<img alt="GitHub views (14d)" src="https://img.shields.io/badge/GitHub%20views%20%2814d%29-865-0EA5E9?style=for-the-badge">
</p>
<p align="center">
<img alt="Clone snapshot (UTC)" src="https://img.shields.io/badge/Clone%20snapshot%20%28UTC%29-2026-03-09%2000%3A00-334155?style=flat-square">
<img alt="View snapshot (UTC)" src="https://img.shields.io/badge/View%20snapshot%20%28UTC%29-2026-03-09%2000%3A00-334155?style=flat-square">
</p>
## Project Docs

View file

@ -1,6 +1,6 @@
<svg width="1200" height="560" viewBox="0 0 1200 560" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
<title id="title">GitHub Release Downloads and Clone Trend</title>
<desc id="desc">Line chart of release downloads with highlighted points and a scaled 14-day git clone volume bar.</desc>
<title id="title">GitHub Release Downloads and Traffic Trend</title>
<desc id="desc">Line chart of release downloads with 14-day traffic bars for clones and views.</desc>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1200" y2="560" gradientUnits="userSpaceOnUse">
<stop stop-color="#061423"/>
@ -15,6 +15,10 @@
<stop stop-color="#7C3AED"/>
<stop offset="1" stop-color="#C084FC"/>
</linearGradient>
<linearGradient id="viewFill" x1="86" y1="482" x2="1110" y2="482" gradientUnits="userSpaceOnUse">
<stop stop-color="#0EA5E9"/>
<stop offset="1" stop-color="#7DD3FC"/>
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge>
@ -78,14 +82,18 @@
<text x="776" y="56" fill="#D7F7FF" font-size="15" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Release trend line with highlighted points</text>
<rect x="58" y="390" width="1084" height="132" rx="12" fill="#0A1A2B" stroke="#2A4762" stroke-width="1"/>
<text x="84" y="420" fill="#E6F3FF" font-size="18" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Git Clones (last 14 days): 2624</text>
<rect x="86" y="450" width="1024" height="36" rx="10" fill="#15263A" stroke="#2B4255" stroke-width="1"/>
<line x1="86" y1="442" x2="86" y2="494" stroke="#436280" stroke-width="1"/>
<line x1="598.0" y1="442" x2="598.0" y2="494" stroke="#436280" stroke-width="1"/>
<line x1="1110" y1="442" x2="1110" y2="494" stroke="#436280" stroke-width="1"/>
<rect x="86" y="450" width="959.6" height="36" rx="10" fill="url(#cloneFill)"/>
<text x="82" y="438" text-anchor="start" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">0</text>
<text x="598.0" y="438" text-anchor="middle" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">1400</text>
<text x="1114" y="438" text-anchor="end" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">2800</text>
<text x="86" y="510" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Scale: 0 to 2800 clones in the last 14 days.</text>
<text x="84" y="420" fill="#E6F3FF" font-size="18" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Repository Traffic (last 14 days)</text>
<text x="86" y="434" fill="#C4B5FD" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Clones: 2624</text>
<rect x="86" y="442" width="1024" height="18" rx="9" fill="#15263A" stroke="#2B4255" stroke-width="1"/>
<rect x="86" y="442" width="959.6" height="18" rx="9" fill="url(#cloneFill)"/>
<text x="86" y="466" fill="#7DD3FC" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Views: 865</text>
<rect x="86" y="474" width="1024" height="18" rx="9" fill="#15263A" stroke="#2B4255" stroke-width="1"/>
<rect x="86" y="474" width="316.3" height="18" rx="9" fill="url(#viewFill)"/>
<line x1="86" y1="426" x2="86" y2="500" stroke="#436280" stroke-width="1"/>
<line x1="598.0" y1="426" x2="598.0" y2="500" stroke="#436280" stroke-width="1"/>
<line x1="1110" y1="426" x2="1110" y2="500" stroke="#436280" stroke-width="1"/>
<text x="84" y="512" text-anchor="start" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">0</text>
<text x="598.0" y="512" text-anchor="middle" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">1400</text>
<text x="1112" y="512" text-anchor="end" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">2800</text>
<text x="86" y="526" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Shared scale: 0 to 2800 events in the last 14 days.</text>
</svg>

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -18,6 +18,7 @@ import re
import subprocess
import sys
import urllib.request
import urllib.parse
from dataclasses import dataclass
@ -28,6 +29,7 @@ OWNER = "h3pdesign"
REPO = "Neon-Vision-Editor"
API_URL = f"https://api.github.com/repos/{OWNER}/{REPO}/releases?per_page=100"
CLONES_API_URL = f"https://api.github.com/repos/{OWNER}/{REPO}/traffic/clones"
VIEWS_API_URL = f"https://api.github.com/repos/{OWNER}/{REPO}/traffic/views"
CLONES_WINDOW_DAYS = 14
@ -44,6 +46,12 @@ class ClonePoint:
count: int
@dataclass(frozen=True)
class ViewPoint:
timestamp: dt.datetime
count: int
def github_api_get(url: str) -> object:
base_headers = {
"Accept": "application/vnd.github+json",
@ -153,6 +161,53 @@ def fetch_clone_traffic() -> tuple[list[ClonePoint], int | None, dt.datetime | N
return points, None, latest_timestamp
def fetch_view_traffic() -> tuple[list[ViewPoint], int | None, dt.datetime | None]:
try:
payload = github_api_get(VIEWS_API_URL)
except Exception:
payload = None
if payload is None:
# Local fallback: reuse authenticated gh CLI when direct API auth is unavailable.
try:
out = subprocess.run(
["gh", "api", f"repos/{OWNER}/{REPO}/traffic/views"],
check=True,
capture_output=True,
text=True,
timeout=20,
)
payload = json.loads(out.stdout)
except Exception:
return [], None, None
if not isinstance(payload, dict):
return [], None, None
raw_points = payload.get("views", [])
if not isinstance(raw_points, list):
raw_points = []
points: list[ViewPoint] = []
for point in raw_points:
if not isinstance(point, dict):
continue
ts_raw = point.get("timestamp")
count = point.get("count", 0)
if not isinstance(ts_raw, str) or not isinstance(count, int):
continue
try:
ts = dt.datetime.fromisoformat(ts_raw.replace("Z", "+00:00"))
except ValueError:
continue
points.append(ViewPoint(timestamp=ts, count=count))
points.sort(key=lambda p: p.timestamp)
total_count = payload.get("count")
latest_timestamp = points[-1].timestamp if points else None
if isinstance(total_count, int):
return points, total_count, latest_timestamp
return points, None, latest_timestamp
def y_top(max_value: int, ticks: int = 4) -> int:
if max_value <= 0:
return ticks
@ -162,7 +217,7 @@ def y_top(max_value: int, ticks: int = 4) -> int:
return step * ticks
def generate_svg(points: list[ReleasePoint], clone_total: int, snapshot_date: str) -> str:
def generate_svg(points: list[ReleasePoint], clone_total: int, view_total: int, snapshot_date: str) -> str:
width = 1200
height = 560
left = 130
@ -226,35 +281,43 @@ def generate_svg(points: list[ReleasePoint], clone_total: int, snapshot_date: st
clone_panel: list[str] = [
' <rect x="58" y="390" width="1084" height="132" rx="12" fill="#0A1A2B" stroke="#2A4762" stroke-width="1"/>',
f' <text x="84" y="420" fill="#E6F3FF" font-size="18" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Git Clones (last {CLONES_WINDOW_DAYS} days): {clone_total}</text>',
f' <text x="84" y="420" fill="#E6F3FF" font-size="18" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif" font-weight="600">Repository Traffic (last {CLONES_WINDOW_DAYS} days)</text>',
]
panel_left = 86
panel_right = 1110
bar_top = 450
bar_bottom = 486
clone_bar_top = 442
clone_bar_bottom = 460
view_bar_top = 474
view_bar_bottom = 492
track_width = panel_right - panel_left
clone_scale_max = max(100, y_top(max(1, clone_total), ticks=4))
fill_ratio = min(1.0, clone_total / clone_scale_max)
fill_width = max(8.0, track_width * fill_ratio)
mid_value = clone_scale_max // 2
traffic_scale_max = max(100, y_top(max(1, clone_total, view_total), ticks=4))
clone_fill_ratio = min(1.0, clone_total / traffic_scale_max)
clone_fill_width = max(8.0, track_width * clone_fill_ratio)
view_fill_ratio = min(1.0, view_total / traffic_scale_max)
view_fill_width = max(8.0, track_width * view_fill_ratio)
mid_value = traffic_scale_max // 2
mid_x = panel_left + (track_width * 0.5)
clone_panel.extend(
[
f' <rect x="{panel_left}" y="{bar_top}" width="{track_width}" height="{bar_bottom - bar_top}" rx="10" fill="#15263A" stroke="#2B4255" stroke-width="1"/>',
f' <line x1="{panel_left}" y1="{bar_top - 8}" x2="{panel_left}" y2="{bar_bottom + 8}" stroke="#436280" stroke-width="1"/>',
f' <line x1="{mid_x:.1f}" y1="{bar_top - 8}" x2="{mid_x:.1f}" y2="{bar_bottom + 8}" stroke="#436280" stroke-width="1"/>',
f' <line x1="{panel_right}" y1="{bar_top - 8}" x2="{panel_right}" y2="{bar_bottom + 8}" stroke="#436280" stroke-width="1"/>',
f' <rect x="{panel_left}" y="{bar_top}" width="{fill_width:.1f}" height="{bar_bottom - bar_top}" rx="10" fill="url(#cloneFill)"/>',
f' <text x="{panel_left - 4}" y="{bar_top - 12}" text-anchor="start" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">0</text>',
f' <text x="{mid_x:.1f}" y="{bar_top - 12}" text-anchor="middle" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">{mid_value}</text>',
f' <text x="{panel_right + 4}" y="{bar_top - 12}" text-anchor="end" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">{clone_scale_max}</text>',
f' <text x="{panel_left}" y="{bar_bottom + 24}" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Scale: 0 to {clone_scale_max} clones in the last {CLONES_WINDOW_DAYS} days.</text>',
f' <text x="{panel_left}" y="{clone_bar_top - 8}" fill="#C4B5FD" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Clones: {clone_total}</text>',
f' <rect x="{panel_left}" y="{clone_bar_top}" width="{track_width}" height="{clone_bar_bottom - clone_bar_top}" rx="9" fill="#15263A" stroke="#2B4255" stroke-width="1"/>',
f' <rect x="{panel_left}" y="{clone_bar_top}" width="{clone_fill_width:.1f}" height="{clone_bar_bottom - clone_bar_top}" rx="9" fill="url(#cloneFill)"/>',
f' <text x="{panel_left}" y="{view_bar_top - 8}" fill="#7DD3FC" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Views: {view_total}</text>',
f' <rect x="{panel_left}" y="{view_bar_top}" width="{track_width}" height="{view_bar_bottom - view_bar_top}" rx="9" fill="#15263A" stroke="#2B4255" stroke-width="1"/>',
f' <rect x="{panel_left}" y="{view_bar_top}" width="{view_fill_width:.1f}" height="{view_bar_bottom - view_bar_top}" rx="9" fill="url(#viewFill)"/>',
f' <line x1="{panel_left}" y1="{clone_bar_top - 16}" x2="{panel_left}" y2="{view_bar_bottom + 8}" stroke="#436280" stroke-width="1"/>',
f' <line x1="{mid_x:.1f}" y1="{clone_bar_top - 16}" x2="{mid_x:.1f}" y2="{view_bar_bottom + 8}" stroke="#436280" stroke-width="1"/>',
f' <line x1="{panel_right}" y1="{clone_bar_top - 16}" x2="{panel_right}" y2="{view_bar_bottom + 8}" stroke="#436280" stroke-width="1"/>',
f' <text x="{panel_left - 2}" y="{view_bar_bottom + 20}" text-anchor="start" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">0</text>',
f' <text x="{mid_x:.1f}" y="{view_bar_bottom + 20}" text-anchor="middle" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">{mid_value}</text>',
f' <text x="{panel_right + 2}" y="{view_bar_bottom + 20}" text-anchor="end" fill="#9CC3E6" font-size="12" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">{traffic_scale_max}</text>',
f' <text x="{panel_left}" y="{view_bar_bottom + 34}" fill="#9CC3E6" font-size="13" font-family="SF Pro Text, -apple-system, BlinkMacSystemFont, Segoe UI, sans-serif">Shared scale: 0 to {traffic_scale_max} events in the last {CLONES_WINDOW_DAYS} days.</text>',
]
)
return """<svg width="1200" height="560" viewBox="0 0 1200 560" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-labelledby="title desc">
<title id="title">GitHub Release Downloads and Clone Trend</title>
<desc id="desc">Line chart of release downloads with highlighted points and a scaled 14-day git clone volume bar.</desc>
<title id="title">GitHub Release Downloads and Traffic Trend</title>
<desc id="desc">Line chart of release downloads with 14-day traffic bars for clones and views.</desc>
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1200" y2="560" gradientUnits="userSpaceOnUse">
<stop stop-color="#061423"/>
@ -269,6 +332,10 @@ def generate_svg(points: list[ReleasePoint], clone_total: int, snapshot_date: st
<stop stop-color="#7C3AED"/>
<stop offset="1" stop-color="#C084FC"/>
</linearGradient>
<linearGradient id="viewFill" x1="86" y1="482" x2="1110" y2="482" gradientUnits="userSpaceOnUse">
<stop stop-color="#0EA5E9"/>
<stop offset="1" stop-color="#7DD3FC"/>
</linearGradient>
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="4" result="blur"/>
<feMerge>
@ -339,12 +406,40 @@ def parse_existing_clone_snapshot(content: str) -> str | None:
return value if value else None
def parse_existing_view_total(content: str) -> int | None:
match = re.search(r"GitHub views \(last \d+ days\): <strong>(\d+)</strong>\.", content)
if not match:
return None
try:
return int(match.group(1))
except ValueError:
return None
def parse_existing_view_snapshot(content: str) -> str | None:
match = re.search(r"View data snapshot \(UTC\): <strong>([^<]+)</strong>\.", 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:
encoded_label = urllib.parse.quote(label, safe="")
encoded_message = urllib.parse.quote(message, safe="")
return (
f"https://img.shields.io/badge/{encoded_label}-{encoded_message}-{color}?style={style}"
)
def update_readme(
content: str,
latest_tag: str,
total_downloads: int,
clone_total: int,
clone_snapshot_utc: str,
view_total: int,
view_snapshot_utc: str,
today: str,
) -> str:
release_badge_line = (
@ -358,56 +453,83 @@ def update_readme(
content,
)
content = re.sub(
r'<p align="center">Snapshot total downloads: <strong>\d+</strong> across releases\.</p>',
f'<p align="center">Snapshot total downloads: <strong>{total_downloads}</strong> across releases.</p>',
r'(?m)^<p align="center">Git clones \(last \d+ days\): <strong>\d+</strong>\.</p>\s*$',
"",
content,
)
content = re.sub(
r'(?m)^<p align="center">GitHub views \(last \d+ days\): <strong>\d+</strong>\.</p>\s*$',
"",
content,
)
content = re.sub(
r'(?m)^<p align="center">Clone data snapshot \(UTC\): <strong>[^<]+</strong>\.</p>\s*$',
"",
content,
)
content = re.sub(
r'(?m)^<p align="center">View data snapshot \(UTC\): <strong>[^<]+</strong>\.</p>\s*$',
"",
content,
)
content = re.sub(
r'(?m)^<p align="center">Snapshot total downloads: <strong>\d+</strong> across releases\.</p>\s*$',
"",
content,
)
clone_line = f'<p align="center">Git clones (last {CLONES_WINDOW_DAYS} days): <strong>{clone_total}</strong>.</p>'
if re.search(r'<p align="center">Git clones \(last \d+ days\): <strong>\d+</strong>\.</p>', content):
content = re.sub(
r'<p align="center">Git clones \(last \d+ days\): <strong>\d+</strong>\.</p>',
clone_line,
content,
)
else:
content = content.replace(
'<p align="center">Snapshot total downloads: <strong>',
clone_line + "\n<p align=\"center\">Snapshot total downloads: <strong>",
1,
)
clone_snapshot_line = f'<p align="center">Clone data snapshot (UTC): <strong>{clone_snapshot_utc}</strong>.</p>'
if re.search(r'<p align="center">Clone data snapshot \(UTC\): <strong>[^<]+</strong>\.</p>', content):
content = re.sub(
r'<p align="center">Clone data snapshot \(UTC\): <strong>[^<]+</strong>\.</p>',
clone_snapshot_line,
content,
)
else:
content = content.replace(
clone_line,
clone_line + "\n" + clone_snapshot_line,
1,
)
content = re.sub(
r'(?m)^<p align="center"><strong>Release Download Trend</strong></p>$',
'<p align="center"><strong>Release Download + Clone Trend</strong></p>',
'<p align="center"><strong>Release Download + Traffic Trend</strong></p>',
content,
)
content = re.sub(
r'(?m)^<p align="center"><strong>Release Download \+ Clone Trend</strong></p>$',
'<p align="center"><strong>Release Download + Traffic Trend</strong></p>',
content,
)
content = re.sub(
r'(?m)^<p align="center"><em>Styled line chart with highlighted points shows per-release totals and trend direction\.</em></p>$',
'<p align="center"><em>Styled line chart shows per-release totals plus a scaled 14-day git clone volume bar.</em></p>',
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
content,
)
content = re.sub(
r'(?m)^<p align="center"><em>Styled line chart shows per-release totals plus a 14-day git clone sparkline\.</em></p>$',
'<p align="center"><em>Styled line chart shows per-release totals plus a scaled 14-day git clone volume bar.</em></p>',
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
content,
)
content = re.sub(
r'(?m)^<p align="center"><em>Styled line chart shows per-release totals plus a 14-day git clone volume strip\.</em></p>$',
'<p align="center"><em>Styled line chart shows per-release totals plus a scaled 14-day git clone volume bar.</em></p>',
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
content,
)
content = re.sub(
r'(?m)^<p align="center"><em>Styled line chart shows per-release totals plus a scaled 14-day git clone volume bar\.</em></p>$',
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
content,
)
traffic_badges = (
"<p align=\"center\">\n"
f" <img alt=\"Git clones (14d)\" src=\"{shields_badge('Git clones (14d)', str(clone_total), '7C3AED')}\">\n"
f" <img alt=\"GitHub views (14d)\" src=\"{shields_badge('GitHub views (14d)', str(view_total), '0EA5E9')}\">\n"
"</p>\n"
"<p align=\"center\">\n"
f" <img alt=\"Clone snapshot (UTC)\" src=\"{shields_badge('Clone snapshot (UTC)', clone_snapshot_utc, '334155', style='flat-square')}\">\n"
f" <img alt=\"View snapshot (UTC)\" src=\"{shields_badge('View snapshot (UTC)', view_snapshot_utc, '334155', style='flat-square')}\">\n"
"</p>"
)
old_badges_pattern = (
r'(?s)<p align="center">\s*<img alt="Git clones \(14d\)".*?</p>\s*'
r'<p align="center">\s*<img alt="Clone snapshot \(UTC\)".*?</p>'
)
if re.search(old_badges_pattern, content):
content = re.sub(old_badges_pattern, traffic_badges, content)
else:
content = content.replace(
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>',
'<p align="center"><em>Styled line chart shows per-release totals with 14-day traffic counters for clones and views.</em></p>\n'
+ traffic_badges,
1,
)
content = re.sub(
r"(?m)^> Latest release: \*\*.*\*\*$",
f"> Latest release: **{latest_tag}**",
@ -425,6 +547,7 @@ def update_readme(
)
if f"label={latest_tag}" not in content:
raise RuntimeError("README download badge replacement failed.")
content = re.sub(r"\n</p>\n{3,}## Project Docs", "\n</p>\n\n## Project Docs", content)
return content
@ -447,6 +570,7 @@ def main() -> int:
args = parse_args()
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()
releases_desc = sorted(releases, key=lambda r: r.published_at, reverse=True)
latest = releases_desc[0]
total_downloads = sum(r.downloads for r in releases_desc)
@ -457,12 +581,20 @@ 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):
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):
print(
"View traffic API unavailable. Refusing to reuse stale README view metrics.",
file=sys.stderr,
)
return 1
if clone_total_api is None:
print(
"Warning: clone traffic API unavailable; reusing existing README clone total.",
@ -473,13 +605,25 @@ def main() -> int:
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"
svg = generate_svg(trend_points, clone_total, snapshot_date)
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"
svg = generate_svg(trend_points, clone_total, view_total, snapshot_date)
readme_after = update_readme(
readme_before,
latest_tag=latest.tag,
total_downloads=total_downloads,
clone_total=clone_total,
clone_snapshot_utc=clone_snapshot_utc,
view_total=view_total,
view_snapshot_utc=view_snapshot_utc,
today=snapshot_date,
)
@ -498,7 +642,8 @@ def main() -> int:
print(
f"Updated metrics: latest={latest.tag} ({latest.downloads}) total={total_downloads} "
f"clones14d={clone_total} points={len(trend_points)} clone_points={len(clone_points)} "
f"clone_snapshot_utc={clone_snapshot_utc}"
f"clone_snapshot_utc={clone_snapshot_utc} views14d={view_total} "
f"view_points={len(view_points)} view_snapshot_utc={view_snapshot_utc}"
)
return 0