@@ -184,17 +184,49 @@ async function handleStatus(
184184 status : s . status ,
185185 conclusion : s . conclusion ,
186186 } ) ) ;
187+
188+ // Surface current GitHub API rate-limit state so the loading page can
189+ // explain delays. Uses the same PAT the CI runs with, so the search /
190+ // core remaining numbers are very close to what the CI job sees.
191+ let rateLimit : any = null ;
192+ try {
193+ const rl = await fetch ( "https://api.github.com/rate_limit" , {
194+ headers : {
195+ Authorization : `Bearer ${ env . GH_DISPATCH_TOKEN } ` ,
196+ "User-Agent" : "githubusers-archivebox-io" ,
197+ Accept : "application/vnd.github+json" ,
198+ } ,
199+ } ) ;
200+ if ( rl . ok ) {
201+ const rd = await rl . json ( ) as any ;
202+ const r = rd ?. resources ?? { } ;
203+ rateLimit = {
204+ search : r . search ? {
205+ remaining : r . search . remaining ,
206+ limit : r . search . limit ,
207+ reset : r . search . reset , // epoch seconds
208+ } : null ,
209+ core : r . core ? {
210+ remaining : r . core . remaining ,
211+ limit : r . core . limit ,
212+ reset : r . core . reset ,
213+ } : null ,
214+ } ;
215+ }
216+ } catch { }
217+
187218 return json ( {
188219 ok : true ,
189220 run_id : run . id ,
190- run_status : run . status , // queued | in_progress | completed
191- run_conclusion : run . conclusion , // success | failure | cancelled | null
221+ run_status : run . status ,
222+ run_conclusion : run . conclusion ,
192223 run_started_at : run . run_started_at ,
193224 run_url : run . html_url ,
194225 job_status : job ?. status ,
195226 current_step : steps . find ( ( s : any ) => s . status === "in_progress" ) ?. name
196227 ?? steps . at ( - 1 ) ?. name ?? null ,
197228 steps,
229+ rate_limit : rateLimit ,
198230 } ) ;
199231}
200232
@@ -272,6 +304,25 @@ function loadingPage(user: string): string {
272304 border: 1px solid #6e2120; border-radius: 6px; font-size: 13px;
273305 margin: 10px 0;
274306 }
307+ .ratelimit {
308+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
309+ font-size: 11px; padding: 8px 12px; margin: 0 0 14px;
310+ background: #0d1117; border: 1px solid #21262d;
311+ border-radius: 6px; color: #8b949e;
312+ display: flex; justify-content: space-between; align-items: center;
313+ gap: 12px; flex-wrap: wrap;
314+ }
315+ .ratelimit.cooldown {
316+ background: #2a1a08; border-color: #6e4c18; color: #ffa657;
317+ }
318+ .ratelimit .gauge {
319+ flex: 1; height: 4px; background: #21262d;
320+ border-radius: 2px; overflow: hidden; min-width: 80px;
321+ }
322+ .ratelimit .gauge > span {
323+ display: block; height: 100%; background: #3fb950;
324+ }
325+ .ratelimit.cooldown .gauge > span { background: #d97706; }
275326 a { color: #58a6ff; }
276327 code { background: #21262d; padding: 1px 5px; border-radius: 3px;
277328 font-size: 90%; font-family: inherit; }
@@ -297,6 +348,8 @@ function loadingPage(user: string): string {
297348
298349 <div class="progress-track"><div class="progress-fill" id="progress"></div></div>
299350
351+ <div id="ratelimit" class="ratelimit" style="display:none"></div>
352+
300353 <ol class="steps" id="steps"></ol>
301354
302355 <div id="error" class="err" style="display:none"></div>
@@ -320,6 +373,7 @@ const $steps = document.getElementById("steps");
320373const $err = document.getElementById("error");
321374const $runLink = document.getElementById("run-link");
322375const $spinner = document.getElementById("hdr-spinner");
376+ const $rl = document.getElementById("ratelimit");
323377
324378const startedAt = Date.now();
325379function fmtElapsed(sec) {
@@ -386,6 +440,34 @@ async function checkDeployed() {
386440 }
387441}
388442
443+ function renderRateLimit(rl) {
444+ if (!rl || (!rl.search && !rl.core)) {
445+ $rl.style.display = "none"; return;
446+ }
447+ const now = Math.floor(Date.now() / 1000);
448+ const items = [];
449+ for (const kind of ["search", "core"]) {
450+ const r = rl[kind];
451+ if (!r) continue;
452+ const pct = Math.max(0, Math.min(100, (r.remaining / r.limit) * 100));
453+ const cooldown = r.remaining < (kind === "search" ? 5 : 100);
454+ const secs = Math.max(0, (r.reset || 0) - now);
455+ const label = kind === "search" ? "GitHub search" : "GitHub core";
456+ items.push(
457+ '<div style="display:flex;align-items:center;gap:8px;flex:1;min-width:160px">' +
458+ '<span>' + label + ' ' + r.remaining + '/' + r.limit + '</span>' +
459+ '<div class="gauge"><span style="width:' + pct + '%"></span></div>' +
460+ (cooldown ? '<span style="color:#ffa657">resets in ' + secs + 's</span>' : '') +
461+ '</div>'
462+ );
463+ }
464+ $rl.innerHTML = items.join("");
465+ const lowSearch = rl.search && rl.search.remaining < 5;
466+ const lowCore = rl.core && rl.core.remaining < 100;
467+ $rl.className = "ratelimit" + (lowSearch || lowCore ? " cooldown" : "");
468+ $rl.style.display = "flex";
469+ }
470+
389471function renderSteps(status) {
390472 if (!status || !status.steps) return;
391473 if (status.run_url) {
@@ -439,6 +521,7 @@ function renderSteps(status) {
439521 checkDeployed(),
440522 ]);
441523 renderSteps(status);
524+ if (status) renderRateLimit(status.rate_limit);
442525 if (deployed) {
443526 clearInterval(interval);
444527 $now.textContent = "Dashboard ready — reloading…";
0 commit comments