motir-core: the retire control in the list and detail, honouring the page-state contract
The control itself, in the treatment the design already drew — the sibling story rendered the retired state, this card wires the action that produces it.
Retire and un-retire from the same place, in the list row and in the detail view.
The page-state contract, and it has two halves here — the recurring bug is assuming router.refresh() covers both:
- The row's own state — the mutation response IS the confirmation. Keep the optimistic value; do not refresh it, because a refresh re-reads and produces a visible revert.
- Anything server-rendered elsewhere on the surface — a count of active lessons, a summary line beside the setting — needs
router.refresh(), which is the only thing that reaches it.
A mutation touching both does both.
Show who and when. Once retired, the row and the detail say who switched it off and when, from the data the write recorded.
Refusals are rendered, not thrown. The route returns distinguishable refusals; a global lesson or a stale row should produce a sentence a person can act on, not a toast saying something went wrong.
No new permission logic in the client. The control's presence follows what the server said the user can do; it is not a second implementation of the rule.
⚠️ Amendment (2026-08-23) — Apply again appears on BOTH not-applied rows
Authored 2026-08-21; the parent story MOTIR-3330's 2026-08-23 amendment outranks this card's original wording, and it matches what the design already drew.
design/ai-settings/design-notes.md §L6 draws two not-applied rows — Not applied (switched off) and Not seen in 90 days (aged out) — and gives both the action Apply again. The control therefore does not branch on which badge it sits under: it posts apply, and the server decides whether that means "clear the retirement" or "exempt this row from the clock". On an aged-out row the badge disappears on success, because the sentence it makes is no longer true of what the planner is being told.
Copy is already fixed by §L9 — row action Stop applying / Apply again, badges Not applied / Not seen in {n} days, detail action Stop applying this lesson, and the a11y name Stop applying {takeaway} (§L11). No new primitive (§L8).
Acceptance criteria
- Retire and un-retire work from both the list row and the detail view, in the design's treatment.
- The acted-on row keeps its optimistic state and is not refreshed into a revert.
- Any server-rendered summary on the surface refreshes; both halves asserted.
- The retired row shows the actor and the timestamp.
- Each typed refusal renders as its own message.
- A user without the retire permission sees no control, and the route refuses independently.
- Copy in
enandzh; the catalog-parity gate passes. - A "How to test" section in the PR body — route, role precondition, the seed that produces a retirable lesson, and what to click and see including a refusal case.
Context refs
motir-coredesign/ai-settings/— the retired treatment and affordance, already drawn.motir-coreapp/(authed)/settings/project/ai-planning/— the surface and its i18n namespace.- MOTIR-3338 — the list and detail this adds the action to.
- MOTIR-3345 — the route, its guard and its typed refusals.