feat(changelog): the transformation is visible — live Shipped feed from our own merged PRs #5555
1 changed file+161−6
Modifiedsrc/routes/changelog.tsx+161−6View fileUnifiedSplit
@@ -1,26 +1,94 @@
11/**
2 * /changelog — manually curated list of recent platform releases.
3 * Public, no auth required.
2 * /changelog — the platform's transformation, visible.
3 *
4 * Two layers:
5 * - "Shipped" — LIVE from the platform's own merged pull requests. The
6 * page used to be hand-curated only, which meant a fourteen-PR day
7 * rendered as nothing (owner, 2026-08-27: "i dont see anything happen
8 * — most developers like seeing the transformation"). Gluecron
9 * self-hosts, so every merged PR IS a shipped change; the feed can
10 * never go stale because it is the same ledger the merges write.
11 * - Curated monthly highlights below — the narrative layer, still
12 * hand-written.
13 *
14 * Public, no auth required. The live section renders only while the
15 * platform repo is public — if it ever flips private, the feed vanishes
16 * rather than leaking titles.
417 */
518
619import { Hono } from "hono";
720import type { FC } from "hono/jsx";
21import { and, desc, eq } from "drizzle-orm";
22import { db } from "../db";
23import { pullRequests, repositories, users } from "../db/schema";
824import { Layout } from "../views/layout";
925import { softAuth } from "../middleware/auth";
1026import type { AuthEnv } from "../middleware/auth";
27import { formatRelative } from "../views/ui";
1128
1229const changelog = new Hono<AuthEnv>();
1330changelog.use("*", softAuth);
1431
15changelog.get("/changelog", (c) => {
32/** The platform's own repo — the self-hosting ledger the feed reads. */
33const PLATFORM_OWNER = "ccantynz";
34const PLATFORM_REPO = "Gluecron.com";
35
36interface ShippedRow {
37 number: number;
38 title: string;
39 mergedAt: Date | null;
40}
41
42/** Never throws — an empty feed renders the curated list alone. */
43async function loadShipped(limit = 20): Promise<ShippedRow[]> {
44 try {
45 const [ownerRow] = await db
46 .select({ id: users.id })
47 .from(users)
48 .where(eq(users.username, PLATFORM_OWNER))
49 .limit(1);
50 if (!ownerRow) return [];
51 const [repo] = await db
52 .select({ id: repositories.id, isPrivate: repositories.isPrivate })
53 .from(repositories)
54 .where(
55 and(
56 eq(repositories.ownerId, ownerRow.id),
57 eq(repositories.name, PLATFORM_REPO)
58 )
59 )
60 .limit(1);
61 if (!repo || repo.isPrivate) return [];
62 return await db
63 .select({
64 number: pullRequests.number,
65 title: pullRequests.title,
66 mergedAt: pullRequests.mergedAt,
67 })
68 .from(pullRequests)
69 .where(
70 and(
71 eq(pullRequests.repositoryId, repo.id),
72 eq(pullRequests.state, "merged")
73 )
74 )
75 .orderBy(desc(pullRequests.mergedAt))
76 .limit(limit);
77 } catch {
78 return [];
79 }
80}
81
82changelog.get("/changelog", async (c) => {
1683 const user = c.get("user");
84 const shipped = await loadShipped();
1785 return c.html(
1886 <Layout
1987 title="Changelog — gluecron"
20 description="What's new in gluecron — recent feature releases, improvements, and platform updates."
88 description="What's new in gluecron — live from merged pull requests, plus curated monthly highlights."
2189 user={user}
2290 >
23 <ChangelogPage />
91 <ChangelogPage shipped={shipped} />
2492 </Layout>,
2593 );
2694});
@@ -171,7 +239,7 @@ const RELEASES: ChangelogMonth[] = [
171239// View
172240// ---------------------------------------------------------------------------
173241
174const ChangelogPage: FC = () => (
242const ChangelogPage: FC<{ shipped?: ShippedRow[] }> = ({ shipped = [] }) => (
175243 <>
176244 <style dangerouslySetInnerHTML={{ __html: changelogCss }} />
177245 <div class="cl-root">
@@ -189,6 +257,50 @@ const ChangelogPage: FC = () => (
189257 </header>
190258
191259 <div class="cl-content">
260 {shipped.length > 0 && (
261 <section class="cl-month" aria-labelledby="cl-shipped-h">
262 <h2 class="cl-month-heading" id="cl-shipped-h">
263 Shipped
264 <span class="cl-live-pill">
265 <span class="cl-live-dot" aria-hidden="true" />
266 live from merged pull requests
267 </span>
268 </h2>
269 <p class="cl-shipped-sub">
270 Gluecron builds itself on Gluecron. Every entry below is a real
271 merged pull request from{" "}
272 <a href={`/${PLATFORM_OWNER}/${PLATFORM_REPO}`}>
273 the platform's own repository
274 </a>{" "}
275 — gated by its full CI suite and deployed automatically within
276 minutes of merging. This list updates itself; it cannot go
277 stale.
278 </p>
279 <ul class="cl-entries">
280 {shipped.map((pr) => (
281 <li class="cl-entry" key={pr.number}>
282 <span class="cl-entry-dot" aria-hidden="true" />
283 <div class="cl-entry-body">
284 <strong class="cl-entry-title">
285 <a
286 href={`/${PLATFORM_OWNER}/${PLATFORM_REPO}/pulls/${pr.number}`}
287 class="cl-shipped-link"
288 >
289 {pr.title}
290 </a>
291 </strong>
292 <p class="cl-entry-desc">
293 #{pr.number}
294 {pr.mergedAt
295 ? ` · merged ${formatRelative(pr.mergedAt.toISOString())}`
296 : ""}
297 </p>
298 </div>
299 </li>
300 ))}
301 </ul>
302 </section>
303 )}
192304 {RELEASES.map((rel) => (
193305 <section class="cl-month" key={rel.month}>
194306 <h2 class="cl-month-heading">{rel.month}</h2>
@@ -284,6 +396,49 @@ const changelogCss = `
284396 border-bottom: 1px solid var(--border-subtle, rgba(255,255,255,0.08));
285397}
286398
399/* The live "Shipped" feed */
400.cl-live-pill {
401 display: inline-flex;
402 align-items: center;
403 gap: 6px;
404 margin-left: 10px;
405 padding: 3px 10px;
406 border-radius: 99px;
407 font-size: 11px;
408 font-weight: 600;
409 letter-spacing: 0.04em;
410 text-transform: uppercase;
411 color: var(--accent);
412 background: color-mix(in srgb, var(--accent) 10%, transparent);
413 border: 1px solid color-mix(in srgb, var(--accent) 25%, transparent);
414 vertical-align: middle;
415}
416.cl-live-dot {
417 width: 6px;
418 height: 6px;
419 border-radius: 50%;
420 background: var(--accent);
421 animation: cl-live-pulse 2s ease-in-out infinite;
422}
423@keyframes cl-live-pulse {
424 0%, 100% { opacity: 1; }
425 50% { opacity: 0.35; }
426}
427.cl-shipped-sub {
428 font-size: 13.5px;
429 color: var(--text-muted);
430 margin: -12px 0 20px;
431 line-height: 1.6;
432}
433.cl-shipped-link {
434 color: var(--text-strong);
435 text-decoration: none;
436}
437.cl-shipped-link:hover {
438 color: var(--accent);
439 text-decoration: underline;
440}
441
287442/* Entry list */
288443.cl-entries {
289444 list-style: none;
290445
c comment · e edit title · m merge · a approve · r request changes · ? shortcuts