Smoke-sweep fixes: exact saves, local dates, safer remove, readable markers
- 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:
@@ -15,7 +15,7 @@ import {
|
||||
type PlantInput,
|
||||
} from '@/lib/plants'
|
||||
import { safeExternalUrl } from '@/lib/seedLots'
|
||||
import { cmFromSpacing, spacingFromCm, spacingUnitLabel, type UnitPref } from '@/lib/units'
|
||||
import { editSpacingField, spacingField, spacingUnitLabel, type LengthField, type UnitPref } from '@/lib/units'
|
||||
import { ColorSwatches, CURATED_SWATCHES, expandHex } from './ColorSwatches'
|
||||
|
||||
// Markers are monograms now, but the API still carries an icon per plant; a
|
||||
@@ -26,8 +26,10 @@ const categoryOptions = PLANT_CATEGORIES.map((c) => ({ value: c, label: CATEGORY
|
||||
/**
|
||||
* "A new plant" — or edit (`plant`), or a fresh create prefilled from another
|
||||
* (`template`, the Duplicate action; the only way to customize a built-in).
|
||||
* Spacing is typed in the page's unit and stored in centimeters. A 409 rebases
|
||||
* onto the server's current row.
|
||||
* Spacing is typed in the page's unit and stored in centimeters — as a
|
||||
* LengthField, so a Save that didn't touch it sends the centimeters that were
|
||||
* loaded rather than re-parsing "17.7 in" into 44.958. A 409 rebases onto the
|
||||
* server's current row.
|
||||
*/
|
||||
export function PlantDialog({
|
||||
plant,
|
||||
@@ -48,7 +50,7 @@ export function PlantDialog({
|
||||
|
||||
const [name, setName] = useState(source ? (isEdit ? source.name : `${source.name} (copy)`) : '')
|
||||
const [category, setCategory] = useState<PlantCategory>(source?.category ?? 'vegetable')
|
||||
const [spacing, setSpacing] = useState(String(spacingFromCm(source?.spacingCm ?? 30, unit)))
|
||||
const [spacing, setSpacing] = useState<LengthField>(() => spacingField(source?.spacingCm ?? 30, unit))
|
||||
const [color, setColor] = useState(expandHex(source?.color ?? CURATED_SWATCHES[0]))
|
||||
const [days, setDays] = useState(source?.daysToMaturity != null ? String(source.daysToMaturity) : '')
|
||||
const [vendor, setVendor] = useState(source?.vendor ?? '')
|
||||
@@ -68,8 +70,8 @@ export function PlantDialog({
|
||||
setFormError('Give the plant a name.')
|
||||
return
|
||||
}
|
||||
const spacingCm = cmFromSpacing(parseFloat(spacing), unit)
|
||||
if (!Number.isFinite(spacingCm) || spacingCm < 1) {
|
||||
const spacingCm = spacing.cm
|
||||
if (spacingCm === null || spacingCm < 1) {
|
||||
setFormError(`Spacing must be at least 1 ${unitLabel}.`)
|
||||
return
|
||||
}
|
||||
@@ -107,7 +109,7 @@ export function PlantDialog({
|
||||
setVersion(current.version)
|
||||
setName(current.name)
|
||||
setCategory(current.category)
|
||||
setSpacing(String(spacingFromCm(current.spacingCm, unit)))
|
||||
setSpacing(spacingField(current.spacingCm, unit))
|
||||
setColor(expandHex(current.color))
|
||||
setDays(current.daysToMaturity != null ? String(current.daysToMaturity) : '')
|
||||
setVendor(current.vendor)
|
||||
@@ -142,8 +144,8 @@ export function PlantDialog({
|
||||
step="any"
|
||||
min="1"
|
||||
required
|
||||
value={spacing}
|
||||
onChange={(e) => setSpacing(e.target.value)}
|
||||
value={spacing.text}
|
||||
onChange={(e) => setSpacing(editSpacingField(e.target.value, unit))}
|
||||
wrapperClassName="flex-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user