oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

1# Calendar
2
3A calendar for one team, on that team's own instance. Part of the
4oficina suite, alongside [docs](https://amadan.net/oficina/docs) and
5[sheets](https://amadan.net/oficina/sheets).
6
7Live at `https://calendar.bds.oncarlos.com`.
8
9## What it does
10
11Day, week, month and agenda views. Events that repeat. Reminders by
12email. Import and export as `.ics`, and subscription links you can paste
13into Google or Apple Calendar.
14
15Every screen works with JavaScript switched off — including the week
16grid, whose overlapping events are laid out on the server, and
17click-a-time-to-create, which is an ordinary link for every half hour of
18the day.
19
20## What is sealed, and what is not
21
22**Titles, places and notes are encrypted at rest.** The database file
23this instance keeps — the one the platform replicates to object storage
24— never holds a readable one. There is a test that greps the raw bytes.
25
26**Times are not.** They cannot be: every view is a range query over
27them, the reminder scheduler reads them before the app has opened
28anything, and the platform's own timer registry holds every reminder
29instant in plaintext regardless. So:
30
31> Publish this database and the worst anyone learns is **the shape of
32> the team's time** — when people are busy, for how long, how often —
33> and who is on the team. Never what any event is, where it is, or what
34> was written about it.
35
36That is a real leak, and a larger one than it first sounds: a team's
37free/busy says a lot. It is stated here rather than left for somebody to
38discover.
39
40Three more things sit outside the seal:
41
42- **A reminder email carries the title.** That is the product working,
43 but the mail relay's logs are not ours.
44- **A subscription link puts this calendar's plaintext into whatever
45 account you paste it into** — Google, Apple, a phone. The share page
46 says so before it offers you the button.
47- **A compromised running host reads everything.** The instance holds
48 the key while it runs. **We never claim we cannot read your data.**
49
50## Time zones
51
52An event's record is the wall-clock time you typed plus the zone you
53meant it in. The UTC instant is derived from those, and only ever used
54as an index.
55
56That matters when a government moves a daylight-saving boundary and a
57new time zone database ships. A calendar that stored the instant would
58now have your 09:00 standup at 08:00, with nothing able to say what was
59originally meant. Here the standup is still at 09:00 and only the index
60moves.
61
62Your zone is a setting, never guessed from your browser. A calendar that
63silently re-times itself because you opened it on holiday is a calendar
64that lies to you.
65
66## Repeating events
67
68The repeat picker offers six choices, and they are labelled against the
69event you are editing — "Every Wednesday", not "Weekly".
70
71A calendar you import may use a rule more complicated than those six.
72It is kept exactly as it arrived and exported unchanged; its future
73occurrences are not drawn on the grid, and the event's page shows you
74the rule. Nothing is thrown away.
75
76## Reminders
77
78The instance sleeps when nobody is using it, which is what makes it
79cheap to run and what makes reminders interesting. An ordinary timer
80inside the process would not fire, because the process is not there.
81
82So the app tells the platform when it next has work, and the platform
83wakes it then. A six-hourly check is the backstop, and any page view
84opportunistically sends anything overdue.
85
86A reminder can still arrive late — a machine has to wake up first. When
87that happens the email says so, rather than pretending it was on time.
88
89## Exports and subscriptions
90
91`.ics` export carries IANA time zone identifiers and **no `VTIMEZONE`
92block**. Google Calendar, Apple Calendar and Thunderbird resolve zones
93from their own databases and are fine. **Outlook desktop may not be** —
94it has historically resolved only the zones it finds inside the file. If
95you import one of our files into Outlook and see a wrong time, that is
96why, and it is worth telling us.
97
98A subscription link is a capability: anyone holding it can read that
99calendar, because a calendar app cannot sign in. Only its hash is
100stored, so it is shown once when you create it and never again. Revoke
101it and it stops working.
102
103Subscribed clients poll constantly — Apple roughly every fifteen minutes
104— and each poll wakes this instance. The share page shows you how many
105times each link has been polled, because a feed is not free.
106
107## Running it
108
109```sh
110make ci # vet, gofmt, tests, migration check — the one gate
111make release # the stripped linux/arm64 binary
112```
113
114Locally:
115
116```sh
117go run ./cmd/calendar -env development -addr localhost:8099 -db /tmp/calendar.db
118```
119
120`-env development` is the only way to get development behaviour: it is
121never honoured from the environment, because platform config *is*
122environment variables and a stray `CALENDAR_ENV=development` in
123production would open the allowlist and put sign-in links in the log.
124
125In production the app refuses to start without `CALENDAR_ORIGIN`,
126`CALENDAR_INSTANCE_KEY`, `CALENDAR_DEK`, `CALENDAR_ALLOWED_ADDRESSES`
127and a mail relay. Each refusal names what to set. A misconfigured
128instance is down rather than quietly wrong.
129
130## Deploying
131
132```sh
133make release
134carlos deploy --account oficina --app calendar --channel edge \
135 --timeout 5m releases/calendar-linux-arm64
136```
137
138Then check the **process**, not the edge:
139
140```sh
141curl -sS https://calendar.bds.oncarlos.com/api/version
142```
143
144`X-Carlos-Version` is the router's adopted release and flips the moment
145the promote lands. The process takes about another 110 seconds to cycle.
146
147`stable` needs a passkey step-up in the console, and so does moving an
148instance between `stable` and `edge`. This app runs on `edge`.
149
150## Reading further
151
152- `AGENTS.md` — the binding rules, and the reasoning behind each
153- `PRODUCT.md` — what this is for and what it deliberately is not
154- `docs/superpowers/specs/` — the design
155- `docs/superpowers/brainstorms/` — the raw material, including the
156 alternatives that were rejected and why
157