Skip to content

Commit 4699145

Browse files
pirateclaude
andcommitted
Stop auto-re-mining; persist deployed dashboards across runs
Three coordinated changes so a dashboard, once built, stays valid indefinitely unless explicitly refreshed: 1. CI: actions/cache for cloudflare/public/*.html across runs (single shared key) — wrangler deploy replaces the assets dir, so without this each single-user run would wipe every other user's deployed HTML. With this, runs restore the full snapshot, add/refresh their own user, and write back the union. 2. CI: cron / push runs skip users that already have a deployed HTML. Only single-user dispatches (with inputs.user set) ever re-mine an existing user. 3. Worker: /api/refresh checks if /<user>.html exists via the ASSETS binding and returns {status: "already_deployed"} unless ?force=1 is passed. Loading page reloads on this status. 4. Template: footer "↻ Refresh stats" button calls /api/refresh?user=X&force=1 — the only path to re-mining an already-deployed user. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 082b5d6 commit 4699145

3 files changed

Lines changed: 125 additions & 8 deletions

File tree

.github/workflows/mine-and-deploy.yml

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@ jobs:
5555
stats-cache-v1-${{ inputs.user || 'full' }}-
5656
stats-cache-v1-
5757
58+
# Persist deployed user HTMLs across runs so that single-user
59+
# mining doesn't wipe other users from the CF bucket when wrangler
60+
# replaces the assets dir on deploy. Single shared key — every run
61+
# restores the latest snapshot of all deployed dashboards, adds /
62+
# refreshes its own user(s), and writes back the full set.
63+
- name: Restore deployed dashboards
64+
uses: actions/cache@v4
65+
with:
66+
path: |
67+
cloudflare/public/*.html
68+
key: deployed-htmls-v1-${{ github.run_id }}
69+
restore-keys: |
70+
deployed-htmls-v1-
71+
5872
- name: Install gh CLI
5973
run: |
6074
type -p gh >/dev/null || (
@@ -75,7 +89,7 @@ jobs:
7589
mkdir -p cloudflare/public
7690
# Build the list of users we'll mine THIS run.
7791
if [ -n "$INPUT_USER" ]; then
78-
echo "Single-user mine: $INPUT_USER"
92+
echo "Single-user mine (forced): $INPUT_USER"
7993
# Persist new users into users.txt so future scheduled runs
8094
# include them.
8195
if ! grep -qiE "^${INPUT_USER}$" cloudflare/users.txt; then
@@ -84,13 +98,28 @@ jobs:
8498
fi
8599
echo "$INPUT_USER" > /tmp/targets.txt
86100
else
87-
echo "Full mine of users.txt"
88-
grep -vE '^\s*(#|$)' cloudflare/users.txt > /tmp/targets.txt
101+
# Full mine: only mine users that don't have a deployed
102+
# dashboard yet. Once a dashboard exists, it stays put
103+
# until someone clicks the manual "Refresh" button (which
104+
# dispatches with inputs.user set).
105+
echo "Full mine of users.txt (skip already-deployed)"
106+
: > /tmp/targets.txt
107+
while IFS= read -r u || [ -n "$u" ]; do
108+
u="${u%%#*}"
109+
u="${u//[[:space:]]/}"
110+
[ -z "$u" ] && continue
111+
if [ -f "cloudflare/public/${u}.html" ]; then
112+
echo " skip @$u — dashboard already deployed"
113+
continue
114+
fi
115+
echo "$u" >> /tmp/targets.txt
116+
done < cloudflare/users.txt
89117
fi
90118
# Pre-stage pirate's enhanced version
91119
if [ -f stats.html ]; then
92120
cp stats.html cloudflare/public/pirate.html
93121
fi
122+
echo "Targets:"
94123
cat /tmp/targets.txt
95124
96125
- name: Mine each user (with live in-progress deploys)

cloudflare/worker/index.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,34 @@ async function handleRefresh(
6363
if (req.method !== "POST" && req.method !== "GET") {
6464
return new Response("method not allowed", { status: 405 });
6565
}
66+
// Manual refresh button passes ?force=1 to bypass the "already deployed"
67+
// short-circuit. Without force, a request for a user whose dashboard
68+
// already exists is a no-op (we want pages to stay valid indefinitely
69+
// once mined).
70+
const force = url.searchParams.get("force") === "1";
71+
72+
// Short-circuit if the static dashboard is already deployed. The Worker
73+
// fallback only sends people here when the asset is missing, so this
74+
// mainly catches direct /api/refresh callers (bots, refresh button
75+
// without force).
76+
if (!force) {
77+
const probe = new URL(url.toString());
78+
probe.pathname = `/${user}.html`;
79+
const probeResp = await env.ASSETS.fetch(
80+
new Request(probe.toString(), { method: "GET" }),
81+
);
82+
if (probeResp.status === 200) {
83+
return json({
84+
ok: true,
85+
user,
86+
status: "already_deployed",
87+
message: "Dashboard already exists. Pass ?force=1 to re-mine.",
88+
}, 200);
89+
}
90+
}
6691

67-
// Dedup: don't re-dispatch within 8 min of the most recent one for this
68-
// user. Uses Workers Cache API — no KV/DO binding needed.
92+
// Dedup: don't re-dispatch within ~6 hours of the most recent one for
93+
// this user. Uses Workers Cache API — no KV/DO binding needed.
6994
const cache = caches.default;
7095
const dedupKey = new Request(
7196
`https://internal-dedup.invalid/dispatch/${user}`,
@@ -529,6 +554,12 @@ async function dispatch() {
529554
showError("Dispatch failed (HTTP " + r.status + "): " + (data.error || "unknown"));
530555
return false;
531556
}
557+
if (data.status === "already_deployed") {
558+
// Race: dashboard came back online between page render and dispatch.
559+
$now.textContent = "Dashboard already deployed — reloading…";
560+
setTimeout(() => location.reload(), 400);
561+
return false;
562+
}
532563
$now.textContent = data.status === "already_running"
533564
? "Mining already running — joining in progress…"
534565
: "Mining job dispatched.";

stats_template.html

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,22 @@
596596
text-align: center;
597597
margin-top: 24px;
598598
}
599+
.refresh-btn {
600+
display: inline-block; margin-left: 10px;
601+
padding: 3px 10px; border-radius: 4px;
602+
background: var(--panel-2); border: 1px solid var(--border);
603+
color: var(--muted); cursor: pointer; font: inherit;
604+
font-size: 11px;
605+
}
606+
.refresh-btn:hover:not(:disabled) {
607+
color: var(--fg); border-color: var(--accent);
608+
}
609+
.refresh-btn:disabled { opacity: 0.6; cursor: progress; }
610+
.refresh-msg {
611+
font-size: 11px; color: var(--muted); margin-top: 6px;
612+
text-align: center;
613+
}
614+
.refresh-msg.err { color: #f85149; }
599615
/* Mining-in-progress banner (shown only when DATA.mining_status !== "complete") */
600616
#mining-banner {
601617
position: sticky; top: 0; z-index: 100;
@@ -758,6 +774,7 @@ <h2>All repositories</h2>
758774
</section>
759775

760776
<div class="footer-note" id="footer-note"></div>
777+
<div class="refresh-msg" id="refresh-msg"></div>
761778

762779
</main>
763780

@@ -1496,10 +1513,50 @@ <h1>${u.name || u.login}</h1>
14961513
// ---------- footer ----------
14971514
{
14981515
const gen = new Date(DATA.generated_at).toLocaleString();
1499-
document.getElementById("footer-note").textContent =
1516+
const footer = document.getElementById("footer-note");
1517+
footer.textContent =
15001518
`Generated ${gen} · ${fmtExact(DATA.totals.commits)} commits across ` +
1501-
`${fmtExact(DATA.totals.repos)} repos · counts pirate-authored commits only ` +
1502-
`(deduped by SHA across all local clones + GitHub API).`;
1519+
`${fmtExact(DATA.totals.repos)} repos · deduped by SHA across local clones + GitHub API.`;
1520+
// Manual refresh button — dashboards are never auto-re-mined; this is
1521+
// the only path to triggering a fresh mine for the displayed user.
1522+
const btn = document.createElement("button");
1523+
btn.className = "refresh-btn";
1524+
btn.type = "button";
1525+
btn.textContent = "↻ Refresh stats";
1526+
btn.title = "Re-run the GitHub mining job for @" + DATA.user.login;
1527+
const msg = document.getElementById("refresh-msg");
1528+
btn.addEventListener("click", async () => {
1529+
if (!confirm("Re-mine @" + DATA.user.login + "? This kicks off a fresh "
1530+
+ "GitHub Actions run (~5-15 min). The current dashboard "
1531+
+ "stays visible until the new one finishes deploying."))
1532+
return;
1533+
btn.disabled = true;
1534+
msg.classList.remove("err");
1535+
msg.textContent = "Dispatching mining job…";
1536+
try {
1537+
const r = await fetch(
1538+
"/api/refresh?user=" + encodeURIComponent(DATA.user.login)
1539+
+ "&force=1",
1540+
{ method: "POST" },
1541+
);
1542+
const d = await r.json().catch(() => ({}));
1543+
if (!r.ok) {
1544+
msg.classList.add("err");
1545+
msg.textContent = "Dispatch failed: " + (d.error || ("HTTP " + r.status));
1546+
btn.disabled = false;
1547+
return;
1548+
}
1549+
msg.textContent = d.status === "already_running"
1550+
? "A mining run is already in progress — check back in ~10 min."
1551+
: "Mining dispatched. This dashboard stays visible until the new "
1552+
+ "run finishes (~5-15 min); reload then to see fresh stats.";
1553+
} catch (e) {
1554+
msg.classList.add("err");
1555+
msg.textContent = "Network error: " + e.message;
1556+
btn.disabled = false;
1557+
}
1558+
});
1559+
footer.appendChild(btn);
15031560
}
15041561
</script>
15051562
</body>

0 commit comments

Comments
 (0)