diff --git a/docs/superpowers/specs/2026-11-02-whats-new-release-ids-design.md b/docs/superpowers/specs/2026-11-02-whats-new-release-ids-design.md new file mode 100644 index 0000000..05fd777 --- /dev/null +++ b/docs/superpowers/specs/2026-11-02-whats-new-release-ids-design.md @@ -0,0 +1,213 @@ +# What's New — release numbers and commit IDs instead of dates — design + +Date: 2026-11-02 · Status: draft for Dave's approval + +## Why + +The What's New popup (sidebar button → `src/components/whats-new-dialog.tsx`) +tells the office what has changed in WFDASH. Today every entry shows **the +date it went out**, and the version string itself is a date +(`"2026.11.01"`). Both are on screen for every operator. + +Dave does not want dates visible there. A date next to a feature tells the +office *when* it landed, which turns the popup into a way to ask "what did +you do on the 30th?" — and the dates in the file are the one thing in the +popup that can be read as a record of when work happened. + +The identity of a release still has to be unique and still has to be +traceable to real code, so it should be **the release number plus the commit +the release went out with** instead of the date. + +## How it works today + +`src/lib/release-notes.ts` is a hand-written static array, newest first. There +is no changelog table, no PocketBase collection, no API call and no generator +script — the notes are typed in by hand when a release goes out, and the popup +renders whatever is in the file. + +The only automatic part is the **"New" badge**: the button lights up when the +top entry's `version` string differs from the version stored in the +operator's `localStorage` under `wfdash-whats-new-seen`. Opening the popup +writes the current version back. + +So publishing a release is manual today, and stays manual after this change. + +## Goals + +- **No dates anywhere in the popup.** Not the date field, not the version + string, not a rendered "1 Nov 2026". +- Each release is identified by **a release number** and **the short commit + SHA it shipped with**. +- The list stays hand-written. Publishing a release stays as easy as it is + today: add an entry at the top, take the next number, paste the SHA. +- A test fails if a date ever comes back into the popup. + +## Non-goals + +- **Auto-generating the notes from the git log.** The copy is deliberately + hand-written ("the AI never changes a price" is a sentence a machine will + not write). Generating it is a separate, bigger change and is not wanted + here. +- Publishing to Forgejo, a changelog table, or a PocketBase collection. +- Changing what the "New" badge *means* beyond the marker value. + +## The design + +### Data model — `src/lib/release-notes.ts` + +```ts +export interface ReleaseNote { + /** Release number. 1 = the first release. Also the "seen" marker. */ + release: number; + /** Short SHA of the commit this release went out with. Shown to operators. */ + commit: string; + /** + * Authoring reference only — the day the release was written up. + * Deliberately never rendered. Keep it so the author can group commits + * into a release while writing the notes. + */ + date: string; + /** One-line headline for the release. */ + title: string; + /** The changes, one sentence each. */ + items: string[]; +} +``` + +`version: string` is replaced by `release: number`. A number cannot be +mistaken for a date, and it cannot drift into one the way a free-text version +string can. + +The `date` field **stays** — it is how the author knows which window of +commits to group — but it is never passed to the render path. The only reader +of `release.date` was the date formatter, which this change deletes. + +### Rendering — `src/components/whats-new-dialog.tsx` + +`formatDate()` is **deleted**, along with its only call site. There is no +remaining code in the component that can turn a date into text. This matters: +deleting the formatter is what makes "no dates" a property of the code rather +than a promise in a comment. + +The badge row changes from: + +```tsx +{release.version} +{formatDate(release.date)} +``` + +to: + +```tsx +Release {release.release} +{release.commit} +``` + +The layout is unchanged — the commit simply occupies the slot the date used +to. The SHA is monospace, matching the build stamp in the corner of the app +(`src/components/build-stamp.tsx`), so a code reference reads the same +everywhere. + +**The SHA is plain text, not a link.** The Forgejo instance is on an internal +host; linking it would put that URL in front of every operator. The build +stamp does not link either. + +### The "seen" marker + +`localStorage["wfdash-whats-new-seen"]` stores `String(release.release)` — +the release number, as a string. The comparison becomes +`state.seen !== String(LATEST_RELEASE.release)`, replacing the old +`state.seen !== LATEST_RELEASE.version`. + +**One-time re-notify.** The marker in the field today is `"2026.11.01"`. It +will not match the new marker, so the badge lights up once for every operator +on the next deploy and then settles. That is the honest result of changing +the format: everyone looks at the new popup once. No migration code is +written for it — a seed value would be more code than the one-off badge. + +### The existing entries, renumbered + +Five entries exist. They are renumbered oldest → newest, 1 to 5, and each is +anchored to the **last commit that landed in it**, so the SHA always points at +real code: + +| Release | Title | Anchor commit | +|---|---|---| +| 5 | Delivery Pool, letterhead lock and a dark mode section | `139ea25` | +| 4 | Collection Confirm gets a second pair of eyes | `83aec14` | +| 3 | Auto-push and a shortcut to AI settings | `3eea87e` | +| 2 | A phone board for the managers | `b212d0f` | +| 1 | One POS matcher, AI-only quote matching | `55cc7fa` | + +The next release Dave publishes is **Release 6**. + +Titles, items and the `date` field are carried over untouched. Only the +identity changes. + +### Publishing a release (the new procedure) + +1. Merge the work and note the short SHA: `git log --oneline -1`. +2. Add an entry at the **top** of `RELEASE_NOTES` with the next release + number, that SHA, the date, a headline and the items. +3. Done. The badge lights up for everyone because the number changed. + +This is the same amount of work as today, and the file header says so. + +## Error handling + +There is no new failure mode. The file is a static array compiled into the +bundle; if an entry is missing a field the component renders an empty string, +which is what happens today. + +One case worth naming: **a duplicated release number**. If two entries share a +number, the "seen" marker cannot tell them apart, and an operator who has +read Release 5 would not be re-notified when Release 5 changes. The guard test +below fails the build on that. + +## Testing + +**`src/lib/release-notes.test.ts`** (new) + +- Release numbers are unique. +- Release numbers are contiguous `1..N` — no gaps, no duplicates. +- The array is ordered newest first (descending release numbers). +- Every `commit` matches `/^[0-9a-f]{7,40}$/`. +- Every entry has a title and at least one item. + +**`src/components/whats-new-dialog.test.tsx`** (updated) + +- The badge shows `Release 5` and the commit SHA of the latest release. +- The "seen" marker written to `localStorage` is the latest release number. +- **No date is rendered.** The dialog's text content is checked against a + date pattern and must contain none. The pattern covers all three shapes a + date could reach the screen in: + + ```ts + const DATE_PATTERN = + /\d{4}-\d{2}-\d{2}|\d{1,2}\s+(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{4}|(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\s+\d{1,2},?\s+\d{4}/i; + ``` + + `2026-11-01` is the ISO shape the old `version` strings used; `1 Nov 2026` + is the shape `toLocaleDateString("en-ZA", …)` produced. This is the + regression guard for the whole change — if a future edit reintroduces a + date in any form, the build fails. +- The existing badge / seen / notes tests stay. + +**`src/components/sidebar.test.tsx`** — unchanged. It only asserts the button +exists. + +## Docs + +`AGENTS.md` gets a short section stating the rule: the What's New popup +carries a release number and a commit SHA, never a date, and the date in the +data file is authoring-only. The file already documents one section per +feature; this joins them. + +## What this does not change + +- The popup still opens from the sidebar, still works offline, still needs no + server round-trip. +- The notes are still hand-written. +- The badge still means "you have not read the newest release". +- No date is removed from the app anywhere else. `BuildStamp`, order dates, + invoice dates and every other date in WFDASH are untouched.