# Calendar

A calendar for one team, on that team's own instance. Part of the
oficina suite, alongside [docs](https://amadan.net/oficina/docs) and
[sheets](https://amadan.net/oficina/sheets).

Live at `https://calendar.bds.oncarlos.com`.

## What it does

Day, week, month and agenda views. Events that repeat. Reminders by
email. Import and export as `.ics`, and subscription links you can paste
into Google or Apple Calendar.

Every screen works with JavaScript switched off — including the week
grid, whose overlapping events are laid out on the server, and
click-a-time-to-create, which is an ordinary link for every half hour of
the day.

## What is sealed, and what is not

**Titles, places and notes are encrypted at rest.** The database file
this instance keeps — the one the platform replicates to object storage
— never holds a readable one. There is a test that greps the raw bytes.

**Times are not.** They cannot be: every view is a range query over
them, the reminder scheduler reads them before the app has opened
anything, and the platform's own timer registry holds every reminder
instant in plaintext regardless. So:

> Publish this database and the worst anyone learns is **the shape of
> the team's time** — when people are busy, for how long, how often —
> and who is on the team. Never what any event is, where it is, or what
> was written about it.

That is a real leak, and a larger one than it first sounds: a team's
free/busy says a lot. It is stated here rather than left for somebody to
discover.

Three more things sit outside the seal:

- **A reminder email carries the title.** That is the product working,
  but the mail relay's logs are not ours.
- **A subscription link puts this calendar's plaintext into whatever
  account you paste it into** — Google, Apple, a phone. The share page
  says so before it offers you the button.
- **A compromised running host reads everything.** The instance holds
  the key while it runs. **We never claim we cannot read your data.**

## Time zones

An event's record is the wall-clock time you typed plus the zone you
meant it in. The UTC instant is derived from those, and only ever used
as an index.

That matters when a government moves a daylight-saving boundary and a
new time zone database ships. A calendar that stored the instant would
now have your 09:00 standup at 08:00, with nothing able to say what was
originally meant. Here the standup is still at 09:00 and only the index
moves.

Your zone is a setting, never guessed from your browser. A calendar that
silently re-times itself because you opened it on holiday is a calendar
that lies to you.

## Repeating events

The repeat picker offers six choices, and they are labelled against the
event you are editing — "Every Wednesday", not "Weekly".

A calendar you import may use a rule more complicated than those six.
It is kept exactly as it arrived and exported unchanged; its future
occurrences are not drawn on the grid, and the event's page shows you
the rule. Nothing is thrown away.

## Reminders

The instance sleeps when nobody is using it, which is what makes it
cheap to run and what makes reminders interesting. An ordinary timer
inside the process would not fire, because the process is not there.

So the app tells the platform when it next has work, and the platform
wakes it then. A six-hourly check is the backstop, and any page view
opportunistically sends anything overdue.

A reminder can still arrive late — a machine has to wake up first. When
that happens the email says so, rather than pretending it was on time.

## Exports and subscriptions

`.ics` export carries IANA time zone identifiers and **no `VTIMEZONE`
block**. Google Calendar, Apple Calendar and Thunderbird resolve zones
from their own databases and are fine. **Outlook desktop may not be** —
it has historically resolved only the zones it finds inside the file. If
you import one of our files into Outlook and see a wrong time, that is
why, and it is worth telling us.

A subscription link is a capability: anyone holding it can read that
calendar, because a calendar app cannot sign in. Only its hash is
stored, so it is shown once when you create it and never again. Revoke
it and it stops working.

Subscribed clients poll constantly — Apple roughly every fifteen minutes
— and each poll wakes this instance. The share page shows you how many
times each link has been polled, because a feed is not free.

## Running it

```sh
make ci        # vet, gofmt, tests, migration check — the one gate
make release   # the stripped linux/arm64 binary
```

Locally:

```sh
go run ./cmd/calendar -env development -addr localhost:8099 -db /tmp/calendar.db
```

`-env development` is the only way to get development behaviour: it is
never honoured from the environment, because platform config *is*
environment variables and a stray `CALENDAR_ENV=development` in
production would open the allowlist and put sign-in links in the log.

In production the app refuses to start without `CALENDAR_ORIGIN`,
`CALENDAR_INSTANCE_KEY`, `CALENDAR_DEK`, `CALENDAR_ALLOWED_ADDRESSES`
and a mail relay. Each refusal names what to set. A misconfigured
instance is down rather than quietly wrong.

## Deploying

```sh
make release
carlos deploy --account oficina --app calendar --channel edge \
  --timeout 5m releases/calendar-linux-arm64
```

Then check the **process**, not the edge:

```sh
curl -sS https://calendar.bds.oncarlos.com/api/version
```

`X-Carlos-Version` is the router's adopted release and flips the moment
the promote lands. The process takes about another 110 seconds to cycle.

`stable` needs a passkey step-up in the console, and so does moving an
instance between `stable` and `edge`. This app runs on `edge`.

## Reading further

- `AGENTS.md` — the binding rules, and the reasoning behind each
- `PRODUCT.md` — what this is for and what it deliberately is not
- `docs/superpowers/specs/` — the design
- `docs/superpowers/brainstorms/` — the raw material, including the
  alternatives that were rejected and why
