@@ -27,6 +27,9 @@ export default {
2727 if ( url . pathname === "/api/status" ) {
2828 return handleStatus ( req , env , url ) ;
2929 }
30+ if ( url . pathname === "/api/progress" ) {
31+ return handleProgress ( req , env , url ) ;
32+ }
3033
3134 // -- Static assets ----------------------------------------------------
3235 const assetResp = await env . ASSETS . fetch ( req ) ;
@@ -135,6 +138,46 @@ function json(obj: unknown, status = 200): Response {
135138}
136139
137140
141+ async function handleProgress (
142+ req : Request ,
143+ env : Env ,
144+ url : URL ,
145+ ) : Promise < Response > {
146+ // POST { phase, message, totals } — keyed by ?user=X — read back via
147+ // /api/status. Auth: Bearer header must match GH_DISPATCH_TOKEN
148+ // (the same token the Action gets from secrets) so random clients
149+ // can't spam fake progress.
150+ if ( req . method !== "POST" ) {
151+ return json ( { error : "POST only" } , 405 ) ;
152+ }
153+ const auth = req . headers . get ( "Authorization" ) ?? "" ;
154+ if ( ! auth . startsWith ( "Bearer " ) ||
155+ auth . slice ( 7 ) !== env . GH_DISPATCH_TOKEN ) {
156+ return json ( { error : "unauthorized" } , 401 ) ;
157+ }
158+ const user = url . searchParams . get ( "user" ) ?. trim ( ) ;
159+ if ( ! user || ! VALID_LOGIN . test ( user ) ) {
160+ return json ( { error : "invalid user" } , 400 ) ;
161+ }
162+ const body = await req . text ( ) ;
163+ // Sanity cap — small JSON only.
164+ if ( body . length > 4096 ) return json ( { error : "too large" } , 413 ) ;
165+ try { JSON . parse ( body ) ; } catch { return json ( { error : "bad json" } , 400 ) ; }
166+ const cache = caches . default ;
167+ await cache . put (
168+ new Request ( `https://internal-progress.invalid/${ user } ` ) ,
169+ new Response ( body , {
170+ headers : {
171+ "Cache-Control" : "max-age=3600" ,
172+ "Content-Type" : "application/json" ,
173+ "X-Received-At" : new Date ( ) . toISOString ( ) ,
174+ } ,
175+ } ) ,
176+ ) ;
177+ return json ( { ok : true } ) ;
178+ }
179+
180+
138181async function handleStatus (
139182 req : Request ,
140183 env : Env ,
@@ -215,6 +258,49 @@ async function handleStatus(
215258 }
216259 } catch { }
217260
261+ // Tail the job's live log for richer progress info (e.g. the Python
262+ // script's `>> [N/M] ...` lines). The GH API redirects to a signed
263+ // download URL — fetch() follows by default.
264+ let recentLog : string [ ] = [ ] ;
265+ if ( job ?. id ) {
266+ try {
267+ const lr = await fetch (
268+ `https://api.github.com/repos/${ repo } /actions/jobs/${ job . id } /logs` ,
269+ {
270+ headers : {
271+ Authorization : `Bearer ${ env . GH_DISPATCH_TOKEN } ` ,
272+ "User-Agent" : "githubusers-archivebox-io" ,
273+ Accept : "application/vnd.github+json" ,
274+ } ,
275+ } ,
276+ ) ;
277+ if ( lr . ok ) {
278+ const txt = await lr . text ( ) ;
279+ // Each line is "<ISO timestamp> <message>"; strip timestamp +
280+ // filter to lines that look like Python script output.
281+ const interesting = txt
282+ . split ( "\n" )
283+ . map ( ( l ) => l . replace ( / ^ \d { 4 } - \d { 2 } - \d { 2 } T [ \d : . ] + Z \s ? / , "" ) )
284+ . filter ( ( l ) => / ^ ( > > | \s * \[ | \s * - { 2 } | \s * ! | \s * r e s o l v e d \b | \s * s c a n n i n g | \s * f e t c h i n g | \s * m i n i n g | \s * d e p l o y i n g | \s * s e a r c h q u o t a | \s * r e s o l v i n g ) / i
285+ . test ( l ) )
286+ . slice ( - 20 ) ;
287+ recentLog = interesting ;
288+ }
289+ } catch { }
290+ }
291+
292+ // Read the latest progress update posted by the running Python script.
293+ let progress : any = null ;
294+ try {
295+ const pres = await caches . default . match (
296+ new Request ( `https://internal-progress.invalid/${ user } ` ) ,
297+ ) ;
298+ if ( pres ) {
299+ progress = await pres . json ( ) ;
300+ progress . received_at = pres . headers . get ( "X-Received-At" ) ;
301+ }
302+ } catch { }
303+
218304 return json ( {
219305 ok : true ,
220306 run_id : run . id ,
@@ -227,6 +313,8 @@ async function handleStatus(
227313 ?? steps . at ( - 1 ) ?. name ?? null ,
228314 steps,
229315 rate_limit : rateLimit ,
316+ recent_log : recentLog ,
317+ progress,
230318 } ) ;
231319}
232320
@@ -323,6 +411,30 @@ function loadingPage(user: string): string {
323411 display: block; height: 100%; background: #3fb950;
324412 }
325413 .ratelimit.cooldown .gauge > span { background: #d97706; }
414+ .phase-msg {
415+ background: #0e2640; border: 1px solid #1f4d7a;
416+ color: #58a6ff; padding: 12px 14px; border-radius: 6px;
417+ margin: 0 0 14px; font-size: 13px;
418+ display: flex; justify-content: space-between; align-items: center;
419+ gap: 12px; flex-wrap: wrap;
420+ }
421+ .phase-msg .pm-msg { flex: 1; min-width: 200px; }
422+ .phase-msg .pm-counts {
423+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
424+ font-size: 11px; color: #8b949e;
425+ }
426+ .phase-msg .pm-counts strong { color: #c9d1d9; }
427+ .livelog {
428+ background: #0d1117; border: 1px solid #21262d;
429+ border-radius: 6px; padding: 10px 12px; margin: 14px 0 0;
430+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
431+ font-size: 11px; color: #c9d1d9;
432+ max-height: 180px; overflow-y: auto; line-height: 1.45;
433+ white-space: pre-wrap; word-break: break-all;
434+ }
435+ .livelog .l-hdr { color: #58a6ff; }
436+ .livelog .l-warn { color: #ffa657; }
437+ .livelog .l-err { color: #f85149; }
326438 a { color: #58a6ff; }
327439 code { background: #21262d; padding: 1px 5px; border-radius: 3px;
328440 font-size: 90%; font-family: inherit; }
@@ -348,10 +460,14 @@ function loadingPage(user: string): string {
348460
349461 <div class="progress-track"><div class="progress-fill" id="progress"></div></div>
350462
463+ <div id="phase-msg" class="phase-msg" style="display:none"></div>
464+
351465 <div id="ratelimit" class="ratelimit" style="display:none"></div>
352466
353467 <ol class="steps" id="steps"></ol>
354468
469+ <pre id="livelog" class="livelog" style="display:none"></pre>
470+
355471 <div id="error" class="err" style="display:none"></div>
356472
357473 <div class="footer-row">
@@ -374,6 +490,8 @@ const $err = document.getElementById("error");
374490const $runLink = document.getElementById("run-link");
375491const $spinner = document.getElementById("hdr-spinner");
376492const $rl = document.getElementById("ratelimit");
493+ const $log = document.getElementById("livelog");
494+ const $pmsg = document.getElementById("phase-msg");
377495
378496const startedAt = Date.now();
379497function fmtElapsed(sec) {
@@ -440,6 +558,38 @@ async function checkDeployed() {
440558 }
441559}
442560
561+ function renderProgress(p) {
562+ if (!p || !p.phase) { $pmsg.style.display = "none"; return; }
563+ const countKeys = ["repos", "commits", "prs", "issues", "stars",
564+ "repos_accessible"];
565+ const counts = countKeys
566+ .filter(k => p[k] != null)
567+ .map(k => '<strong>' + p[k] + '</strong> ' + k);
568+ $pmsg.innerHTML =
569+ '<div class="pm-msg">' +
570+ (p.message || p.phase) +
571+ ' <code style="font-size:10px;color:#8b949e;margin-left:6px">' +
572+ p.phase + '</code></div>' +
573+ (counts.length ? '<div class="pm-counts">' + counts.join(" · ") + '</div>' : "");
574+ $pmsg.style.display = "flex";
575+ }
576+
577+ function renderLog(lines) {
578+ if (!Array.isArray(lines) || lines.length === 0) {
579+ $log.style.display = "none"; return;
580+ }
581+ const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
582+ $log.innerHTML = lines.map(l => {
583+ const cls = /^!|fail|error|❌/i.test(l) ? "l-err"
584+ : /quota|warn|^ !/i.test(l) ? "l-warn"
585+ : /^>>/.test(l) ? "l-hdr"
586+ : "";
587+ return '<span class="' + cls + '">' + esc(l) + '</span>';
588+ }).join("\n");
589+ $log.style.display = "block";
590+ $log.scrollTop = $log.scrollHeight;
591+ }
592+
443593function renderRateLimit(rl) {
444594 if (!rl || (!rl.search && !rl.core)) {
445595 $rl.style.display = "none"; return;
@@ -521,7 +671,11 @@ function renderSteps(status) {
521671 checkDeployed(),
522672 ]);
523673 renderSteps(status);
524- if (status) renderRateLimit(status.rate_limit);
674+ if (status) {
675+ renderProgress(status.progress);
676+ renderRateLimit(status.rate_limit);
677+ renderLog(status.recent_log);
678+ }
525679 if (deployed) {
526680 clearInterval(interval);
527681 $now.textContent = "Dashboard ready — reloading…";
0 commit comments