Skip to content

Commit f45b87b

Browse files
authored
feat(roadmap): the journey — every closed milestone as a chronological timeline (#129) (#130)
Jesse-style project history, keel-style sourcing: a vertical timeline of ALL 14 closed milestones, oldest first, dated by GitHub on the day each phase finished — from 'Safety hardening' (2026-07-17) through Phase 13 TUI and Phase 14 Self-update (2026-08-19). The intro says plainly: no narrative written after the fact; this is the record. fetch-milestones.mjs uncaps closed milestones (5 → all; the set is small and cheap). Open milestones keep the intention-not-commitment framing below; the shipped-top-5 section retires in favor of the full timeline. ×3 locales, revs bumped. Verified: 14 entries oldest→newest ascending, Phase 14/13/Safety present, timeline renders at 390px ×3 locales zero overflow.
1 parent 5fa1f04 commit f45b87b

3 files changed

Lines changed: 101 additions & 7 deletions

File tree

scripts/fetch-milestones.mjs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ const itemsApi = (number) => `${REPO_API}/issues?milestone=${number}&state=all&p
3333
* open/closed counts stay in the output, so the page can say "and N more". */
3434
const MAX_ITEMS_PER_MILESTONE = 30;
3535
/** Recently-shipped section: enough to show momentum, not a full history. */
36-
const MAX_CLOSED_MILESTONES = 5;
37-
36+
// #129 — the journey shows every closed milestone; the count is small
37+
// (14-25) and the data is cheap. The roadmap reads the same file.
38+
const MAX_CLOSED_MILESTONES = 100;
3839
const headers = {
3940
accept: "application/vnd.github+json",
4041
"user-agent": "keeltrading.com-milestones-fetch",

src/components/pages/RoadmapPage.astro

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ const data: MilestonesFile =
5757
* recently closed. Splitting preserves that order on each side. */
5858
const open = data.milestones.filter((milestone) => milestone.state === "open");
5959
const closed = data.milestones.filter((milestone) => milestone.state === "closed");
60+
/** #129 — the full journey, oldest first. */
61+
const journey = [...closed].sort(
62+
(a, b) => (a.closedAt ?? "").localeCompare(b.closedAt ?? ""),
63+
);
6064
6165
/** Full counts travel with each milestone; items are capped in the script. */
6266
const overflow = (milestone: RoadmapMilestone) =>
@@ -145,6 +149,30 @@ const overflow = (milestone: RoadmapMilestone) =>
145149
</section>
146150
)}
147151

152+
{journey.length > 0 && (
153+
<section class="journey" aria-labelledby="roadmap-journey">
154+
<h2 id="roadmap-journey">{c.journeyTitle}</h2>
155+
<p class="journey-intro">{c.journeyIntro}</p>
156+
<ol class="journey-timeline">
157+
{journey.map((milestone) => (
158+
<li class="card milestone">
159+
<h3>
160+
<a href={milestone.url}>{milestone.title}</a>
161+
</h3>
162+
{milestone.description && <p class="desc">{milestone.description}</p>}
163+
<p class="milestone-meta">
164+
{milestone.closedAt && (
165+
<span>{c.closedOn(formatDate(milestone.closedAt))}</span>
166+
)}
167+
<span class="dot" aria-hidden="true">·</span>
168+
<span>{c.progress(milestone.closedIssues, milestone.closedIssues + milestone.openIssues)}</span>
169+
</p>
170+
</li>
171+
))}
172+
</ol>
173+
</section>
174+
)}
175+
148176
{closed.length > 0 && (
149177
<section class="shipped" aria-labelledby="roadmap-closed">
150178
<h2 id="roadmap-closed">{c.closedTitle}</h2>
@@ -317,6 +345,59 @@ const overflow = (milestone: RoadmapMilestone) =>
317345
font-size: 0.88rem;
318346
}
319347

348+
/* #129 — the journey: a vertical timeline, oldest at the top. */
349+
.journey {
350+
margin-block: 2.5rem;
351+
}
352+
353+
.journey-intro {
354+
color: var(--muted);
355+
font-size: 0.95rem;
356+
max-width: 58ch;
357+
}
358+
359+
.journey-timeline {
360+
list-style: none;
361+
margin: 1.2rem 0 0;
362+
padding: 0;
363+
position: relative;
364+
display: grid;
365+
gap: 0.9rem;
366+
}
367+
368+
.journey-timeline::before {
369+
content: "";
370+
position: absolute;
371+
inset-inline-start: 0.55rem;
372+
top: 0.6rem;
373+
bottom: 0.6rem;
374+
width: 2px;
375+
background: var(--border);
376+
}
377+
378+
.journey-timeline .milestone {
379+
margin: 0;
380+
padding-inline-start: 2rem;
381+
position: relative;
382+
border: 1px solid var(--border);
383+
}
384+
385+
.journey-timeline .milestone::before {
386+
content: "";
387+
position: absolute;
388+
inset-inline-start: -0.35rem;
389+
top: 1.2rem;
390+
width: 0.7rem;
391+
height: 0.7rem;
392+
border-radius: 999px;
393+
background: var(--accent);
394+
border: 2px solid var(--surface);
395+
}
396+
397+
.journey-timeline .milestone-meta {
398+
margin: 0.35rem 0 0;
399+
}
400+
320401
/* Recently-shipped section: quieter, compact list. */
321402
.shipped {
322403
border-top: 1px solid var(--border);

src/i18n/pages/roadmap.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface RoadmapContent {
2222
openTitle: string;
2323
/** Section heading above the recently closed milestones. */
2424
closedTitle: string;
25+
/** #129 — the chronological journey, oldest first. */
26+
journeyTitle: string;
27+
journeyIntro: string;
2528
/** Milestone due date, when the repo sets one. */
2629
dueOn: (date: string) => string;
2730
/** A milestone with no due date says so — never an invented date. */
@@ -45,7 +48,7 @@ export interface RoadmapContent {
4548

4649
export const roadmap: LocalizedPage<RoadmapContent> = {
4750
en: {
48-
rev: "2026-08-28.1",
51+
rev: "2026-08-29.1",
4952
title: "keel Roadmap — read from the repo, refreshed hourly",
5053
description:
5154
"keel's roadmap: the open milestones of CodeGateSoftware/keel and the issues under them, read from GitHub at build time. An open milestone is an intention, not a commitment.",
@@ -54,6 +57,9 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
5457
"An open milestone is an intention, not a commitment, and a date on one is a target, not a promise. Work ships when it is honest to say it shipped — the changelog records what actually landed.",
5558
],
5659
openTitle: "In progress — open milestones",
60+
journeyTitle: "The journey so far",
61+
journeyIntro:
62+
"Every closed milestone, oldest first — the project's history read straight from the repository, dated by GitHub on the day each phase actually finished. No narrative was written after the fact; this is the record.",
5763
closedTitle: "Recently shipped",
5864
dueOn: (date) => `Target date: ${date}`,
5965
noDate: "No date set",
@@ -70,8 +76,8 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
7076
},
7177

7278
ar: {
73-
rev: "2026-08-28.1",
74-
translatedFromRev: "2026-08-28.1",
79+
rev: "2026-08-29.1",
80+
translatedFromRev: "2026-08-29.1",
7581
title: "خارطةُ طريق كيل — تُقرأ من المستودع وتتجدّد كل ساعة",
7682
description:
7783
"خارطةُ طريق كيل: المراحلُ المفتوحة في CodeGateSoftware/keel والبنودُ تحتها، تُجلب من GitHub وقت البناء. والمرحلةُ المفتوحة نيّةٌ لا التزام.",
@@ -80,6 +86,9 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
8086
"المرحلةُ المفتوحة نيّةٌ لا التزام، والتاريخُ عليها هدفٌ لا وعد. ويُنجَز العمل حين يصحّ قولُ إنه أُنجز — وسجلُّ التغييرات يوثّق ما هبط فعلًا.",
8187
],
8288
openTitle: "قيدُ العمل — مراحلُ مفتوحة",
89+
journeyTitle: "المسيرةُ حتى الآن",
90+
journeyIntro:
91+
"كلُّ المعالم المغلقة، من الأقدم إلى الأحدث — تاريخُ المشروع مقروءًا من المستودع مباشرةً، بتواريخٍ دوّنها GitHub في اليوم الذي اكتملت فيه كلُّ مرحلةٍ فعلًا. لا سردَ كُتب بعد وقوعه؛ هذا هو السجلّ.",
8392
closedTitle: "أُنجز مؤخّرًا",
8493
dueOn: (date) => `تاريخٌ مستهدف: ${date}`,
8594
noDate: "لم يُحدَّد تاريخ",
@@ -96,8 +105,8 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
96105
},
97106

98107
fr: {
99-
rev: "2026-08-28.1",
100-
translatedFromRev: "2026-08-28.1",
108+
rev: "2026-08-29.1",
109+
translatedFromRev: "2026-08-29.1",
101110
title: "Feuille de route de keel — lue depuis le dépôt, actualisée toutes les heures",
102111
description:
103112
"La feuille de route de keel : les jalons ouverts de CodeGateSoftware/keel et leurs tickets, lus depuis GitHub à la construction. Un jalon ouvert est une intention, pas un engagement.",
@@ -106,6 +115,9 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
106115
"Un jalon ouvert est une intention, pas un engagement, et sa date est un objectif, pas une promesse. Le travail est livré quand il est honnête de dire qu'il l'est — le journal des versions consigne ce qui a réellement atterri.",
107116
],
108117
openTitle: "En cours — jalons ouverts",
118+
journeyTitle: "Le parcours jusqu'ici",
119+
journeyIntro:
120+
"Chaque jalon fermé, du plus ancien au plus récent — l'histoire du projet lue directement du dépôt, datée par GitHub au jour où chaque phase s'est réellement achevée. Aucun récit écrit après coup ; ceci est la trace.",
109121
closedTitle: "Livré récemment",
110122
dueOn: (date) => `Date cible : ${date}`,
111123
noDate: "Aucune date fixée",

0 commit comments

Comments
 (0)