oficina / meet Public

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

Plain git — no account needed to clone.

Download

Download this file

1# calls
2
3A [rastrillo](https://github.com/rastrilloorg/rastrillo) app. Read the
4framework's own SKILL.md (repo root, or in the module cache) before
5writing app code — it is the whole app story in ~15KB. The conventions
6below are load-bearing; the framework enforces most of them
7mechanically.
8
9## Layout
10
11- `internal/<app>/` — the app: `models.go` (plain GORM structs
12 + Models), `migrations.go` + `migrations/` (Schema, BootSchema),
13 `app.go` (migrate.Apply + chi router), `handlers.go`,
14 `render.go` + `templates/`. This is SKILL.md's five-file shape.
15- `manifest/` — the declarative path, optional and equal: TOML or .go
16 Resource manifests, each generating a full screen set under `gen/`
17 (see manifest/README.md for the mounting recipe). A hand-written
18 action file at a generated path (carrying `//go:build rastrillo_actions`)
19 takes that one screen over, by existence.
20- `gen/` — generated when manifests are used; never edit. Regenerate
21 with `rastrillo generate`.
22- `internal/calls/seal/` — the app's only encryption surface. A title
23 is sealed by the store and opened by the store; no handler holds a
24 plaintext title it did not get from `store.title`. `pending_joins` is
25 the fingerprinted table: an outsider's address is stored there only as
26 a keyed fingerprint, while `persons.email` holds a verified address in
27 plaintext, the suite's current shape for a person (spec §2.1, as in
28 Docs, Slides and Calendar).
29 Meeting invitation tokens are looked up by hash and sealed for repeat sharing
30 by members. Legacy hash-only invitations remain valid; first sharing creates
31 a sealed invitation alongside them. Replacing a link revokes all current ones.
32- `internal/calls/store.go` — every database access. `standing.go` is
33 the entry authority (spec §2.3), computed per request, never stored.
34- Sign-in: `auth.go` is Docs' shape with the pending-join exception of
35 spec §3.2 and Slides' same-answer wrapper. Keymail or an emailed link
36 is chosen by the address's domain alone, never by membership (Paul,
37 2026-10-06). A Keymail address, member or not, gets the continuation
38 page to Keymail (never a 303 out of the form POST: Safari applies
39 `form-action` to it); every other answer from `POST /signin` is
40 `303 /signin?sent=1`, and the link is mailed after the response by
41 `signin_mail.go`'s worker, only if this request may have one, so
42 neither the answer nor its timing says who is a member. A refusal
43 after verification is the door, rendered in the refusal's own 403 by
44 auth's `Refused` hook (never a query anyone could type). An emailed
45 link opens the confirm door (`RenderConfirm`): `GET /auth/verify`
46 spends nothing, so a mail scanner cannot burn it, and the door's one
47 Sign in button POSTs the token back to `/auth/verify`, mounted for
48 both methods in both modes. `mayRequestLink` does the same
49 database work for a member and an outsider and logs nothing on the
50 response path.
51
52## Rules the family holds hard
53
54- Every query touching user-owned rows goes through `scope.Owned` —
55 reads AND writes, transactions included. A row that isn't yours 404s,
56 never 403s.
57- Never bind a request body onto a GORM model: explicit
58 `map[string]any` + `.Select` allowlist.
59- Migrations apply at boot, once each, and are never re-run or reversed.
60- Schema changes: edit models.go, then run `rastrillo migration generate`
61 and read the SQL it writes — it may be a full table rebuild, not just
62 an `ADD COLUMN`. Never edit a migration that has shipped.
63 Renames are hand-written (`rastrillo migration new rename_x`) because
64 a rename is indistinguishable from a drop plus an add.
65- Money is integer cents. A float never touches a value a person will
66 be held to.
67- Screens work with JavaScript disabled, with two exceptions: browser
68 media participation (the holding screen's camera preview and join
69 button, and the call itself in the room), and, once a call exists,
70 the room's chat panel and its admission queue's Admit controls; both
71 pages still render their title, standing, floor notice and
72 navigation without script. Destructive actions get their own
73 confirm-page URL.
74- The gate is `make ci` (vet + gofmt + tests) — the same definition
75 CI runs. Run it before every push. `CGO_ENABLED=0` throughout: the
76 stack is cgo-free by design.
77- Native Apple bindings under `native/` use cgo to bridge Go to Objective-C.
78 Their toolchain is isolated in `native/go.mod`; server releases remain
79 `CGO_ENABLED=0`. Build with `make native-mac`.
80- `make race` is the test exception to that rule, and the third gate:
81 `-race` refuses to build without cgo, so it sets `CGO_ENABLED=1`. It
82 builds a test binary and never a release, so nothing that ships gains
83 a C dependency. Run it for any change to `internal/calls/hub` or the
84 stream — a hub behind a mutex, a tick goroutine and one goroutine per
85 stream is more concurrency than `make ci` can see.
86- `make browser` is the second gate, for any change to a page or to the
87 client: a real call in real Chromium. It needs the Claude Code bash
88 sandbox **off** — the sandbox blocks UDP, so Chromium gathers no ICE
89 candidates and every call fails with `frames` stuck at 0, which reads
90 exactly like a client bug and is not one.
91
92## Serving
93
94`rastrillo.Resolve` + `Serve` speak the CARLOS activation contract
95(socket/addr/db argv, LISTEN_FDS, $STATE_DIRECTORY, `sidecar run`) —
96the golden deployment target, not a requirement: `./app -addr :8080`
97serves anywhere. `GET /healthz` and `GET /api/version` are answered
98by the framework, outside app middleware.
99
100## Deploying
101
102`make release VERSION=<sha>` builds and stamps the artifact; `carlos
103deploy --app calls --account oficina --channel edge --version <sha>
104--target linux-arm64 releases/calls-linux-arm64` ships and promotes it,
105waiting until `/api/version` serves it — but only once `carlos apps
106build-probe --app calls --account oficina /api/version` has been run
107for the app (set on the flagship app since 2026-09-09); a new app or
108account needs that first, or the wait watches `X-Carlos-Version`
109instead, which can report the version adopted before the process has
110actually changed. A migration applied under an earlier filename fails
111at boot; otherwise migrations never re-run. Every deploy, and every
112relay config change, restarts the instance and drops live calls — no
113drain exists. The wait can time out before
114hand-over — check `/api/version` before retrying. See the README's
115Deploying section for the canary recipe.
116
117## UX conventions (seeded from profile: considered)
118
119Accessible by construction, progressively enhanced, and never dependent on JavaScript to be usable, with the exceptions named above: browser media participation, on the holding screen and in the room, and, once a call exists, the room's chat panel and admission controls.
120
121These are rastrillo's own conventions, and the profile is named for what it does rather than after anyone else's work. For wider reading on interface quality, impeccable.style, the WAI-ARIA Authoring Practices and Inclusive Components are all worth your time — none of them is endorsed by, or authoritative for, this framework.
122
123- [x] Icons — lucide, inline — `internal/<app>/icons`
124- [x] Fields — label, hint, help and error in one envelope, wired with aria-describedby — `ui/field, ui/field-select, ui/field-text`
125- [x] Destructive actions — their own confirm-page URL, never a modal — `ui/confirm-form`
126- [x] State — never colour alone — always a text label too — `ui/status-pill, ui/badge`
127- [ ] Dates — relative, with the absolute date in a title attribute
128- [ ] Errors — server-rendered beside the field, in plain language
129- [x] Empty states — say what the screen is for, not just that it is empty — `ui/empty-state`
130
131`[x]` is enforced by a vendored component: use it, do not hand-roll an
132equivalent. `[ ]` is guidance to apply by hand. A convention absent from
133this list is one rastrillo has no opinion about.
134
135Edit any line. **This file is the source of truth, not the profile name.**
136The profile only seeded this list at scaffold time; nothing re-reads it, so
137changing a line here changes what the app does. A convention deleted from
138this file is not a convention to reinstate.
139
140## Work in a worktree, and catch main up first
141
142Take a worktree for the work. Do not work in the shared checkout.
143
144 git -C <repo> fetch origin
145 git -C <repo> worktree add ../<repo>-<task> -b <branch> origin/main
146
147Both halves matter, for different reasons.
148
149**The worktree**, because several sessions share this machine and some
150share this repository, and a checkout has one HEAD, one index and one
151working tree between all of them. A branch switch under somebody else's
152uncommitted work destroys it with nothing on screen to say so, and two
153sessions editing the same paths disagree quietly instead of conflicting
154loudly. A worktree gives the work its own HEAD and its own files, and
155costs a directory.
156
157**From `origin/main`, after a fetch** — not from whatever the checkout
158happens to be sitting on. A branch cut from a stale main is a merge
159conflict scheduled for later, against files somebody else has renamed in
160the meantime. A branch cut from another task's branch is worse: it
161carries that task's commits into yours, where they are reviewed as
162though you wrote them, and the two can only be separated by hand.
163
164Remove it when the work has landed — `git worktree remove` — so that
165`git worktree list` keeps saying something true.
166
167Written down because of 2026-09-17, in this repository, while it was
168still called `meet`. The shared checkout was
169sitting on one task's branch while holding a *different* task's feature
170uncommitted on disk — some four hundred lines, a new migration among
171them — put there by a session nobody else knew was working in that tree.
172Two other sessions were meanwhile coordinating over the same checkout by
173asking each other not to touch it, which is the arrangement a worktree
174makes unnecessary. Nothing was lost. Nothing about the arrangement would
175have said so if it had been, and by then the same feature existed twice,
176once on each side of a rename, because the two sessions had branched
177from different starting points.
178