c0e39717 Andy McCall 21 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 19 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
67be8ac0 Andy McCall 19 days ago ↶
105
- ** L2 (done, #204):** ` GET /live?topics=a,b` (1 to 16 topics; every one
106
must be one the viewer may hear, read access to its repository or, for
107
` user:<id>` , being that user, or the request is refused whole, saying
108
nothing of which); SSE ` change` events ` {topic, version, kind}` with the
109
bus id as the event id; ` Last-Event-ID` replays the missed events on
110
those topics or sends ` reload` , as does falling behind the broadcast.
111
The client in ` hub.js` : ` data-live` + ` data-fragment` elements refetch
112
` ?fragment=<name>` from the page's own route and swap it in, waiting
113
while someone types inside one.
114
b5a0a69b Andy McCall 19 days ago ↶
115
- ** L3 (done, #205):** the proposal page updates itself. Its timeline
116
(comments, reviews, assistant answers, events, the landed or closed
117
outcome, and a "… is answering" placeholder while an assistant works)
118
listens on ` repo:<id>/proposal:<n>` , and its Landing section on that and
119
` repo:<id>` (check results). ` ?fragment=timeline|landing` renders the page
120
through the same route and returns the part between
121
` <!--frag:NAME-->` markers (` web::fragment` ), so the update is the page.
122
A comment, an assistant asked, answering or not answering, reviews, state
123
changes and landings publish on the proposal's topic.
124
8a48dd83 Andy McCall 18 days ago ↶
125
- ** L4 (done, #206):** the top bar's notification count listens on the
126
viewer's ` user:<id>` topic on every page (the Shell carries it), the
127
inbox's list on it too, and the dashboard's cards and activity feed on
128
it and the topics of up to 15 of the viewer's repositories (a stream
129
takes 16). Nothing new on the server: the client refetches the page and
130
swaps each marked part, so every route already serves them.
131
a10446c6 Andy McCall 18 days ago ↶
132
- ** L5 (done, #207):** the issue page's timeline listens on
133
` repo:<id>/issue:<n>` and ` repo:<id>` ; the repository overview's latest
134
change and bookmarks on ` repo:<id>` , and its Diffusion card and the
135
Diffusion tab on ` diffusion:<repo id>` (the tab on ` repo:<id>` too).
136
The repository chrome (` RepoShell` ) carries the id for these topics.
137
33d98147 Andy McCall 16 days ago
138
- ** Across processes (oxygenhub#273):** the live bus rides the hub's event
139
bus (docs/roles.md, "Event bus"). On PostgreSQL every web process hears
140
every other's events over ` LISTEN/NOTIFY` and numbers them in its own
141
replay buffer, so a page's ` Last-Event-ID` belongs to the node that gave
142
it; reconnecting elsewhere with an id that node never gave sends
143
` reload` . The resolver's choices and landings travel the same way;
144
presence stays per node.
145
c0e39717 Andy McCall 21 days ago ↶
146
## 6. Order
147
148
149
150
| 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 |
151
| L2 | ` GET /live?topics=` with subscription checks, ` Last-Event-ID` replay, and the ` hub.js` client with ` data-live` / ` data-fragment` | #204 |
152
| L3 | The proposal page live: comments, reviews, assistant answers and pending placeholders, checks, landing state | #205 |
153
| L4 | Dashboard and inbox live, with the notification count in the top bar | #206 |
154
| L5 | Issue page, Diffusion tab and repository overview live | #207 |
155
| L6 | Fold the Flow run stream and the resolver's rooms into the same bus | #208 |
156
| L7 | Services: JetStream behind the bus and the live gateway as its own service | #209 |