Repository navigation
A Time field holds one time, so a 9-to-12 shift needs two fields and reads "9:00 AM · 12:00 PM" #590
Description
Needs your OK: this changes the field editor and the slot editor, and how a shift's time reads on the signup page, in emails and in "Add to calendar". Swap pipeline:proposed for pipeline:ready to have it built.
What happens
A Time field holds one time of day, as HH:MM (TimeConfigSchema in src/schemas/slot-fields.ts, checked by validateOneValue in src/services/slot-fields.ts). Most shifts have a start and an end. So an organizer building shifts of 9–12, 12–3 and 3–6 has to add two Time fields, such as Starts and Ends, and fill in both on every slot.
The two values then read as two separate things:
- The signup page and the Build tab join a slot's values with " · ". A shift reads "9:00 AM · 12:00 PM", not "9:00 AM – 12:00 PM" (
meta.join(' · ')insrc/app/s/[slug]/signup-view.tsx, and the row summary insrc/components/build-wysiwyg/WysiwygSlot.tsx). - Emails print "Starts: 9:00 AM" and "Ends: 12:00 PM" on separate lines (
slotDetailsinsrc/lib/slot-label.ts). The subject line has "9:00 AM · 12:00 PM". - "Add to calendar" knows only the start.
handleDownloadIcsinsrc/app/s/[slug]/commit-dialog.tsxpasses noend, sobuildIcsinsrc/lib/ics.tsmakes every timed slot one hour long. A 9–12 shift goes into the calendar as 9–10.
The AI paths have the same gap. The field guides for Magic Compose and the AI connector say a time is HH:MM, used for "shift starts" (FIELD_TYPE_GUIDE in src/lib/signup-rules.ts, FIELD_GUIDE in src/mcp/tools/guides.ts). So an AI draft keeps the start and drops the end, or types the range into a text field. #503's check 7 already expects ranges typed into text fields, like "Morning 8–12".
Fix, in outline
- One field, with an optional end.
TimeConfigSchemagains an optionalhasEnd: boolean. It lives in the field'sconfigjsonb, so there is no migration. - The value. A Time field with
hasEndtakes"09:00"or"09:00-12:00". A plain Time field still takes only"09:00".validateOneValuechecks both times withisRealTime, and refuses an end that isn't later than the start. - One place reads the start.
slotTimeOfDayinsrc/lib/reminder-fields.tsreturns the start of a range.extractSlotAt, the signup page'shasTime, reminders and the sign-up lockout then work from the start time, as they do today. - One place shows it.
renderFieldValueinsrc/lib/slot-label.tsshows "9:00 AM – 12:00 PM". The Build tab, the signup page, the emails and their subjects, and the Responses tab all go through it, so they agree. - "Add to calendar" gets the end. A small helper beside
slotTimeOfDaygives the end of the same paired time field, and the dialog passes it tobuildIcsasend. - The Build tab. The field editor (
InlineFieldForm.tsx) shows a "Has an end time" checkbox for a Time field, when adding it and when editing it. The slot editor (SlotEditor.tsx) shows two time boxes for that field, "From" and "To", and saves them as one value. - The AI connector.
FIELD_GUIDEsays that a Time field with an end time takesHH:MM-HH:MM. The tools already accept the config and the values through the shared schemas.
Watch for
- Turning the end time off while slots hold a range is refused by
updateField's existing check ("change would invalidate existing slot values"). Keep that, and show it on the Build tab in plain words. - Reminder timing must not move. The start stays the slot's instant, and a date-only slot still anchors at noon UTC. Pin both with tests.
- The responses download writes values as stored (
src/lib/responses-csv.ts), so a range goes out as09:00-12:00. Update its comment, which says times go out asHH:MM. - Magic Compose is out of scope.
coerceValueinsrc/lib/magic-compose/to-template.tsblanks a time it can't read, and the prompt is pinned by a golden file and evals. Teaching it ranges is a follow-up. Open PRs Let Magic Compose answer with questions, and draft from the answers #573 and Ask a few questions when a Magic Compose description doesn't say what people sign up for #577 changeprompt.ts, and Magic Compose drafts from a prompt too thin to build slots from, instead of asking for more #570's one-day "from" and "to" answers would use it. - Open PRs. Check the page when Publish and share is pressed #580 changes
SlotEditor.tsx. Whichever lands second has a small merge. - Docs. The CHANGELOG.
FIELD_GUIDEis what assistants read, so its change counts as UX. The legal pages need no change: an end time is slot data the organizer types, not data about a person.
Choices this makes
- One field with an end, not a new field type or two linked fields. Emails print each field as one "Label: value" line (CLAUDE.md, Email), and the code avoids rules that guess which fields go together. Two linked fields would need such a rule. A new type would double every
fieldType === 'time'check, including the reminder pairing inpairedTimeField. - Stored as
"HH:MM-HH:MM", a string like every other value. The Build tab's rows hold strings, and the download and grouping keys keep working. An object,{ start, end }, is the alternative. - The end must be later than the start. Overnight shifts (10 PM – 2 AM) are left for later. They would need an end before the start to mean "the next day", in the wording and in the calendar export.
- Shown as "9:00 AM – 12:00 PM", with an en dash, everywhere.
- Existing signups keep their two fields. Nothing merges a Starts and Ends pair.
- The checkbox says "Has an end time".
Done when
- A Time field with "Has an end time" saves
09:00-12:00from the Build tab. The slot reads "9:00 AM – 12:00 PM" on the Build tab, the signup page, the confirmation and reminder emails, and the Responses tab. - "Add to calendar" for that slot exports 9:00 to 12:00.
- Unit tests:
validateOneValuetakes a start alone and a range, and refuses an end at or before the start, and a range in a plain Time field.slotTimeOfDayreturns the start.renderFieldValueshows the range. - DB tests: a slot with a range gets the same
slot_atas a slot with only its start. Turning the end time off while a slot holds a range is refused. pnpm lint && pnpm typecheck && pnpm knip && pnpm testandpnpm test:dbpass.
Found by the UX walkthrough (scenario O2) on 2026-10-24, and checked against the code on main at 4ed27a9.
Generated by Claude Code
Activity