oficina / calendar Public

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

Plain git — no account needed to clone.

Download

Download this file

Product

<!-- impeccable:product-schema 1 -->

Platform

web

Users

Not yet confirmed by Paul. Written from the suite's own pattern — docs and sheets both ruled the same way on 2026-09-01 — and flagged here rather than presented as settled.

Paul and his collaborators, dogfooding. Real use, small N. Design for the people actually in it rather than a hypothesised market: a feature nobody here uses is not evidence of anything.

They are running the CARLOS suite already, so Calendar is not entered cold. A person arrives with a session and the calendar is one of several apps on one instance.

Beyond the team there is one further audience, and it is not a person: a subscribed calendar client — Google, Apple, Thunderbird — reading a feed URL. It cannot sign in, it polls forever, and it is the only consumer of this app that will never see a page. The export surface is designed for it rather than as an afterthought to the UI.

Guests and invitations do not exist yet. That is a deferral with a written trigger, not an oversight — see AGENTS.md.

Product Purpose

A calendar for one team, on that team's own instance, whose events are sealed at rest.

Success is that a team can run its actual week — see the day, add something, be reminded, and subscribe from the calendar app they already use — without the storage layer ever holding a plaintext title, and without the product lying to them about what that does and does not protect.

Positioning

Four claims a neighbouring product could not truthfully copy:

  1. Sealed at rest, honestly scoped. The bucket never holds a plaintext title, place or note. The instance holds the key while it runs, so a compromised live host can read a calendar — and the README says so. We never claim "we can't read your data."

The honest scoping matters more here than in sheets, because a calendar's plaintext shape is free/busy: publish this database and the whole team's availability is legible even though no event's content is. That is a bigger leak than "the size of every sheet", and AGENTS.md says so in those words rather than letting the rule be read as covering it.

  1. No affordance is script-required — including the grid. Overlapping events in a week view are normally packed by JavaScript measuring boxes in a browser. Here the server does it, and click-a-time-to- create is 48 ordinary links per day. The scriptless page is the product, not the fallback.
  1. The record is what you said, not when it happens. An event is a wall-clock time plus a zone; the UTC instant is a derived index. A tzdata update moves the index and leaves the meeting at nine. Most calendars store the instant and cannot answer what was meant.
  1. Reminders that survive a machine that is asleep. The instance hibernates, so an in-process timer is not a scheduler. Three independent paths fire a reminder, and the product states when one arrived late rather than implying it was on time.

Operating Context

  • One binary per instance, CGO_ENABLED=0, cgo-free SQLite, deployed to CARLOS. Review on a deployed canary, never localhost.
  • The instance hibernates, and that is a product constraint rather than an implementation detail: it is why reminders are built the way they are, and why a subscription feed's poll count is shown to the person deciding whether to keep it.
  • Live at https://calendar.bds.oncarlos.com; console page at https://console.carloku.com/a/bds/apps/calendar.

The experience this is trying to produce

A calendar's job is not to list events. It is to answer "how is my day" — and the feeling a day produces is whether there is room in it.

So Today opens with a sentence, not a grid:

> Wednesday. Four things; the first at 09:30, the last ends at 17:00. > That leaves two hours and 15 minutes unbooked between them.

and when there is none:

> There is no unbooked hour between them.

which is true, slightly alarming, and the day the person is actually having. The arithmetic counts overlapping meetings once and excludes notes, because a birthday does not make a Tuesday fuller.

Three kinds, because a person makes the distinction whether or not the software does. A meeting has other people in it and being late matters. A commitment is time promised to yourself: it fills the day, but only you are inconvenienced when it moves. A note is true of a day without occupying any of it. They differ in reminder defaults, in whether they count toward booked hours, and in how they draw.

The past is written in the past tense. "Standup, ended 09:30" rather than "09:30". A calendar that describes this morning in the future tense makes you read the clock to know where you are.

Deleting a series asks once, and the expensive answer is last with its count out loud — "All 52 of them", never a bare "All".

Your zone is the headline; another zone is a whisper. Times are shown in the reader's own zone always. Where an event was authored elsewhere, that is a quiet line beside it, never the primary.

Accessibility

WCAG 2.2 AA, no exemptions. 24×24 minimum targets, 320px reflow, colour never carrying meaning alone — every chip prints its own time and title, cancelled says the word, today has three signals, and the month cell prints its date whatever the theme did.

At 320px the month reflows to a stacked list of days with something in them. The week keeps its two dimensions and scrolls: a week view IS a two-dimensional comparison, and flattening it produces the agenda, which already exists and is better at being a list.

What is deliberately not here

Each of these is a decision, listed so its absence is legible, and each has a trigger in AGENTS.md or the brainstorm rather than a vague "later":

attendees and invitations; guests; web push; external feed subscription (holidays); VTIMEZONE emission; expanding foreign RRULEs; drag to move and resize; search; working-hours shading; free/busy across members; attachments; print; "find a time"; anything with "AI" in it.

Voice

Plain, exact, and never reassuring beyond the facts. Where a feature has a cost, the copy states it in the same breath — the share page names both what a subscription link gives away and what it costs to run before offering the button, and the import page says what will not come across before the upload rather than after.