openvibers/openvibe.today AGENTS.md
AGENTS.md
| 1 | # AGENTS.md — OpenVibe.Today |
| 2 | |
| 3 | ## Project overview |
| 4 | |
| 5 | OpenVibe.Today (`https://openvibe.today`) is the network's time bus and calendar authority: a person |
| 6 | puts a stream or an event on the schedule, the network subscribes through RFC 5545 calendars, reminders |
| 7 | go out through Network notifications, and every change publishes a `today.event.*` event. It also |
| 8 | serves the daily brief and imports public calendars/holidays. |
| 9 | |
| 10 | - **Entry:** `server/index.js`; app `server/app.js`; port `5100`, service id `today`. |
| 11 | - **API:** `server/http/api.js` (`/api/v1`), `principal.js` (person resolution), `discovery.js` |
| 12 | (robots/sitemap/llms); pages in `server/http/pages.js`, `server/views/index.js`, |
| 13 | `server/render/layout.js`. |
| 14 | - **Events:** `server/events/` — `publisher.js`/`store.js` (transactional outbox), `consumer.js` |
| 15 | (Live stream started/ended, account export/deletion), `validate.js`. |
| 16 | - **Calendar:** `server/ics.js` (RFC 5545 output), `server/reminders.js`, `server/agenda.js`, |
| 17 | `server/brief.js`; HTTP fetch guard `server/net/`. |
| 18 | - **Imports:** `server/imports/` (`index.js`, `normalize.js`, `holidays.js`), `scripts/import-now.js`. |
| 19 | - **Browser:** `public/js/today.js` is the only own script; all pages are server-rendered through |
| 20 | `openvibe-shared/shell`, readable without JavaScript, with stable `td-` ids/classes. No bundler. |
| 21 | |
| 22 | ## Owns, does not own |
| 23 | |
| 24 | Owns `today_events` (`tev_…`: owner subject, kind `stream|event`, Live channel, title, description, |
| 25 | start/end, IANA tz, status, visibility, url, ICS `seq`), `today_idempotency` (subject + key → the first |
| 26 | event), `today_reminders`, `today_feeds` (one private 32-byte token per person) and `today_outbox` |
| 27 | (fixed `evt_` id, lease, backoff). |
| 28 | |
| 29 | Does not own: identity, follows and notifications (Network — Today verifies tokens/JWKS, reads follows |
| 30 | with the person's own token, pushes reminders through `/internal/notifications/push` with its own |
| 31 | service token); the live stream and its lifecycle (Live — Today consumes `live.stream.started|ended` |
| 32 | and never starts/stops a stream); the Rundown (Live's public API, linked not copied); event delivery |
| 33 | (Events — Today publishes its outbox and subscribes to live/account topics). |
| 34 | |
| 35 | ## Who calls it, what it calls |
| 36 | |
| 37 | People use the pages/API with the `today_at` session cookie or a `Bearer` Network user token. SDK |
| 38 | `openvibe-sdk/today` is the client surface (events, own agenda, feed token/rotate, brief, reminders). |
| 39 | Other sites link into Today, for example `GET /remind?title=&at=&url=&from=` for a reminder. Today |
| 40 | calls Network (JWKS, follows, identity resolve-batch, notification push), Events (outbox and |
| 41 | subscriptions), Live (the public Rundown for the brief) and, for imports, the configured public |
| 42 | calendar/holiday hosts through the guarded fetcher. |
| 43 | |
| 44 | ## Database and migrations |
| 45 | |
| 46 | PostgreSQL through `openvibe-sdk/db` (PgBouncer for the app, owner role for migrations); tests use |
| 47 | PGlite. `migrations/0001_initial.sql` creates the event, idempotency, reminder, feed and outbox |
| 48 | tables; later migrations add the rest. Applied at boot; never edit a migration that has run. A change |
| 49 | writes its `today_outbox` row **in the same transaction** as the event. The inbound event id is |
| 50 | recorded in `today_processed` in the same transaction as its effect, so a replay does nothing. |
| 51 | |
| 52 | ## Auth |
| 53 | |
| 54 | Network RS256 JWTs verified offline with JWKS; the `today_at` cookie is the page session, a Bearer |
| 55 | token the API. A write made with the session cookie must come from `https://openvibe.today`, else |
| 56 | `403 today.cross_site`. A person acts only on their own events, reminders and feed. `/internal/events` |
| 57 | is loopback-only, verified with the v2 signature over the raw body and the ±300 s window; `/internal` |
| 58 | and `/metrics` answer 404 from nginx. |
| 59 | |
| 60 | ## Deploy |
| 61 | |
| 62 | `deploy/systemd/openvibe-today.service` (`127.0.0.1:5100`, env `/etc/openvibe/today.env`) and |
| 63 | `deploy/nginx/openvibe.today.conf` (TLS, www → apex, sign-in/API/ICS limits, `/auth/me` on the API |
| 64 | zone, `/ics/` GET only, `/internal` and `/metrics` 404). `sudo ovhost deploy today` is the production |
| 65 | path (git-checkout, install on lockfile change, restart, wait for `/api/ready`); rollback with |
| 66 | `sudo ovhost rollback today --to <sha>`. |
| 67 | |
| 68 | ## Commands and tests |
| 69 | |
| 70 | ```bash |
| 71 | npm ci |
| 72 | npm run dev # http://127.0.0.1:5100 |
| 73 | npm test # every test/*.test.js, PGlite + mock Network |
| 74 | npm test -- events ics # only matching files |
| 75 | npm run test:pg # the same on PostgreSQL |
| 76 | npm run imports:run # run the calendar imports once |
| 77 | ``` |
| 78 | |
| 79 | `test/run.js` uses `openvibe-shared/test-runner` (60 s, serial); temp PGlite and an in-process Network |
| 80 | mock, never the network. Tests cover validation, ICS folding/escaping and cancellation, the outbox and |
| 81 | replay, reminders claimed once, the brief, the pages, imports and account export/deletion. |
| 82 | |
| 83 | ## Conventions and pitfalls |
| 84 | |
| 85 | - CommonJS, 4-space indent, single quotes, semicolons; no linter. Errors are |
| 86 | `{ code, field, message }`/problem+json; never a raw row. |
| 87 | - **The ICS output is a compatibility surface:** CRLF, 75-octet folding that never splits a UTF-8 |
| 88 | sequence, TEXT escaping, UTC DTSTART/DTEND (one hour when no end), `SEQUENCE` from `seq`, and a |
| 89 | cancelled event emitted with `STATUS:CANCELLED` for 7 days. A calendar client is not a test you can |
| 90 | apologize to. |
| 91 | - **Reminders are claimed atomically** (`UPDATE … WHERE sent_at IS NULL RETURNING`); a failed push |
| 92 | releases the claim for the next tick, and a cancelled event never sends. |
| 93 | - **The event id is fixed at write time** and the outbox is retried with backoff; Events is |
| 94 | idempotent on that id, so a retry/restart must not make a second event. |
| 95 | - **The private feed is a bearer token:** rotate replaces it and the old URL is 404 at once; export |
| 96 | includes the feed row without the token. |
| 97 | - **Followers-only events are absent from public lists**, not filtered in the page; `follows_unavailable` |
| 98 | is the honest answer when Network is down. |
| 99 | - **Imports never invent a date:** a parsed calendar entry keeps its source and time; a holiday is |
| 100 | labelled as a holiday. |
| 101 |