Cannabis Freedom Index

Changelog eba2e7f

Let the changelog reload uncommitted work without restarting the dev server.

5 files of code, design or docs +77 −7 lines

Code, design and documents 5 files

src/components/UncommittedReload.astro +21 −0
@@ -0,0 +1,21 @@
+---
+// The uncommitted changelog entry is a snapshot. Reload asks the dev server to read the working
+// tree again (src/lib/gitlog.js). Shown only in dev; a production build has no server to ask.
+const { at } = Astro.props;
+const when = new Date(at).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit" });
+---
+<p class="gl-asof">
+  <time datetime={at}>As of {when}</time>
+  <button type="button" class="btn" data-gl-reload>Reload</button>
+</p>
+<script>
+  for (const b of document.querySelectorAll("[data-gl-reload]")) {
+    if (b.dataset.bound) continue;
+    b.dataset.bound = "1";
+    b.addEventListener("click", () => {
+      b.disabled = true;
+      b.textContent = "Reading…";
+      location.reload();
+    });
+  }
+</script>

src/lib/gitlog.js +19 −0
@@ -188,4 +188,7 @@ function readLog() {
 // Built on this computer (not by the deploy workflow, which sets CI), the changes not yet committed
 // come first, as one more change: { hash: "WORKTREE", short: "uncommitted", ... }
+// The dev server keeps this module loaded, so that snapshot would stay as it was when the server
+// started. The changelog pages call reloadUncommitted() on each request, and the page has a Reload
+// button for when it is left open while files change.
 function uncommitted() {
   if (process.env.CI) return [];
@@ -208,4 +211,20 @@ function uncommitted() {
 export const commits = [...uncommitted(), ...readLog()].map((c) => ({ ...c, groups: workOut(c) }));
 
+const EMPTY = { history: null, places: [], texts: [], data: [], code: [] };
+
+// Read the working tree again and put that in front of `commits`. Null when everything is committed.
+export function reloadUncommitted() {
+  if (!import.meta.env.DEV) return commits.find((c) => c.local) ?? null;
+  const [c] = uncommitted();
+  const fresh = c ? { ...c, groups: workOut(c) } : null;
+  const i = commits.findIndex((x) => x.local);
+  if (fresh && i >= 0) commits[i] = fresh;
+  else if (fresh) commits.unshift(fresh);
+  else if (i >= 0) commits.splice(i, 1);
+  return fresh;
+}
+
+export { EMPTY as emptyGroups };
+
 // one line of what a commit did: [[count, label, the id of its section on the commit's page]]
 export function summaryOf(c) {

src/pages/changelog/[hash].astro +24 −6
@@ -2,13 +2,31 @@
 // One change to the site: its message, and what it did, section by section (src/lib/gitlog.js).
 import Base from "../../layouts/Base.astro";
-import { commits, summaryOf } from "../../lib/gitlog.js";
+import UncommittedReload from "../../components/UncommittedReload.astro";
+import { commits, emptyGroups, reloadUncommitted, summaryOf } from "../../lib/gitlog.js";
 import { formatDate, places, SEGMENTS, segmentPoints } from "../../lib/countries.js";
 import { tierColor } from "../../lib/tiers.js";
 
 export function getStaticPaths() {
-  return commits.map((c, i) => ({ params: { hash: c.short }, props: { c, newer: commits[i - 1], older: commits[i + 1] } }));
+  // Keep the address available in dev even if the tree was clean when the server started, so Reload
+  // still has a page to land on after a later edit.
+  const list = [...commits];
+  if (import.meta.env.DEV && !list.some((c) => c.local)) {
+    list.unshift({
+      hash: "WORKTREE", short: "uncommitted", date: new Date().toISOString(),
+      subject: "Uncommitted changes", body: "Work on this computer that is not committed yet. Shown only in local builds.",
+      parent: list[0]?.hash ?? null, files: [], add: 0, del: 0, local: true, groups: emptyGroups,
+    });
+  }
+  return list.map((c, i) => ({ params: { hash: c.short }, props: { c, newer: list[i - 1] ?? null, older: list[i + 1] ?? null } }));
 }
-const { c, newer, older } = Astro.props;
+let { c, newer, older } = Astro.props;
 const base = import.meta.env.BASE_URL;
+if (import.meta.env.DEV) Astro.response.headers.set("Cache-Control", "no-store");
+if (import.meta.env.DEV && c.local) {
+  c = reloadUncommitted() ?? {
+    ...c, date: new Date().toISOString(), files: [], add: 0, del: 0, groups: emptyGroups,
+    body: "Nothing on this computer is uncommitted.",
+  };
+}
 const g = c.groups;
 const CAP = 200; // longest list shown in full
@@ -28,10 +46,10 @@ const moves = g.places.filter((p) => p.scores?.length || (p.status === "added" &
 <Base title={`${c.subject} · Changelog`} description={`${formatDate(c.date.slice(0, 10))}: ${summary.map(([n, w]) => `${n} ${w}`).join(", ") || "changes to the site"}.`}>
   <main class="changes-page gl gl-commit">
-    <div class="page-bar"><a class="back" href={`${base}changelog/`}>← Changelog</a></div>
     <section class="hero hero-sm">
-      <p class="eyebrow">Changelog <span><time datetime={c.date}>{formatDate(c.date.slice(0, 10))}</time></span> <span>{c.short}</span></p>
+      <p class="eyebrow"><a href={`${base}changelog/`}>Changelog</a> <span><time datetime={c.date}>{formatDate(c.date.slice(0, 10))}</time></span> <span>{c.short}</span></p>
       <h1>{c.subject}</h1>
       {c.body && <p class="lede gl-body">{c.body}</p>}
-      <p class="gl-sum gl-sum-big">{summary.map(([n, what, at]) => <a href={`#${at}`}><em>{n}</em> {what}</a>)} <span class="gl-lines"><ins>+{c.add.toLocaleString("en-GB")}</ins> <del>−{c.del.toLocaleString("en-GB")}</del> lines</span></p>
+      {import.meta.env.DEV && c.local && <UncommittedReload at={c.date} />}
+      {(summary.length > 0 || c.add + c.del > 0) && <p class="gl-sum gl-sum-big">{summary.map(([n, what, at]) => <a href={`#${at}`}><em>{n}</em> {what}</a>)} <span class="gl-lines"><ins>+{c.add.toLocaleString("en-GB")}</ins> <del>−{c.del.toLocaleString("en-GB")}</del> lines</span></p>}
     </section>
 

src/pages/changelog/index.astro +8 −1
@@ -4,7 +4,13 @@
 // scores alone are on /score-changes/.
 import Base from "../../layouts/Base.astro";
-import { commits, summaryOf } from "../../lib/gitlog.js";
+import UncommittedReload from "../../components/UncommittedReload.astro";
+import { commits, reloadUncommitted, summaryOf } from "../../lib/gitlog.js";
 import { formatDate } from "../../lib/countries.js";
 
+if (import.meta.env.DEV) {
+  Astro.response.headers.set("Cache-Control", "no-store");
+  reloadUncommitted();
+}
+const local = commits.find((c) => c.local);
 const base = import.meta.env.BASE_URL;
 // grouped by month, newest first
@@ -22,4 +28,5 @@ for (const c of commits) {
       <h1>Every change, and what exactly changed</h1>
       <p class="lede">Everything done to the index, newest first: new history, rescored countries, rewritten articles, new datasets and changes to the site itself. Open one to see it word by word. Only the changes that moved a country's score are on <a href={`${base}score-changes/`}>Score changes</a>.</p>
+      {import.meta.env.DEV && local && <UncommittedReload at={local.date} />}
     </section>
     {months.map((g) => (

src/styles/global.css +5 −0
@@ -3001,4 +3001,6 @@ a.ch-db-track:hover { background: color-mix(in srgb, var(--text) 6%, transparent
 
 /* the changelog (src/pages/changelog/): commits and what exactly they changed */
+.gl-commit .eyebrow a { color: var(--text-2); border-bottom: 1px solid var(--muted); }
+.gl-commit .eyebrow a:hover { color: var(--text); }
 .gl-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--border); }
 .gl-list a { display: grid; grid-template-columns: 120px minmax(0, 1fr) auto; gap: 4px 18px; padding: 14px 0; border-bottom: 1px solid var(--border); color: inherit; text-decoration: none; }
@@ -3008,4 +3010,7 @@ a.ch-db-track:hover { background: color-mix(in srgb, var(--text) 6%, transparent
 .gl-local a { border-left: 3px dashed #f0b54a; padding-left: 12px; }
 .gl-local b::after { content: "local only"; margin-left: 10px; padding: 1px 6px; border: 1px solid #f0b54a; border-radius: 2px; font: 400 10px var(--mono); text-transform: uppercase; letter-spacing: .08em; color: #f0b54a; vertical-align: middle; }
+.gl-asof { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin: 16px 0 0; font: 400 13px var(--mono); color: var(--muted); }
+.gl-asof .btn { padding: 5px 12px; font-size: 13px; }
+.gl-asof .btn:disabled { opacity: .6; cursor: progress; }
 .gl-list .gl-sum { grid-column: 2; }
 .gl-list .gl-lines { grid-row: 1; grid-column: 3; }

    ↑↓ to move, ↵ to open.