205205.meter .seg + .seg { margin-left: 2px; }
206206.meter .seg.a { background: var(--meter-a); }
207207.meter .seg.b { background: var(--meter-b); }
208+ .meter.mini { height: 8px; max-width: 340px; margin: 6px 0 2px; border-radius: 4px; }
208209/* bar list — one measure, single hue, labeled rows */
209210.barlist { display: grid; grid-template-columns: max-content 1fr max-content; gap: 6px 12px; align-items: center; margin: 14px 0; }
210211.barlist .lbl { font-size: 13px; }
@@ -360,13 +361,13 @@ def migration_meter(audit: dict) -> str:
360361<h3>Migration progress</h3>
361362<div class="meter" role="img" aria-label="{ n_repointed } of { n_total } static datasets repointed to data-lectures">{ segs } </div>
362363<div class="legend">
363- <span class="key"><span class="swatch" style="background:var(--meter-a)"></span>repointed — consumers read data-lectures ({ n_repointed } )</span>
364- <span class="key"><span class="swatch" style="background:var(--meter-b)"></span>landed here, consumers not yet repointed ({ n_landed } )</span>
365- <span class="key"><span class="swatch" style="background:var(--meter-track); border:1px solid var(--line)"></span>not migrated ({ n_total - n_repointed - n_landed } , of which { len (queued )} queued in P3 )</span>
364+ <span class="key"><span class="swatch" style="background:var(--meter-a)"></span>migrated — every consuming lecture reads the central copy ({ n_repointed } )</span>
365+ <span class="key"><span class="swatch" style="background:var(--meter-b)"></span>copied here, lectures not yet switched ({ n_landed } )</span>
366+ <span class="key"><span class="swatch" style="background:var(--meter-track); border:1px solid var(--line)"></span>not migrated ({ n_total - n_repointed - n_landed } , of which { len (queued )} queued for the next waves )</span>
366367</div>
367- <p class="note">Denominator: the { n_total } distinct static files lectures read today. The interim→final
368- URL cutover ( { issue_link ( "QuantEcon/data-lectures#15" ) } ) advances datasets from <em>repointed</em> to
369- <em>final</em> once <code>data.quantecon.org</code> is live; no dataset is final yet .</p>
368+ <p class="note">Denominator: the { n_total } distinct static files lectures read today. The full
369+ per-series manifest and the milestone plan are on the
370+ <a href="migration.html">migration tracker</a> .</p>
370371"""
371372
372373
@@ -400,11 +401,12 @@ def what_changed() -> str:
400401<h2>What moved since the { PREV_SNAPSHOT } snapshot</h2>
401402<p class="lede">The audit was first taken by hand on { PREV_SNAPSHOT } . Regenerating from live
402403repo state two days later, the picture has already changed — which is why this dashboard is generated.</p>
403- <div class="finding ok"><b>4 datasets migrated and repointed.</b>
404- <p>P1 moved <code>lingcod_msy_recovery.csv</code> (was a Colab-breaking local path);
405- P2 moved the <code>pandas_panel</code> trio <code>realwage.csv</code> / <code>countries.csv</code> /
406- <code>employ.csv</code> (5 of their 6 reads pointed at the retired legacy repo). All consumers
407- now read data-lectures. Details on the <a href="migration.html">migration tracker</a>.</p></div>
404+ <div class="finding ok"><b>4 datasets migrated.</b>
405+ <p>The first two migration waves moved <code>lingcod_msy_recovery.csv</code> (was a
406+ Colab-breaking local path) and the <code>pandas_panel</code> trio <code>realwage.csv</code> /
407+ <code>countries.csv</code> / <code>employ.csv</code> (5 of their 6 reads pointed at the retired
408+ legacy repo). Every consuming lecture now reads the central copy. Details on the
409+ <a href="migration.html">migration tracker</a>.</p></div>
408410<div class="finding ok"><b>Legacy-repo URLs: 8 → 0.</b>
409411<p>The pre-MyST <code>QuantEcon/lecture-python</code> repo was renamed to
410412<code>lecture-python.rst</code> and archived; the P2 repoints plus the <code>ols</code>
@@ -471,6 +473,88 @@ def render_index(audit: dict) -> str:
471473# Page: migration tracker
472474# ---------------------------------------------------------------------------
473475
476+ # Reader-facing status labels — the dashboard is read by people who are not
477+ # doing the migration, so the manifest speaks plainly; the lifecycle terms
478+ # (pending/landed/repointed/final) appear only in the explained stepper detail.
479+ STATUS_META = {
480+ # status → (pill css, label)
481+ "final" : ("p-local" , "✓ migrated" ),
482+ "repointed" : ("p-local" , "✓ migrated" ),
483+ "landed" : ("p-own" , "● copied, not yet switched" ),
484+ "queued" : ("p-legacy" , "queued" ),
485+ "unscheduled" : ("p-embed" , "not scheduled" ),
486+ }
487+
488+
489+ def dataset_status (fname : str , migration : dict ) -> tuple [str , str ]:
490+ """→ (status, pilot) for any static dataset the audit sees."""
491+ rec = (migration .get ("datasets" ) or {}).get (fname )
492+ if rec :
493+ return rec .get ("status" , "pending" ), rec .get ("pilot" , "" )
494+ for wave in migration .get ("pending" ) or []:
495+ if fname in (wave .get ("datasets" ) or []):
496+ return "queued" , wave .get ("pilot" , "" )
497+ return "unscheduled" , ""
498+
499+
500+ def series_manifest (audit : dict ) -> str :
501+ """Every static dataset, grouped by the lecture series that owns the bytes
502+ today, with its migration status — the how-far-along view."""
503+ migration = audit ["migration" ] or {}
504+ groups : dict [str , list ] = {}
505+ for d in audit ["datasets" ]:
506+ groups .setdefault (repo_of_record (d ), []).append (d )
507+ order = ["lecture-python-intro" , "lecture-python-programming" ,
508+ "lecture-python.myst" , "lecture-python-advanced.myst" ,
509+ "data-lectures" ]
510+ rows = ""
511+ for repo in [r for r in order if r in groups ] + sorted (set (groups ) - set (order )):
512+ ds = sorted (groups [repo ], key = lambda x : x ["file" ])
513+ statuses = [dataset_status (d ["file" ], migration )[0 ] for d in ds ]
514+ n = len (ds )
515+ n_done = sum (1 for s in statuses if s in ("repointed" , "final" ))
516+ n_landed = statuses .count ("landed" )
517+ n_queued = statuses .count ("queued" )
518+ counts = [f"{ n_done } migrated" if n_done else "" ,
519+ f"{ n_landed } copied, not switched" if n_landed else "" ,
520+ f"{ n_queued } queued" if n_queued else "" ,
521+ f"{ n - n_done - n_landed - n_queued } not scheduled"
522+ if n - n_done - n_landed - n_queued else "" ]
523+ pct = lambda k : max (0.8 , 100 * k / n ) if k else 0
524+ segs = ""
525+ if n_done :
526+ segs += f'<div class="seg a" style="width:{ pct (n_done ):.1f} %"></div>'
527+ if n_landed :
528+ segs += f'<div class="seg b" style="width:{ pct (n_landed ):.1f} %"></div>'
529+ rows += (f'<tr class="group"><td colspan="4">{ esc (repo )} — '
530+ f'{ n } file{ "s" if n != 1 else "" } · '
531+ f'{ esc (" · " .join (c for c in counts if c ))} '
532+ f'<div class="meter mini" role="img" aria-label="{ n_done } of { n } repointed">{ segs } </div>'
533+ f'</td></tr>' )
534+ for d , status_pilot in zip (ds , (dataset_status (d ["file" ], migration ) for d in ds )):
535+ status , pilot = status_pilot
536+ css , label = STATUS_META .get (status , ("p-embed" , status ))
537+ if pilot and status == "queued" :
538+ label += f" · wave { pilot } "
539+ pills = " " .join (pattern_pill (p ) for p in d ["patterns" ])
540+ rows += (f'<tr><td class="mono">{ esc (d ["file" ])} </td>'
541+ f'<td class="note">{ esc (d ["description" ])} </td>'
542+ f'<td>{ pills } </td>'
543+ f'<td><span class="pill { css } ">{ esc (label )} </span></td></tr>' )
544+ return f"""
545+ <h2>All datasets by series</h2>
546+ <p class="lede">Every static file the lectures read today, grouped by the series that owns the
547+ bytes, with its migration status — the how-far-along view. <em>Not scheduled</em> means no
548+ milestone has claimed the file yet; the broad sweep (see the milestones below) eventually
549+ covers them all. Data written by the lectures themselves and live-API reads are not migration
550+ targets and are tracked on the <a href="audit.html">audit page</a>.</p>
551+ <div class="tablewrap"><table>
552+ <tr><th>File</th><th>Contents</th><th>Hosting today</th><th>Status</th></tr>
553+ { rows }
554+ </table></div>
555+ """
556+
557+
474558def stepper (fname : str , rec : dict , verified : bool ) -> str :
475559 status = rec .get ("status" , "pending" )
476560 if status not in STATUS_STEPS :
@@ -499,6 +583,73 @@ def stepper(fname: str, rec: dict, verified: bool) -> str:
499583 return f'<div class="steps">{ steps } </div><p class="note" style="margin-bottom:0">{ check } </p>'
500584
501585
586+ def milestones (audit : dict ) -> str :
587+ """The migration programme as reader-facing milestones — completed waves
588+ (derived from migration.yml records), upcoming waves, the broad sweep, and
589+ the final-URL switch. This section is what lets the rest of the dashboard
590+ stay plan-agnostic: wave codes like P3 mean something only because they
591+ are presented here."""
592+ mig = audit ["migration" ] or {}
593+ recs = mig .get ("datasets" ) or {}
594+ manifests = audit ["manifests" ]
595+
596+ by_pilot : dict [str , list ] = {}
597+ for fname , rec in recs .items ():
598+ by_pilot .setdefault (rec .get ("pilot" , "?" ), []).append ((fname , rec ))
599+ items = ""
600+ for pilot in sorted (by_pilot ):
601+ entries = by_pilot [pilot ]
602+ files = sorted (f for f , _ in entries )
603+ done = all (r .get ("status" ) in ("repointed" , "final" ) for _ , r in entries )
604+ dates = [str ((r .get ("repoints" ) or [{}])[- 1 ].get ("date" , "" )) for _ , r in entries ]
605+ series = {c ["repo" ] for f , _ in entries
606+ for c in (manifests .get (f , {}).get ("consumers" ) or [])}
607+ n_series = len (series )
608+ mark , cls = ("✓" , "ok" ) if done else ("○" , "" )
609+ items += f"""
610+ <div class="finding { cls } ">
611+ <b>{ mark } Wave { esc (pilot )} — { len (files )} dataset{ "s" if len (files ) != 1 else "" } ,
612+ consumed by { n_series } lecture series{ " — completed " + esc (max (dates )) if done and max (dates ) else "" } </b>
613+ <p>{ " · " .join (f"<code>{ esc (f )} </code>" for f in files )} </p>
614+ </div>
615+ """
616+ for w in mig .get ("pending" ) or []:
617+ files = w .get ("datasets" ) or []
618+ flist = (" · " .join (f"<code>{ esc (f )} </code>" for f in sorted (files ))
619+ if files else "produces new snapshot files here; no existing file moves" )
620+ tracking = " · " .join (issue_link (t ) for t in w .get ("tracking" ) or [])
621+ items += f"""
622+ <div class="finding">
623+ <b>○ Wave { esc (w .get ("pilot" , "" ))} — { esc (w .get ("title" , w .get ("scope" , "" )))} </b>
624+ <p>{ flist } </p>
625+ <p>Tracking: { tracking } </p>
626+ </div>
627+ """
628+ n_unsched = sum (1 for d in audit ["datasets" ]
629+ if dataset_status (d ["file" ], mig )[0 ] == "unscheduled" )
630+ items += f"""
631+ <div class="finding">
632+ <b>○ Broad sweep — every remaining static file</b>
633+ <p>The { n_unsched } not-yet-scheduled files move once the waves above have proven the
634+ process for each kind of data. Mechanical from there: one data PR here, one switch PR
635+ per consuming lecture repo.</p>
636+ </div>
637+ <div class="finding">
638+ <b>○ Final URLs — serve everything from <code>data.quantecon.org</code></b>
639+ <p>Migrated lectures currently read this repository's GitHub URL. Once the custom domain
640+ is live, every migrated dataset switches to its permanent
641+ <code>data.quantecon.org/lectures/…</code> address in a single sweep
642+ ({ issue_link ("QuantEcon/data-lectures#15" )} ). No dataset has made this step yet.</p>
643+ </div>
644+ """
645+ return f"""
646+ <h2>Milestones</h2>
647+ <p class="lede">The migration proceeds in small waves, each proving the process for a harder
648+ kind of data before the broad sweep moves the rest.</p>
649+ { items }
650+ """
651+
652+
502653def render_migration (audit : dict ) -> str :
503654 mig = audit ["migration" ] or {}
504655 recs = mig .get ("datasets" ) or {}
@@ -518,67 +669,56 @@ def render_migration(audit: dict) -> str:
518669<div class="pipeline">
519670<div class="head">
520671<span class="name mono">{ esc (fname )} </span>
521- <span class="pill p-dl">{ esc (rec .get ("pilot" , "" ))} </span>
672+ <span class="pill p-dl">wave { esc (rec .get ("pilot" , "" ))} </span>
522673{ pattern_pill (prior )} <span class="note">→</span> { pattern_pill ("data-lectures" )}
523674<span class="note" style="margin-left:auto"><a href="{ RAW } /{ esc (fname )} ">file</a> ·
524675<a href="{ DL } /blob/main/lectures/{ esc (fname )} .yml">manifest</a></span>
525676</div>
526677<p class="note" style="margin:0 0 10px">{ esc (m .get ("title" , "" ))} — consumed by { consumers or "—" } </p>
527678{ stepper (fname , rec , verified )}
528679</div>
529- """
530-
531- waves = ""
532- for w in mig .get ("pending" ) or []:
533- files = "" .join (f'<li><code>{ esc (f )} </code> — { esc ((by_name .get (f ) or {}).get ("description" , "" ))} </li>'
534- for f in w .get ("datasets" ) or []) or \
535- "<li class='note'>produces a new file here; no existing static file moves</li>"
536- tracking = " · " .join (issue_link (t ) for t in w .get ("tracking" ) or [])
537- waves += f"""
538- <div class="finding">
539- <b>{ esc (w .get ("pilot" , "" ))} — { esc (w .get ("scope" , "" ))} </b>
540- <ul>{ files } </ul>
541- <p>Tracking: { tracking } </p>
542- </div>
543680"""
544681
545682 problems = audit ["problems" ]["migration_inconsistencies" ]
546683 consistency = ('<div class="finding ok"><b>Consistency check: clean.</b>'
547- '<p>migration.yml , the manifests and today\' s scan of every consumer repo agree.</p></div>'
684+ '<p>The tracker , the dataset manifests and today\' s scan of every lecture repo agree.</p></div>'
548685 if not problems else
549686 '<div class="finding crit"><b>Consistency check: FAILING.</b><ul>'
550687 + "" .join (f"<li>{ esc (p )} </li>" for p in problems ) + "</ul></div>" )
551688
552689 n_re = sum (1 for r in recs .values () if r .get ("status" ) in ("repointed" , "final" ))
553690 queued = {f for w in (mig .get ("pending" ) or []) for f in w .get ("datasets" ) or []}
691+ n_unsched = sum (1 for d in audit ["datasets" ]
692+ if dataset_status (d ["file" ], mig )[0 ] == "unscheduled" )
554693 body = f"""
555694<header class="doc">
556- <p class="eyebrow">Migration tracker · source of truth: <code>migration.yml</code> + <code>lectures/*.yml</code></p>
557- <h1>Datasets moving into data-lectures</h1>
558- <p class="meta">Each dataset's position in the lifecycle
559- <em>pending → landed → repointed → final</em>, with the PRs that moved it. The build
560- verifies every status against a fresh scan of the consumer repos, so this page cannot
695+ <p class="eyebrow">Migration tracker · generated and verified on every build</p>
696+ <h1>Moving lecture data into one central repository</h1>
697+ <p class="meta">The QuantEcon lectures read data from many places — their own repos, local
698+ files, retired repos, live APIs. This tracker shows the move to a single canonical home
699+ (<a href="{ DL } ">data-lectures</a>): what has moved, what's next, and how far along it is.
700+ Every status is verified against a fresh scan of the lecture repos, so this page cannot
561701claim a migration that didn't happen.</p>
562702</header>
563703<div class="stats">
564- <div class="stat ok"><b>{ n_re } </b><span>repointed — consumers read data-lectures today </span></div>
565- <div class="stat"><b>{ len (queued )} </b><span>queued in pending waves (P3) </span></div>
566- <div class="stat"><b>0 </b><span>final — on data.quantecon.org (gated on DNS, { issue_link ( "QuantEcon/data-lectures#15" ) } )</span></div>
704+ <div class="stat ok"><b>{ n_re } </b><span>datasets migrated — every consuming lecture reads the central copy </span></div>
705+ <div class="stat"><b>{ len (queued )} </b><span>queued for the next waves </span></div>
706+ <div class="stat"><b>{ n_unsched } </b><span>not yet scheduled (the broad sweep )</span></div>
567707</div>
708+ <h3>How a dataset moves</h3>
709+ <p><b>pending</b> — identified for migration; nothing has moved yet.
710+ <b>landed</b> — the file and its metadata are merged into the central repo; lectures unchanged.
711+ <b>repointed</b> — every consuming lecture now reads the central copy (shown as
712+ <em>✓ migrated</em> in the table below).
713+ <b>final</b> — the lecture reads the permanent <code>data.quantecon.org</code> address
714+ (the last milestone below).</p>
568715{ consistency }
569- <h2>Migrated datasets</h2>
716+ { series_manifest (audit )}
717+ { milestones (audit )}
718+ <h2>Migration record — dataset by dataset</h2>
719+ <p class="lede">The full provenance for each migrated dataset: which pull requests landed the
720+ data and switched each consuming lecture, and where it sits in the lifecycle.</p>
570721{ pipelines }
571- <h2>Pending waves</h2>
572- <p class="lede">Scoped but not landed. Files listed here still appear under their current
573- hosting pattern in the <a href="audit.html">audit</a>.</p>
574- { waves }
575- <h2>How a dataset moves</h2>
576- <p><b>pending</b> — identified for migration; nothing in data-lectures yet.
577- <b>landed</b> — file + manifest merged here; consumers unchanged.
578- <b>repointed</b> — every consumer reads this repo's interim raw URL
579- (<code>github.com/QuantEcon/data-lectures/raw/main/lectures/…</code>).
580- <b>final</b> — every consumer reads <code>https://data.quantecon.org/lectures/…</code>;
581- the cutover is one sweep tracked in { issue_link ("QuantEcon/data-lectures#15" )} , gated on Phase 4 DNS.</p>
582722"""
583723 return page ("Migration tracker — data-lectures" , "migration" , body , audit )
584724
0 commit comments