diff --git a/scripts/fetch-milestones.mjs b/scripts/fetch-milestones.mjs index aee71ce..153cd59 100644 --- a/scripts/fetch-milestones.mjs +++ b/scripts/fetch-milestones.mjs @@ -33,8 +33,9 @@ const itemsApi = (number) => `${REPO_API}/issues?milestone=${number}&state=all&p * open/closed counts stay in the output, so the page can say "and N more". */ const MAX_ITEMS_PER_MILESTONE = 30; /** Recently-shipped section: enough to show momentum, not a full history. */ -const MAX_CLOSED_MILESTONES = 5; - +// #129 — the journey shows every closed milestone; the count is small +// (14-25) and the data is cheap. The roadmap reads the same file. +const MAX_CLOSED_MILESTONES = 100; const headers = { accept: "application/vnd.github+json", "user-agent": "keeltrading.com-milestones-fetch", diff --git a/src/components/pages/RoadmapPage.astro b/src/components/pages/RoadmapPage.astro index f26b730..403fd7a 100644 --- a/src/components/pages/RoadmapPage.astro +++ b/src/components/pages/RoadmapPage.astro @@ -57,6 +57,10 @@ const data: MilestonesFile = * recently closed. Splitting preserves that order on each side. */ const open = data.milestones.filter((milestone) => milestone.state === "open"); const closed = data.milestones.filter((milestone) => milestone.state === "closed"); +/** #129 — the full journey, oldest first. */ +const journey = [...closed].sort( + (a, b) => (a.closedAt ?? "").localeCompare(b.closedAt ?? ""), +); /** Full counts travel with each milestone; items are capped in the script. */ const overflow = (milestone: RoadmapMilestone) => @@ -145,6 +149,30 @@ const overflow = (milestone: RoadmapMilestone) => )} + {journey.length > 0 && ( +
+

{c.journeyTitle}

+

{c.journeyIntro}

+
    + {journey.map((milestone) => ( +
  1. +

    + {milestone.title} +

    + {milestone.description &&

    {milestone.description}

    } +

    + {milestone.closedAt && ( + {c.closedOn(formatDate(milestone.closedAt))} + )} + + {c.progress(milestone.closedIssues, milestone.closedIssues + milestone.openIssues)} +

    +
  2. + ))} +
+
+ )} + {closed.length > 0 && (

{c.closedTitle}

@@ -317,6 +345,59 @@ const overflow = (milestone: RoadmapMilestone) => font-size: 0.88rem; } + /* #129 — the journey: a vertical timeline, oldest at the top. */ + .journey { + margin-block: 2.5rem; + } + + .journey-intro { + color: var(--muted); + font-size: 0.95rem; + max-width: 58ch; + } + + .journey-timeline { + list-style: none; + margin: 1.2rem 0 0; + padding: 0; + position: relative; + display: grid; + gap: 0.9rem; + } + + .journey-timeline::before { + content: ""; + position: absolute; + inset-inline-start: 0.55rem; + top: 0.6rem; + bottom: 0.6rem; + width: 2px; + background: var(--border); + } + + .journey-timeline .milestone { + margin: 0; + padding-inline-start: 2rem; + position: relative; + border: 1px solid var(--border); + } + + .journey-timeline .milestone::before { + content: ""; + position: absolute; + inset-inline-start: -0.35rem; + top: 1.2rem; + width: 0.7rem; + height: 0.7rem; + border-radius: 999px; + background: var(--accent); + border: 2px solid var(--surface); + } + + .journey-timeline .milestone-meta { + margin: 0.35rem 0 0; + } + /* Recently-shipped section: quieter, compact list. */ .shipped { border-top: 1px solid var(--border); diff --git a/src/i18n/pages/roadmap.ts b/src/i18n/pages/roadmap.ts index d6586e1..8e0dd40 100644 --- a/src/i18n/pages/roadmap.ts +++ b/src/i18n/pages/roadmap.ts @@ -22,6 +22,9 @@ export interface RoadmapContent { openTitle: string; /** Section heading above the recently closed milestones. */ closedTitle: string; + /** #129 — the chronological journey, oldest first. */ + journeyTitle: string; + journeyIntro: string; /** Milestone due date, when the repo sets one. */ dueOn: (date: string) => string; /** A milestone with no due date says so — never an invented date. */ @@ -45,7 +48,7 @@ export interface RoadmapContent { export const roadmap: LocalizedPage = { en: { - rev: "2026-08-28.1", + rev: "2026-08-29.1", title: "keel Roadmap — read from the repo, refreshed hourly", description: "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 = { "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.", ], openTitle: "In progress — open milestones", + journeyTitle: "The journey so far", + journeyIntro: + "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.", closedTitle: "Recently shipped", dueOn: (date) => `Target date: ${date}`, noDate: "No date set", @@ -70,8 +76,8 @@ export const roadmap: LocalizedPage = { }, ar: { - rev: "2026-08-28.1", - translatedFromRev: "2026-08-28.1", + rev: "2026-08-29.1", + translatedFromRev: "2026-08-29.1", title: "خارطةُ طريق كيل — تُقرأ من المستودع وتتجدّد كل ساعة", description: "خارطةُ طريق كيل: المراحلُ المفتوحة في CodeGateSoftware/keel والبنودُ تحتها، تُجلب من GitHub وقت البناء. والمرحلةُ المفتوحة نيّةٌ لا التزام.", @@ -80,6 +86,9 @@ export const roadmap: LocalizedPage = { "المرحلةُ المفتوحة نيّةٌ لا التزام، والتاريخُ عليها هدفٌ لا وعد. ويُنجَز العمل حين يصحّ قولُ إنه أُنجز — وسجلُّ التغييرات يوثّق ما هبط فعلًا.", ], openTitle: "قيدُ العمل — مراحلُ مفتوحة", + journeyTitle: "المسيرةُ حتى الآن", + journeyIntro: + "كلُّ المعالم المغلقة، من الأقدم إلى الأحدث — تاريخُ المشروع مقروءًا من المستودع مباشرةً، بتواريخٍ دوّنها GitHub في اليوم الذي اكتملت فيه كلُّ مرحلةٍ فعلًا. لا سردَ كُتب بعد وقوعه؛ هذا هو السجلّ.", closedTitle: "أُنجز مؤخّرًا", dueOn: (date) => `تاريخٌ مستهدف: ${date}`, noDate: "لم يُحدَّد تاريخ", @@ -96,8 +105,8 @@ export const roadmap: LocalizedPage = { }, fr: { - rev: "2026-08-28.1", - translatedFromRev: "2026-08-28.1", + rev: "2026-08-29.1", + translatedFromRev: "2026-08-29.1", title: "Feuille de route de keel — lue depuis le dépôt, actualisée toutes les heures", description: "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 = { "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.", ], openTitle: "En cours — jalons ouverts", + journeyTitle: "Le parcours jusqu'ici", + journeyIntro: + "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.", closedTitle: "Livré récemment", dueOn: (date) => `Date cible : ${date}`, noDate: "Aucune date fixée",