diff --git a/.github/scripts/accumulate-traffic.py b/.github/scripts/accumulate-traffic.py
index c253441..7ccc692 100644
--- a/.github/scripts/accumulate-traffic.py
+++ b/.github/scripts/accumulate-traffic.py
@@ -10,12 +10,15 @@ import json
import os
import subprocess
import sys
+import urllib.request
from datetime import datetime, timezone
REPO_SLUG = "system_prompts_leaks"
+REPO_FULL = "asgeirtj/system_prompts_leaks"
TRAFFIC_DIR = f"traffic-{REPO_SLUG}"
HISTORY_REFERRERS = "traffic_referrers_history.json"
HISTORY_PATHS = "traffic_paths_history.json"
+HISTORY_STARS = "traffic_stars_history.json"
def load_json(path):
try:
@@ -73,7 +76,48 @@ def accumulate(publish_dir):
print(f"Accumulated: {len(ref_history)} referrer snapshots, {len(path_history)} path snapshots")
return ref_history, path_history
-def build_dashboard(publish_dir, ref_history, path_history):
+def fetch_star_count():
+ """Current stargazers_count via the public REST API (one cheap call)."""
+ req = urllib.request.Request(
+ f"https://api.github.com/repos/{REPO_FULL}",
+ headers={"Accept": "application/vnd.github+json", "User-Agent": "traffic-dashboard"},
+ )
+ token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
+ if token:
+ req.add_header("Authorization", f"Bearer {token}")
+ try:
+ with urllib.request.urlopen(req, timeout=30) as r:
+ return json.load(r).get("stargazers_count")
+ except Exception as e:
+ print(f"Star fetch failed: {e}")
+ return None
+
+def accumulate_stars(publish_dir):
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
+ data_dir = os.path.join(publish_dir, TRAFFIC_DIR)
+
+ star_history = (
+ load_json(os.path.join(data_dir, HISTORY_STARS))
+ or fetch_from_branch(publish_dir, HISTORY_STARS)
+ or []
+ )
+
+ count = fetch_star_count()
+ if count is not None:
+ # one cumulative point per day; refresh today's if it already exists
+ if star_history and star_history[-1]["date"] == today:
+ star_history[-1]["stars"] = count
+ else:
+ star_history.append({"date": today, "stars": count})
+ star_history.sort(key=lambda x: x["date"])
+
+ with open(os.path.join(data_dir, HISTORY_STARS), "w") as f:
+ json.dump(star_history, f, separators=(",", ":"))
+
+ print(f"Accumulated: {len(star_history)} star snapshots (latest={star_history[-1]['stars'] if star_history else 'n/a'})")
+ return star_history
+
+def build_dashboard(publish_dir, ref_history, path_history, star_history):
data_dir = os.path.join(publish_dir, TRAFFIC_DIR)
views = load_json(os.path.join(data_dir, "traffic_views.json"))
clones = load_json(os.path.join(data_dir, "traffic_clones.json"))
@@ -87,6 +131,7 @@ def build_dashboard(publish_dir, ref_history, path_history):
"clones": clones,
"referrer_series": ref_history,
"paths_series": path_history,
+ "stars_series": star_history,
}, separators=(",", ":"))
html = DASHBOARD_TEMPLATE.replace("__DATA_PLACEHOLDER__", embedded)
@@ -162,6 +207,7 @@ DASHBOARD_TEMPLATE = r"""
Top Pages (top 20 — peak 14-day count across all snapshots)
Top Referrers (top 20 — peak 14-day count across all snapshots)
@@ -207,13 +253,27 @@ const sinceStart = new Date(vDays[0].timestamp);
const sinceFmt = sinceStart.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'});
const dayCount = Math.round((Date.now() - sinceStart.getTime()) / 86400000);
-document.getElementById('stats').innerHTML = [
+const starSeries = (DATA.stars_series||[]).slice().sort((a,b)=>a.date.localeCompare(b.date));
+const curStars = starSeries.length ? starSeries[starSeries.length-1].stars : null;
+// stars gained over the trailing 7 days (find the snapshot ~7 days back)
+let starGain7 = null;
+if (starSeries.length > 1) {
+ const cutoff = new Date(starSeries[starSeries.length-1].date); cutoff.setDate(cutoff.getDate()-7);
+ let base = starSeries[0];
+ for (const p of starSeries) { if (new Date(p.date) <= cutoff) base = p; }
+ starGain7 = curStars - base.stars;
+}
+
+const statCards = [
{ l:'Total Views', v:fmt(views.count), s:fmt(views.uniques)+' unique', t:`since ${sinceFmt} · ${dayCount}d` },
{ l:'Total Clones', v:fmt(clones.count), s:fmt(clones.uniques)+' unique' },
{ l:'Daily Avg', v:fmt(avgViews), s:fmt(Math.round(clones.count/cDays.length))+' clones' },
{ l:'Peak Day', v:fmt(peakView.count), s:peakView.timestamp.slice(0,10) },
{ l:'7-Day Trend', v:`
${trend>=0?'+':''}${trend}%`, s:fmt(last7)+' vs '+fmt(prev7) },
-].map(s=>`
${s.l}
${s.v}
${s.s}
${s.t?`
${s.t}
`:''}
`).join('');
+];
+if (curStars != null) statCards.splice(2, 0, { l:'GitHub Stars', v:'★ '+fmt(curStars), s: starGain7!=null ? `
${starGain7>=0?'+':''}${fmt(starGain7)} last 7d` : ' ' });
+
+document.getElementById('stats').innerHTML = statCards.map(s=>`
${s.l}
${s.v}
${s.s}
${s.t?`
${s.t}
`:''}
`).join('');
const baseOpts = { chart:{fontFamily:'Inter,-apple-system,system-ui,sans-serif',background:'transparent',foreColor:isDark?'#94a3b8':'#6b7280',toolbar:{show:true,tools:{download:true,selection:true,zoom:true,zoomin:false,zoomout:false,pan:false,reset:true}}}, theme:{mode:apexTheme()}, grid:{borderColor:isDark?'#1e293b':'#e5e7eb',strokeDashArray:3}, tooltip:{theme:apexTheme()}, };
@@ -237,6 +297,32 @@ new ApexCharts(document.getElementById('viewsChart'), {
legend:{position:'top',horizontalAlign:'left',fontSize:'12px'},
}).render();
+if (starSeries.length) {
+ const cumData = starSeries.map(p=>[new Date(p.date).getTime(), p.stars]);
+ const deltaData = starSeries.map((p,i)=>[new Date(p.date).getTime(), i===0?p.stars:Math.max(0,p.stars-starSeries[i-1].stars)]);
+ new ApexCharts(document.getElementById('starsChart'), {
+ ...baseOpts,
+ chart:{...baseOpts.chart, type:'line', height:320, id:'stars', zoom:{enabled:true,type:'x',autoScaleYaxis:true}},
+ series:[
+ {name:'Total stars', type:'area', data:cumData},
+ {name:'Gained / day', type:'column', data:deltaData},
+ ],
+ colors:['#f59e0b','#6366f1'],
+ fill:{type:['gradient','solid'],gradient:{shadeIntensity:1,opacityFrom:0.35,opacityTo:0.05,stops:[0,95]}},
+ stroke:{curve:'smooth',width:[2.5,0]},
+ plotOptions:{bar:{borderRadius:2,columnWidth:'55%'}},
+ xaxis:{type:'datetime',labels:{datetimeUTC:false}},
+ yaxis:[
+ {seriesName:'Total stars',labels:{formatter:v=>v>=1000?(v/1000).toFixed(1)+'k':Math.round(v)}},
+ {seriesName:'Gained / day',opposite:true,labels:{formatter:v=>Math.round(v)}},
+ ],
+ dataLabels:{enabled:false},
+ markers:{size:0,hover:{size:5}},
+ legend:{position:'top',horizontalAlign:'left',fontSize:'12px'},
+ tooltip:{shared:true,x:{format:'MMM d, yyyy'}},
+ }).render();
+}
+
new ApexCharts(document.getElementById('clonesChart'), {
...baseOpts,
chart:{...baseOpts.chart, type:'bar', height:240, stacked:false},
@@ -366,4 +452,5 @@ if __name__ == "__main__":
publish_dir = sys.argv[1]
print(f"Processing traffic data in {publish_dir}")
ref_history, path_history = accumulate(publish_dir)
- build_dashboard(publish_dir, ref_history, path_history)
+ star_history = accumulate_stars(publish_dir)
+ build_dashboard(publish_dir, ref_history, path_history, star_history)
diff --git a/.github/workflows/traffic-to-badge.yml b/.github/workflows/traffic-to-badge.yml
index 1d1617a..05bc9f0 100644
--- a/.github/workflows/traffic-to-badge.yml
+++ b/.github/workflows/traffic-to-badge.yml
@@ -40,6 +40,8 @@ jobs:
logo: github
- name: Accumulate history and build dashboard
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 .github/scripts/accumulate-traffic.py "${{ steps.traffic.outputs.traffic_path }}"
- name: Deploy to traffic branch