Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions scripts/fetch-milestones.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
81 changes: 81 additions & 0 deletions src/components/pages/RoadmapPage.astro
Original file line number Diff line number Diff line change
Expand Up @@ -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) =>
Expand Down Expand Up @@ -145,6 +149,30 @@ const overflow = (milestone: RoadmapMilestone) =>
</section>
)}

{journey.length > 0 && (
<section class="journey" aria-labelledby="roadmap-journey">
<h2 id="roadmap-journey">{c.journeyTitle}</h2>
<p class="journey-intro">{c.journeyIntro}</p>
<ol class="journey-timeline">
{journey.map((milestone) => (
<li class="card milestone">
<h3>
<a href={milestone.url}>{milestone.title}</a>
</h3>
{milestone.description && <p class="desc">{milestone.description}</p>}
<p class="milestone-meta">
{milestone.closedAt && (
<span>{c.closedOn(formatDate(milestone.closedAt))}</span>
)}
<span class="dot" aria-hidden="true">·</span>
<span>{c.progress(milestone.closedIssues, milestone.closedIssues + milestone.openIssues)}</span>
</p>
</li>
))}
</ol>
</section>
)}

{closed.length > 0 && (
<section class="shipped" aria-labelledby="roadmap-closed">
<h2 id="roadmap-closed">{c.closedTitle}</h2>
Expand Down Expand Up @@ -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);
Expand Down
22 changes: 17 additions & 5 deletions src/i18n/pages/roadmap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -45,7 +48,7 @@ export interface RoadmapContent {

export const roadmap: LocalizedPage<RoadmapContent> = {
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.",
Expand All @@ -54,6 +57,9 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
"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",
Expand All @@ -70,8 +76,8 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
},

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 وقت البناء. والمرحلةُ المفتوحة نيّةٌ لا التزام.",
Expand All @@ -80,6 +86,9 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
"المرحلةُ المفتوحة نيّةٌ لا التزام، والتاريخُ عليها هدفٌ لا وعد. ويُنجَز العمل حين يصحّ قولُ إنه أُنجز — وسجلُّ التغييرات يوثّق ما هبط فعلًا.",
],
openTitle: "قيدُ العمل — مراحلُ مفتوحة",
journeyTitle: "المسيرةُ حتى الآن",
journeyIntro:
"كلُّ المعالم المغلقة، من الأقدم إلى الأحدث — تاريخُ المشروع مقروءًا من المستودع مباشرةً، بتواريخٍ دوّنها GitHub في اليوم الذي اكتملت فيه كلُّ مرحلةٍ فعلًا. لا سردَ كُتب بعد وقوعه؛ هذا هو السجلّ.",
closedTitle: "أُنجز مؤخّرًا",
dueOn: (date) => `تاريخٌ مستهدف: ${date}`,
noDate: "لم يُحدَّد تاريخ",
Expand All @@ -96,8 +105,8 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
},

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.",
Expand All @@ -106,6 +115,9 @@ export const roadmap: LocalizedPage<RoadmapContent> = {
"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",
Expand Down
Loading