oficina / calendar Public

Clone
git clone https://amadan.net/oficina/calendar

Plain git — no account needed to clone.

Download

Download this file

1# Quick add — design
2
3**Status:** specified 2026-09-02, before the code this time. Authored by
4Claude Opus 5. Not yet reviewed by a model at least as capable as the
5author, and not yet approved by Paul.
6
7Asked for: a small input beside "New event" that expands into a
8popover textarea, takes natural language — "Meeting with Dermot Lacey
9at 3pm in McCloskeys" — parses it AS YOU TYPE into a detail pane below
10the input, and creates the event. Email addresses in the text become
11invitees. Start, end and durations all parse.
12
13---
14
15## 1. One parser, and it is the Go one
16
17**The parse happens on the server. There is no JavaScript parser.**
18
19The live pane is a debounced `fetch` to `POST /events/quick/parse`
20which returns the pane as an HTML fragment. Typing costs a round trip
21of about a form submission; the pane is a preview of what the server
22will actually do.
23
24Rejected: **parsing in JavaScript for instant feedback.** Two
25implementations of "next Tuesday at half three" is two answers to the
26same sentence, and the one the person read in the pane would not be the
27one that got saved. A calendar that shows you 15:30 and books 03:30 has
28done something worse than being slow. The scriptless floor needs the Go
29parser regardless, so the JS one would be a second copy of a thing we
30already have to have.
31
32Rejected: **compiling the Go parser to WASM** so one implementation can
33run in both places. It is the right shape and the wrong weight — a
34megabyte of WASM on every page load to save 80ms on a keystroke.
35
36## 2. The scriptless floor is a working feature, not a fallback
37
38- The popover is a `<details>`. It opens with no script.
39- The textarea is inside an ordinary `<form>` that POSTs to
40 `/events/quick`.
41- Submitting **creates the event** and redirects to it.
42
43With script, the pane has already shown what will happen, so the submit
44is a confirmation. Without script, the person has not seen the pane —
45so the event page they land on is the confirmation, and it is one click
46from Edit and one from Delete. Creating an event is cheap and
47reversible, which is what makes "create, then show" honest here where
48it would not be for a destructive action.
49
50Rejected: **always landing on a prefilled `/events/new` form.** Safer,
51and it makes the fast path two steps for everybody in order to protect
52the case where the parse was wrong — which the pane already covers for
53anyone who has script, and which Edit covers for anyone who does not.
54
55## 2.1 Enter adds it
56
57**Enter submits. Shift+Enter is a newline. Cmd/Ctrl+Enter also submits.**
58The Slack convention, plus the chord people arrive from GitHub and
59Linear expecting.
60
61Enter can fire before the debounced parse has landed, so the pane may
62never have shown that sentence. Accepted, because **the pane is a
63preview and never a gate**: the server parses again on submit, so
64racing it can only mean "you did not get to look", never "the wrong
65event was created". Anyone who wants to look pauses — the same pause
66the debounce is waiting for.
67
68The hint that says so is revealed by `quickadd.js`, never rendered by
69the template. With no script Enter is an ordinary newline, and a page
70advertising a shortcut it cannot honour is worse than a silent one. The
71browser gate asserts the hint is absent with JavaScript off.
72
73The Add button is untouched. This is an enhancement on a floor that
74already worked.
75
76## 2.2 A run of days is a run of days
77
78**A date RANGE makes one event spanning those days.** "london
79tues–thurs" is Tuesday, Wednesday and Thursday in London.
80
81Every range pattern is tried before any single-date one, and that
82ordering is the fix rather than a detail. Run the singles first and
83"12–14 March" matches "14 March" — the range's own END — which books one
84day and leaves a stray "12" in the title. The reported symptom was
85exactly this shape: "london tues–thurs" became a Tuesday titled
86"london –thurs".
87
88**A range with no clock in it is ALL DAY**, and the pane says so in
89words. A three-day trip is not an hour on the Tuesday with two days of
90silence after it.
91
92**A range WITH a clock is one continuous block**: the start time on the
93first day, the end time on the last. "Summit Monday to Wednesday
949am–5pm" is 09:00 Monday until 17:00 Wednesday. That is the literal
95reading, and it is not the only possible one — somebody may have meant
96nine to five on each of three days. That reading is a recurrence, and
97ruling 4 forbids inventing a rule the picker cannot re-display, so the
98literal one wins and the pane prints both ends in full for anyone who
99meant the other.
100
101**A range only ever matches around a calendar word** — a month name, a
102weekday name, or a slash. A digits-only range is a TIME, always:
103"Retro 3-4pm" must never become the third to the fourth.
104
105The separators are `-`, `–`, `—`, `to`, `until`, `till`, `through` and
106`thru`. The en dash is not decoration: macOS substitutes one for a
107typed hyphen mid-sentence, so a parser that only knows ASCII fails on
108text the person watched themselves type correctly.
109
110**The end date stored is EXCLUSIVE**, like every other range in this
111app, and **the pane prints the INCLUSIVE last day**. Showing the stored
112end would tell somebody their trip runs a day longer than it does.
113
114## 3. Nothing is guessed silently
115
116Every defaulted field is **named in the pane in words**, not implied:
117
118- No time found → the next half hour, one hour long, and the pane says
119 so.
120- A start with no end → one hour.
121- Anything the parser could not place goes in the title, and the pane
122 shows the title it built so a swallowed word is visible.
123
124The pane is the contract. If it is not in the pane, the parser did not
125do it.
126
127## 4. Attendees are recorded, NOT invited
128
129An address in the text becomes an attendee row. **No mail is sent to
130them, and the pane says that in those words.**
131
132This is the one place this feature can do real harm: somebody types
133"lunch with doc@tito.io", reads the word "invitee", and believes Doc
134has been told. Doc has not been told. Until there is an invitation flow
135with an RSVP and a sender identity, the honest label is "Who's coming
136(not emailed)" and the pane repeats it.
137
138**Attendee addresses are SEALED**, like a title. AGENTS.md's falsifiable
139form gives up "who is on the team" — team members are already plaintext
140in `people`. An external attendee is not on the team, and who this team
141meets with is exactly the kind of fact the one rule exists to keep out
142of the bucket. A hash column carries dedupe so sealing costs no lookup.
143
144## 5. The recurrence subset is the one that already exists
145
146Ruling 4 stands: quick add offers no rule the picker does not. "every
147Tuesday" maps to the existing weekly choice; anything else is left in
148the title rather than invented, and the pane shows it sitting there.
149
150## 6. What the parser takes
151
152Times `3pm`, `15:00`, `3:30pm`, `half three`; time ranges `3-4pm`, `3pm
153to 4:30`, `from 3 until 4`; durations `for 90 minutes`, `for 2h`, `for
154an hour`; dates `today`, `tomorrow`, `on Friday`, `next Tuesday`, `12
155March`, `12/3`; DATE RANGES `tues–thurs`, `Monday to Wednesday`,
156`12–14 March`, `March 12-14`, `30 September to 2 October`,
157`12/10-14/10`; `all day`; locations `in X` / `at X` where X is not a
158time; attendees by email address; repeats `every day` / `every Tuesday`
159/ `every week` / `every month` / `every year`.
160
161**`at` is ambiguous on purpose and time wins.** "at 3" is a time, "at
162McCloskeys" is a place, and the rule is: try to read a time, and only
163if that fails treat the rest as a place. Getting this backwards puts
164"3pm" in the location field of every event anybody types.
165
166## 7. Testing
167
168The parser is a pure function of (text, now, zone) and is tested as
169one — a table of sentences against expected fields, including the
170ambiguities above.
171
172**A JS harness is added for the live pane**, approved by Paul in this
173session: Playwright, which drives a real browser in this environment
174where chromedp does not (it parks at `about:blank`; recorded as debt in
175the calendar spec). It asserts the enhanced path — type, wait, read the
176pane — and the scriptless path with JavaScript disabled, which is the
177only way to prove the floor is real rather than claimed.
178