A quieter version list, and a Latest that means something (#157) #161

Merged
qwc merged 3 commits from feature/version-badge-colors into main 2026-09-03 11:55:24 +02:00
Owner

Closes #157.

Three commits, smallest to largest.

1. Nothing in the version list is red any more

Red meant "PDF" — a file type presented as a verdict — and expiry was red too, so both shouted at the same volume for very different things.

Badge Was Now
PDF red #dc2626 grey #6b7280
Expires… --color-danger --color-warning (orange)
Pinned primary blue purple #7c3aed
Temp. latest orange pink #db2777
Latest green green (unchanged — the one the system works out on its own)
No expiration new, outlined grey
Download grey btn-secondary primary blue + a download icon

The six badges each carried their own copy of the same six chip properties; those move to the .version-badge base class the templates already apply, so a variant only names its colours.

Also:

  • EXPIRES SOONEXPIRES TODAY. The window has already closed by then; the next hourly pass takes it.
  • NO EXPIRATION on versions the pattern keeps, instead of leaving the reader to infer it from an absent badge. Shown only where it means something — with retention off nothing expires, so it would otherwise sit on every row of every project.

A new test in internal/templates cross-checks every version-badge-* class the templates name against a rule in style.css. A class name in a template is only half a promise, and an unstyled chip renders fine and passes every handler test.

2. A temporary pin protects its version while it is held

#141 exempted permanently pinned versions from retention and reasoned that temporary pins needed no protection, since the next upload clears them anyway. That reads the pin backwards, as you pointed out: a temp pin whose version gets collected while the pin still points at it is a pin that did nothing, and the reader lands somewhere else with no sign anything moved.

Any pin is now exempt for as long as it is held, and the protection ends with the pin rather than outliving it — both upload paths clear a temporary pin before enforceRetentionPolicy runs, so a version already past its window goes on that same pass.

TestTemporaryPinDoesNotSurviveRetention is replaced by its opposite, plus TestUnpinningReturnsAVersionToRetention to pin down that the protection really is temporary.

3. Latest is the newest release, not the newest tag

The real bug in the issue. A prerelease outsorts every release below it, so a project publishing v2.0.0-rc1 alongside v1.9.0 had the candidate as its latest — on the frontpage card, at /project/{slug}/latest/, in search by default, badged on the project page — right up until retention deleted it for not being a release.

The keep pattern is the project's own definition of a real release, so it decides. latestVersionTag now resolves, in order:

  1. a pin, if set — an explicit statement about where readers land;
  2. the newest tag the keep pattern matches;
  3. the sorted newest, if the pattern describes none of the project's tags — a project with versions must have a latest.

One rule in one place: the project page was computing its own latest inline and could disagree with the permalink it prints two lines above the version list. It calls the shared helper now, and its unused LatestVersion template key goes with it (only EffectiveLatest was ever rendered).

Notes for review

  • This changes what existing installs resolve as latest, with no migration: a project whose newest tag is a prerelease will move its frontpage card, /latest/ permalink and search default down to the newest release. That is the point of the issue, but it is worth eyeballing on staging against real projects before it goes near prod.
  • Built-in docs updated in the same PR: how-to/version-retention.md (new badge table, a new section on what the pattern decides beyond deletion, the pin rule) and how-to/pin-versions.md.
  • Every new assertion was confirmed to fail against the old code before being kept.

🤖 Written with Claude Code

Closes #157. Three commits, smallest to largest. ## 1. Nothing in the version list is red any more Red meant "PDF" — a file type presented as a verdict — and expiry was red too, so both shouted at the same volume for very different things. | Badge | Was | Now | |---|---|---| | PDF | red `#dc2626` | grey `#6b7280` | | Expires… | `--color-danger` | `--color-warning` (orange) | | Pinned | primary blue | purple `#7c3aed` | | Temp. latest | orange | pink `#db2777` | | Latest | green | green (unchanged — the one the system works out on its own) | | **No expiration** | — | new, outlined grey | | Download | grey `btn-secondary` | primary blue + a download icon | The six badges each carried their own copy of the same six chip properties; those move to the `.version-badge` base class the templates already apply, so a variant only names its colours. Also: - **`EXPIRES SOON` → `EXPIRES TODAY`.** The window has already closed by then; the next hourly pass takes it. - **`NO EXPIRATION`** on versions the pattern keeps, instead of leaving the reader to infer it from an absent badge. Shown only where it means something — with retention off nothing expires, so it would otherwise sit on every row of every project. A new test in `internal/templates` cross-checks every `version-badge-*` class the templates name against a rule in `style.css`. A class name in a template is only half a promise, and an unstyled chip renders fine and passes every handler test. ## 2. A temporary pin protects its version while it is held #141 exempted permanently pinned versions from retention and reasoned that temporary pins needed no protection, since the next upload clears them anyway. That reads the pin backwards, as you pointed out: a temp pin whose version gets collected while the pin still points at it is a pin that did nothing, and the reader lands somewhere else with no sign anything moved. Any pin is now exempt for as long as it is held, and the protection ends *with* the pin rather than outliving it — both upload paths clear a temporary pin **before** `enforceRetentionPolicy` runs, so a version already past its window goes on that same pass. `TestTemporaryPinDoesNotSurviveRetention` is replaced by its opposite, plus `TestUnpinningReturnsAVersionToRetention` to pin down that the protection really is temporary. ## 3. Latest is the newest release, not the newest tag The real bug in the issue. A prerelease outsorts every release below it, so a project publishing `v2.0.0-rc1` alongside `v1.9.0` had the *candidate* as its latest — on the frontpage card, at `/project/{slug}/latest/`, in search by default, badged on the project page — right up until retention deleted it for not being a release. The keep pattern is the project's own definition of a real release, so it decides. `latestVersionTag` now resolves, in order: 1. a pin, if set — an explicit statement about where readers land; 2. the newest tag the keep pattern matches; 3. the sorted newest, if the pattern describes none of the project's tags — a project with versions must have a latest. One rule in one place: the project page was computing its own latest inline and could disagree with the permalink it prints two lines above the version list. It calls the shared helper now, and its unused `LatestVersion` template key goes with it (only `EffectiveLatest` was ever rendered). ## Notes for review - **This changes what existing installs resolve as latest**, with no migration: a project whose newest tag is a prerelease will move its frontpage card, `/latest/` permalink and search default down to the newest release. That is the point of the issue, but it is worth eyeballing on staging against real projects before it goes near prod. - Built-in docs updated in the same PR: `how-to/version-retention.md` (new badge table, a new section on what the pattern decides beyond deletion, the pin rule) and `how-to/pin-versions.md`. - Every new assertion was confirmed to fail against the old code before being kept. 🤖 Written with [Claude Code](https://claude.com/claude-code)
Red meant "PDF". A file type is not a verdict, and a red chip next to every
PDF version read as an error the reader had to check. Expiry was red too, so
the two shouted at the same volume for very different things.

The palette now says what each badge is: grey for the file type, orange for a
deadline, purple and pink for the two pins a human chose, green for the Latest
the system worked out on its own. The six badges also shared one copy of the
same six chip properties each; those move to the .version-badge base class the
templates already apply, so a variant only names its colours.

Two things the list did not say out loud:

- A version the keep pattern keeps now carries a "No expiration" badge instead
  of leaving the reader to infer it from an absent one. Outlined rather than
  solid — it marks the absence of a deadline. It appears only where it means
  something: with retention off nothing expires, so the badge on every row of
  every project would be noise.
- "Expires soon" is now "Expires today". The window has already closed by
  then; the next hourly pass takes it.

Download is what a reader comes to a version row to do, so it gets the primary
colour and a download icon rather than the same grey as Unpin.

A class name in a template is only half a promise, so a new test cross-checks
every version-badge class the templates name against a rule in style.css —
an unstyled chip renders and passes every handler test otherwise.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marcel M. Otte <marcel.otte@mmo.to>
#141 stopped retention from collecting a permanently pinned version out from
under its pin, and reasoned that a temporary pin needed no such protection: it
is cleared by the next upload anyway, so it does not claim the version is
worth keeping.

That reads the pin backwards. The pin is the claim — an editor pinning an old
nightly is saying "send readers here, for now". A temporary pin whose version
gets collected while the pin still points at it is a pin that did nothing, and
the reader lands on a different version with no sign anything moved. The
"temporary" in temporary pin is about when the pin ends, not about whether it
means anything while it lasts.

So any pin is exempt for as long as it is held, and the protection ends with
the pin rather than outliving it: both upload paths clear a temporary pin
before enforceRetentionPolicy runs, so a version already past its window goes
on that same pass.

The project page's badges are computed by the mirror of the same rule, so
"No expiration" now covers a temp-pinned version as well.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marcel M. Otte <marcel.otte@mmo.to>
Latest is the newest release, not the newest tag (#157)
All checks were successful
CI / test (pull_request) Successful in 1m25s
CI / build (pull_request) Successful in 49s
CI / docker (pull_request) Has been skipped
58de99e0d5
A prerelease outsorts every release below it, so a project publishing
v2.0.0-rc1 alongside v1.9.0 had the candidate as its latest: named on the
frontpage card, served at /project/{slug}/latest/, searched by default, badged
on the project page. Then retention deleted it, because the keep pattern does
not call a candidate a release — and readers silently moved to a version
nobody chose for them.

The keep pattern is the project's own definition of a real release, so it is
the thing that should decide. latestVersionTag now takes it: a pin still wins
outright, then the newest tag the pattern matches, then — if the pattern
describes none of a project's tags — the sorted newest, because a project with
versions must have a latest.

One rule, applied in one place. The project page was computing its own latest
inline and could disagree with the permalink it prints two lines above the
version list; it calls the shared helper now. Its unused "LatestVersion"
template key goes with it — only EffectiveLatest was ever rendered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Marcel M. Otte <marcel.otte@mmo.to>
qwc merged commit 39aa837fc6 into main 2026-09-03 11:55:24 +02:00
qwc deleted branch feature/version-badge-colors 2026-09-03 11:55:25 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
qwc-open/asiakirjat!161
No description provided.