c0e39717 Andy McCall 22 days ago ↶
1
# Live pages
2
3
Status: design, 2026-10-14 (oxygenhub#202). Nothing here is built yet
4
beyond what §2 lists.
5
6
## 1. The problem
7
8
Pages are rendered once. When something happens behind them (an
9
assistant answers, a run finishes, a review arrives, a bump is opened)
10
the page does not know until it is reloaded. Proposal 38 on
11
doomsdayone/bomberman94 showed it: the assistant's answer, or the reason
12
there was none, only appeared on reload.
13
14
## 2. What exists
15
16
- ** Flow runs** stream their logs and job states over Server-Sent Events
17
(` /{owner}/{name}/flow/{id}/live` , read by ` EventSource` in
18
` assets/hub.js` ).
19
- ** The conflict resolver** keeps in-memory rooms (` crates/hub/src/live.rs` ):
20
a ` tokio::sync::broadcast` channel per conflict, for presence and
21
choices.
22
- ** Notifications and webhooks** already describe most writes as events
23
(` notify::deliver` , ` hooks::emit` ).
24
25
## 3. Not a single-page app
26
27
Moving to Next.js or another single-page app was considered and set
28
aside:
29
30
- every page would be rendered twice (Rust templates and JavaScript), and
31
the two drift;
32
- public pages would still need server rendering;
33
- pages would stop working without JavaScript, and accessibility gets
34
harder;
35
- permissions would be checked in two places;
36
- it rewrites about a hundred templates to gain one thing: pages that
37
update themselves.
38
39
## 4. The design: notify, then fetch
40
41
1. ** Events name a topic, not data.** Every write publishes a small
42
event: a topic, a version, and a kind. Topics:
43
- ` repo:<id>` (bookmarks moved, settings);
44
- ` repo:<id>/proposal:<n>` and ` repo:<id>/issue:<n>` (comments,
45
reviews, assistant answers, labels, state, checks);
46
- ` run:<id>` (jobs and logs, as today);
47
- ` user:<id>` (inbox, dashboard);
48
- ` diffusion:<repo id>` (bumps, pills).
49
2. ** One stream per page.** A page asks for its topics on one
50
connection: ` GET /live?topics=...` , ` text/event-stream` . The browser
51
reconnects by itself; ` Last-Event-ID` resumes, and the server replays
52
what was missed from a short buffer, or says "reload" when it cannot.
53
3. ** The page fetches what changed.** An event says which part of the
54
page is stale. The page fetches that part as HTML from the normal
55
route (` ?fragment=comments` , ` ?fragment=checks` and so on) and
56
swaps it in. So:
57
- the templates that render the page render the update;
58
- permission is checked once, by the route that renders the
59
fragment; an event reaching the wrong viewer reveals nothing but a
60
topic and a version;
61
- fragments can be cached and are tested like pages.
62
4. ** Subscribing is checked too.** ` /live` only accepts topics the viewer
63
may read (a private repository's topics need read access), so even
64
the fact that something changed does not leak.
65
5. ** The page still works without it.** No JavaScript or no connection:
66
the page is what it is today, and a reload shows everything. Pending
67
work shows a placeholder ("Claude is reviewing…") that the stream
68
replaces.
69
6. ** A small client, ours.** About 2 KB in ` hub.js` : elements carry
70
` data-live="<topic>"` and ` data-fragment="<name>"` ; an event for the
71
topic refetches the fragment and swaps it, keeping focus, scroll and
72
anything being typed. No framework.
73
74
## 5. At scale, across services
75
76
When the hub is split into services:
77
78
- ** Services publish events to a durable log.** NATS JetStream is the
79
choice: small, self-hosted, clustered, with replay, so a sovereign site
80
runs it on its own machines. Postgres ` LISTEN/NOTIFY` is an acceptable
81
first step for a single database.
82
- ** A stateless live gateway** holds the browsers' connections. It
83
checks a viewer's subscription (asking the owning service, cached
84
briefly), relays topic events, and replays from the log for
85
` Last-Event-ID` . It never renders and never reads repositories, so it
86
scales out on its own; losing an instance means browsers reconnect.
87
- ** Fragments stay with the service that owns the data** (issues,
88
proposals, Flow, Diffusion). Moving a page to a new service does not
89
touch the live layer.
90
- ** The resolver's rooms** become topics on the log, so people on
91
different instances see each other.
92
- ** Later:** WebTransport where streams need to be bidirectional and
93
many; SSE stays the default.
94
7e01a766 Andy McCall 20 days ago ↶
95
## 5a. Status
96
97
- ** L1 (done, #203):** ` events::LiveBus` in the hub: one broadcast of
98
` {id, topic, version, kind}` that subscribers filter by topic (a channel
99
per topic buys nothing at this size), a version per topic, and the last
100
1024 events for ` Last-Event-ID` (older: "reload"). Published from
101
` hooks::emit` (the repository's topic, and the proposal's or issue's the
102
event names), ` notify::tell` (` user:<id>` ), Flow job changes
103
(` run:<id>` ) and Diffusion bumps (` diffusion:<repo id>` , both sides).
104
c0e39717 Andy McCall 22 days ago ↶
105
## 6. Order
106
107
108
109
| L1 | Event bus inside the hub (broadcast per topic, versions, a short replay buffer); publish from the writes that notify and webhooks already see | #203 |
110
| L2 | ` GET /live?topics=` with subscription checks, ` Last-Event-ID` replay, and the ` hub.js` client with ` data-live` / ` data-fragment` | #204 |
111
| L3 | The proposal page live: comments, reviews, assistant answers and pending placeholders, checks, landing state | #205 |
112
| L4 | Dashboard and inbox live, with the notification count in the top bar | #206 |
113
| L5 | Issue page, Diffusion tab and repository overview live | #207 |
114
| L6 | Fold the Flow run stream and the resolver's rooms into the same bus | #208 |
115
| L7 | Services: JetStream behind the bus and the live gateway as its own service | #209 |