docs: What's New release numbers and commit IDs instead of dates

The What's New popup shows the date each release went out, and the
version strings are dates too. That gives the office a way to ask
"what did you do on the 30th?".

Design: each release is identified by a release number and the short
commit SHA it shipped with. The date stays in the data file as an
authoring reference but is never rendered, and the date formatter is
deleted so no date can reach the screen.

Five existing entries are renumbered 1-5, each anchored to the last
commit that landed in it. A guard test fails the build if a date ever
comes back into the popup.
This commit is contained in:
Dave 2026-11-02 20:17:08 +02:00
parent 3ca66ef19a
commit ebfee9eeaa

View file

@ -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
<span className="...pill...">{release.version}</span>
<span className="text-xs text-muted-foreground">{formatDate(release.date)}</span>
```
to:
```tsx
<span className="...pill...">Release {release.release}</span>
<span className="font-mono text-xs text-muted-foreground">{release.commit}</span>
```
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.