@@ -31,6 +31,15 @@ export default {
3131 return handleProgress ( req , env , url ) ;
3232 }
3333
34+ // -- Dynamic homepage: render live deployed + queued user lists -------
35+ if ( url . pathname === "/" || url . pathname === "/index.html" ) {
36+ try {
37+ return await handleIndex ( env , url ) ;
38+ } catch ( e ) {
39+ // Fall through to the static index.html in /public on any error.
40+ }
41+ }
42+
3443 // -- Static assets ----------------------------------------------------
3544 const assetResp = await env . ASSETS . fetch ( req ) ;
3645 if ( assetResp . status !== 404 ) return assetResp ;
@@ -419,6 +428,137 @@ async function handleStatus(
419428}
420429
421430
431+ // Dynamic homepage. Reads /users.txt (also deployed as a static asset
432+ // by CI) for the canonical list of users we want dashboards for, then
433+ // probes /{user}.html via the ASSETS binding to see which are ready vs
434+ // still queued/mining. Output is cached in Workers Cache for 30s so
435+ // repeated visits don't fan out to N internal asset probes each time.
436+ async function handleIndex ( env : Env , url : URL ) : Promise < Response > {
437+ const cache = caches . default ;
438+ const cacheKey = new Request ( "https://internal-index.invalid/v1" ) ;
439+ const cached = await cache . match ( cacheKey ) ;
440+ if ( cached ) return cached ;
441+
442+ // Read users.txt from the deployed assets.
443+ const probeBase = new URL ( url . toString ( ) ) ;
444+ probeBase . search = "" ;
445+ const usersTxtUrl = new URL ( probeBase . toString ( ) ) ;
446+ usersTxtUrl . pathname = "/users.txt" ;
447+ let users : string [ ] = [ ] ;
448+ try {
449+ const r = await env . ASSETS . fetch ( new Request ( usersTxtUrl . toString ( ) ) ) ;
450+ if ( r . ok ) {
451+ const txt = await r . text ( ) ;
452+ users = txt . split ( "\n" )
453+ . map ( ( l ) => l . split ( "#" , 1 ) [ 0 ] . trim ( ) )
454+ . filter ( ( l ) => l . length > 0 ) ;
455+ }
456+ } catch { }
457+
458+ // Add pirate (intentionally not in users.txt — built locally).
459+ if ( ! users . includes ( "pirate" ) ) users . unshift ( "pirate" ) ;
460+
461+ // Probe each user's /<u>.html for deploy status in parallel.
462+ const states = await Promise . all ( users . map ( async ( u ) => {
463+ const probeUrl = new URL ( probeBase . toString ( ) ) ;
464+ probeUrl . pathname = `/${ u } .html` ;
465+ try {
466+ const r = await env . ASSETS . fetch ( new Request ( probeUrl . toString ( ) ) ) ;
467+ return { user : u , deployed : r . status === 200 } ;
468+ } catch {
469+ return { user : u , deployed : false } ;
470+ }
471+ } ) ) ;
472+
473+ const deployed = states
474+ . filter ( ( s ) => s . deployed )
475+ . map ( ( s ) => s . user )
476+ . sort ( ( a , b ) => a . toLowerCase ( ) . localeCompare ( b . toLowerCase ( ) ) ) ;
477+ const queued = states
478+ . filter ( ( s ) => ! s . deployed )
479+ . map ( ( s ) => s . user )
480+ . sort ( ( a , b ) => a . toLowerCase ( ) . localeCompare ( b . toLowerCase ( ) ) ) ;
481+
482+ const html = indexPage ( deployed , queued ) ;
483+ const resp = new Response ( html , {
484+ headers : {
485+ "content-type" : "text/html; charset=utf-8" ,
486+ "cache-control" : "public, max-age=30" ,
487+ } ,
488+ } ) ;
489+ // Stash a clone for future hits (the response itself can only be
490+ // consumed once; cache.put is fine with the cloned Response).
491+ await cache . put ( cacheKey , resp . clone ( ) ) ;
492+ return resp ;
493+ }
494+
495+
496+ function indexPage ( deployed : string [ ] , queued : string [ ] ) : string {
497+ const escape = ( s : string ) =>
498+ s . replace ( / & / g, "&" ) . replace ( / < / g, "<" ) . replace ( / > / g, ">" ) ;
499+ const deployedRows = deployed . map ( ( u ) => {
500+ const suffix = u === "pirate" ? " — Nick Sweeting (enhanced)" : "" ;
501+ return ` <li class="ready"><a href="/${ escape ( u ) } ">/${ escape ( u ) } </a>${ suffix } </li>` ;
502+ } ) . join ( "\n" ) ;
503+ const queuedRows = queued . map ( ( u ) =>
504+ ` <li class="mining"><span>/${ escape ( u ) } </span> <em>· queued / mining</em></li>`
505+ ) . join ( "\n" ) ;
506+ const queuedSection = queued . length
507+ ? `\n <li class="section-hdr">Queued for next CI run (${ queued . length } )</li>\n${ queuedRows } `
508+ : "" ;
509+ return `<!doctype html>
510+ <html lang="en">
511+ <head>
512+ <meta charset="utf-8">
513+ <meta name="viewport" content="width=device-width,initial-scale=1">
514+ <title>githubusers.archivebox.io</title>
515+ <style>
516+ html, body {
517+ background: #0d1117; color: #e6edf3;
518+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
519+ margin: 0; padding: 0; min-height: 100%;
520+ }
521+ .wrap { max-width: 640px; margin: 0 auto; padding: 48px 24px; }
522+ h1 { font-size: 24px; margin: 0 0 8px; }
523+ p { color: #8b949e; line-height: 1.5; }
524+ a { color: #58a6ff; }
525+ ul { list-style: none; padding: 0; }
526+ ul li { padding: 8px 0; border-bottom: 1px solid #21262d; }
527+ ul li.mining { color: #8b949e; }
528+ ul li.mining em {
529+ color: #d29922; font-style: normal; font-size: 11px;
530+ background: #1f1810; border: 1px solid #443322;
531+ padding: 1px 6px; border-radius: 4px; margin-left: 6px;
532+ }
533+ ul li.section-hdr {
534+ color: #6e7681; font-size: 11px;
535+ border: 0; padding: 16px 0 4px;
536+ text-transform: uppercase; letter-spacing: 0.06em;
537+ }
538+ code { background: #21262d; padding: 2px 6px; border-radius: 4px; font-size: 90%; }
539+ .meta { font-size: 11px; color: #6e7681; margin-top: 24px; }
540+ </style>
541+ </head>
542+ <body>
543+ <div class="wrap">
544+ <h1>githubusers.archivebox.io</h1>
545+ <p>
546+ Precomputed contribution dashboards for selected GitHub users.
547+ Navigate to <code>/<login></code> for any user listed below
548+ (or any other login — mining auto-triggers on first visit).
549+ </p>
550+ <ul>
551+ ${ deployedRows } ${ queuedSection }
552+ </ul>
553+ <p class="meta">
554+ ${ deployed . length } deployed · ${ queued . length } queued · refreshed every 30s
555+ </p>
556+ </div>
557+ </body>
558+ </html>` ;
559+ }
560+
561+
422562function loadingPage ( user : string ) : string {
423563 // Inline HTML for the "mining…" view. Polls /api/status for the GH
424564 // workflow run's step list + /<user>.html for the first partial deploy.
0 commit comments