| 1 | # Product |
| 2 | |
| 3 | <!-- impeccable:product-schema 1 --> |
| 4 | |
| 5 | ## Platform |
| 6 | |
| 7 | web |
| 8 | |
| 9 | ## Users |
| 10 | |
| 11 | **Not yet confirmed by Paul.** Written from the suite's own pattern — |
| 12 | docs and sheets both ruled the same way on 2026-09-01 — and flagged here |
| 13 | rather than presented as settled. |
| 14 | |
| 15 | Paul and his collaborators, dogfooding. Real use, small N. Design for |
| 16 | the people actually in it rather than a hypothesised market: a feature |
| 17 | nobody here uses is not evidence of anything. |
| 18 | |
| 19 | They are running the CARLOS suite already, so Calendar is not entered |
| 20 | cold. A person arrives with a session and the calendar is one of several |
| 21 | apps on one instance. |
| 22 | |
| 23 | Beyond the team there is one further audience, and it is not a person: |
| 24 | a **subscribed calendar client** — Google, Apple, Thunderbird — reading |
| 25 | a feed URL. It cannot sign in, it polls forever, and it is the only |
| 26 | consumer of this app that will never see a page. The export surface is |
| 27 | designed for it rather than as an afterthought to the UI. |
| 28 | |
| 29 | Guests and invitations do not exist yet. That is a deferral with a |
| 30 | written trigger, not an oversight — see AGENTS.md. |
| 31 | |
| 32 | ## Product Purpose |
| 33 | |
| 34 | A calendar for one team, on that team's own instance, whose events are |
| 35 | sealed at rest. |
| 36 | |
| 37 | Success is that a team can run its actual week — see the day, add |
| 38 | something, be reminded, and subscribe from the calendar app they already |
| 39 | use — without the storage layer ever holding a plaintext title, and |
| 40 | without the product lying to them about what that does and does not |
| 41 | protect. |
| 42 | |
| 43 | ## Positioning |
| 44 | |
| 45 | Four claims a neighbouring product could not truthfully copy: |
| 46 | |
| 47 | 1. **Sealed at rest, honestly scoped.** The bucket never holds a |
| 48 | plaintext title, place or note. The instance holds the key while it |
| 49 | runs, so a compromised live host *can* read a calendar — and the |
| 50 | README says so. **We never claim "we can't read your data."** |
| 51 | |
| 52 | The honest scoping matters more here than in sheets, because a |
| 53 | calendar's plaintext shape is *free/busy*: publish this database and |
| 54 | the whole team's availability is legible even though no event's |
| 55 | content is. That is a bigger leak than "the size of every sheet", and |
| 56 | AGENTS.md says so in those words rather than letting the rule be read |
| 57 | as covering it. |
| 58 | |
| 59 | 2. **No affordance is script-required — including the grid.** Overlapping |
| 60 | events in a week view are normally packed by JavaScript measuring |
| 61 | boxes in a browser. Here the server does it, and click-a-time-to- |
| 62 | create is 48 ordinary links per day. The scriptless page is the |
| 63 | product, not the fallback. |
| 64 | |
| 65 | 3. **The record is what you said, not when it happens.** An event is a |
| 66 | wall-clock time plus a zone; the UTC instant is a derived index. A |
| 67 | tzdata update moves the index and leaves the meeting at nine. Most |
| 68 | calendars store the instant and cannot answer what was meant. |
| 69 | |
| 70 | 4. **Reminders that survive a machine that is asleep.** The instance |
| 71 | hibernates, so an in-process timer is not a scheduler. Three |
| 72 | independent paths fire a reminder, and the product states when one |
| 73 | arrived late rather than implying it was on time. |
| 74 | |
| 75 | ## Operating Context |
| 76 | |
| 77 | - **One binary per instance**, `CGO_ENABLED=0`, cgo-free SQLite, |
| 78 | deployed to CARLOS. Review on a deployed canary, never localhost. |
| 79 | - **The instance hibernates**, and that is a product constraint rather |
| 80 | than an implementation detail: it is why reminders are built the way |
| 81 | they are, and why a subscription feed's poll count is shown to the |
| 82 | person deciding whether to keep it. |
| 83 | - Live at `https://calendar.bds.oncarlos.com`; console page at |
| 84 | `https://console.carloku.com/a/bds/apps/calendar`. |
| 85 | |
| 86 | ## The experience this is trying to produce |
| 87 | |
| 88 | A calendar's job is not to list events. It is to answer "how is my day" |
| 89 | — and the feeling a day produces is whether there is room in it. |
| 90 | |
| 91 | So **Today opens with a sentence**, not a grid: |
| 92 | |
| 93 | > Wednesday. Four things; the first at 09:30, the last ends at 17:00. |
| 94 | > That leaves two hours and 15 minutes unbooked between them. |
| 95 | |
| 96 | and when there is none: |
| 97 | |
| 98 | > There is no unbooked hour between them. |
| 99 | |
| 100 | which is true, slightly alarming, and the day the person is actually |
| 101 | having. The arithmetic counts overlapping meetings once and excludes |
| 102 | notes, because a birthday does not make a Tuesday fuller. |
| 103 | |
| 104 | **Three kinds, because a person makes the distinction whether or not the |
| 105 | software does.** A *meeting* has other people in it and being late |
| 106 | matters. A *commitment* is time promised to yourself: it fills the day, |
| 107 | but only you are inconvenienced when it moves. A *note* is true of a day |
| 108 | without occupying any of it. They differ in reminder defaults, in |
| 109 | whether they count toward booked hours, and in how they draw. |
| 110 | |
| 111 | **The past is written in the past tense.** "Standup, ended 09:30" rather |
| 112 | than "09:30". A calendar that describes this morning in the future tense |
| 113 | makes you read the clock to know where you are. |
| 114 | |
| 115 | **Deleting a series asks once, and the expensive answer is last with its |
| 116 | count out loud** — "All 52 of them", never a bare "All". |
| 117 | |
| 118 | **Your zone is the headline; another zone is a whisper.** Times are shown |
| 119 | in the reader's own zone always. Where an event was authored elsewhere, |
| 120 | that is a quiet line beside it, never the primary. |
| 121 | |
| 122 | ## Accessibility |
| 123 | |
| 124 | WCAG 2.2 AA, no exemptions. 24×24 minimum targets, 320px reflow, colour |
| 125 | never carrying meaning alone — every chip prints its own time and title, |
| 126 | cancelled says the word, today has three signals, and the month cell |
| 127 | prints its date whatever the theme did. |
| 128 | |
| 129 | At 320px the month reflows to a stacked list of days with something in |
| 130 | them. The week keeps its two dimensions and scrolls: a week view IS a |
| 131 | two-dimensional comparison, and flattening it produces the agenda, which |
| 132 | already exists and is better at being a list. |
| 133 | |
| 134 | ## What is deliberately not here |
| 135 | |
| 136 | Each of these is a decision, listed so its absence is legible, and each |
| 137 | has a trigger in AGENTS.md or the brainstorm rather than a vague "later": |
| 138 | |
| 139 | attendees and invitations; guests; web push; external feed subscription |
| 140 | (holidays); VTIMEZONE emission; expanding foreign RRULEs; drag to move |
| 141 | and resize; search; working-hours shading; free/busy across members; |
| 142 | attachments; print; "find a time"; anything with "AI" in it. |
| 143 | |
| 144 | ## Voice |
| 145 | |
| 146 | Plain, exact, and never reassuring beyond the facts. Where a feature has |
| 147 | a cost, the copy states it in the same breath — the share page names |
| 148 | both what a subscription link gives away and what it costs to run before |
| 149 | offering the button, and the import page says what will not come across |
| 150 | before the upload rather than after. |
| 151 | |