Smoke-sweep fixes: exact saves, local dates, safer remove, readable markers
Build image / build-and-push (push) Successful in 2m55s
Gadfly review (reusable) / review (pull_request) Successful in 8m59s
Adversarial Review (Gadfly) / review (pull_request) Successful in 8m59s

- Garden and plant dialogs keep centimeters as the source of truth
  (LengthField in lib/units.ts): a no-change Save no longer rewrites
  900 cm as 899.922 or a 45 cm spacing as 44.958, bumping versions and
  writing bogus history entries on the way.
- The UI stamps every date with the browser's local day (lib/dates.ts).
  Journal notes already did; plop placement, fill and removal now do too,
  so a 9 pm placement isn't "planted tomorrow". The fill endpoint gained an
  optional plantedAt; API and agent callers still default to UTC today.
- Removing an object that holds plants asks first and says how many go
  with it. An empty one still goes straight away (one Undo restores it).
- The expanded plant card's action row wraps instead of clipping "Delete".
- Monogram lettering switches to a dark ink on pale marker colors (garlic,
  cabbage, marigold) instead of near-white on near-white.
- Copy-as-plan proposes the next free year and warns when the typed name
  already exists, so two gardens can't both read as "the 2027 plan".
- Plan cards show the base name with a "2027 plan" tag, so the year — the
  point of the name — survives truncation.
- A rejected model spec now says which model and why: a wrapped
  ErrInvalidInput's reason reaches the client as the 400's message, and the
  Settings field shows it inline instead of toasting "invalid input".

Also defuses a clock bomb in TestRemainingReturnsWhenAPlantingIsRemoved,
which only passed while the real date was before 2026-08-01.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-22 22:11:12 -04:00
co-authored by Claude Fable 5
parent 05392ee0db
commit 27f658c1f7
36 changed files with 508 additions and 125 deletions
+32
View File
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest'
import {
cmFromFtIn,
convertDimensionField,
dimensionField,
editDimensionField,
editSpacingField,
spacingField,
cmFromMeters,
cmFromSpacing,
dimensionInputMode,
@@ -242,3 +247,30 @@ describe('formatSize / formatLength', () => {
expect(formatLength(1219, 'imperial')).toBe('40')
})
})
describe('LengthField', () => {
it('keeps the stored centimeters through a unit switch and back', () => {
let f = dimensionField(900, 'imperial')
expect(f.text).toBe('29 6.3″')
f = convertDimensionField(f, 'metric')
expect(f.text).toBe('9')
f = convertDimensionField(f, 'imperial')
expect(f).toEqual({ text: '29 6.3″', cm: 900 })
})
it('moves the centimeters only when the text is edited', () => {
expect(editDimensionField("15' 6\"", 'imperial').cm).toBe(472.44)
const typo = editDimensionField('nope', 'imperial')
expect(typo.cm).toBeNull()
// A typo survives a unit switch as typed rather than turning into a number.
expect(convertDimensionField(typo, 'metric')).toEqual(typo)
})
it('spacing: 45 cm reads 17.7 in and stays 45 cm until typed over', () => {
const f = spacingField(45, 'imperial')
expect(f).toEqual({ text: '17.7', cm: 45 })
expect(editSpacingField('18', 'imperial').cm).toBe(45.72)
expect(editSpacingField('', 'imperial').cm).toBeNull()
expect(editSpacingField('25', 'metric').cm).toBe(25)
})
})