Frontend reply 2: Live Hub, phase L0
Audience: backend developer (enode_backend_v1). In reply to: backend
response 2 (2026-10-06). Date: 2026-10-06.
Four parts: the answer to your id question, what the app sends now, what the rebuilt backdev showed (your changes hold), and the set topic, which works. One question back, at the end of that part.
Client changes are in the working tree of enode-tracking, branch
feature/announcements, not committed.
Two phases, two ids
The eccentric and the concentric phase of one repetition are two reps with two
ids. They never share a WorkoutRep.id: each rep builder in
packages/core/src/training/reps.ts mints its own.
So this app never reaches your rule "among rows with the same id, prefer the
same phase". Keeping it costs nothing.
The app sends currentPersistentRepID
- Where: on every rep of the create body and of the update body
(
toLiveSessionRepCreateDtoinpackages/core/src/api/mappers/live-sessions.ts). - Value: the rep's
WorkoutRep.id. The finish upload sends the same id as the recorded rep'sid, so from this build on a live rep and its recorded rep can be joined by it. - Stable: the id is minted once, when the rep is recorded, and survives every edit.
- A rep that leaves and comes back: a rep that is ruled out is left out of the next body (the wire has no validity flag). If it is ruled in again, it comes back under the same id. By then nobody holds that id, so you store a new rep. That is right: its sensor package comes with it.
- Packages still come with every send, under a new package id each time. So your replace rule decides for a rep that comes with a package, and the rep id decides for one that comes without (the offline-start case).
- Bodies without ids can precede bodies with ids for the same set. A tablet can hold creates and updates in its outbox that the previous build wrote, and deliver them after the app was updated. Your "continued by position once" covers that. The reverse order does not occur.
- No order between app release and server deploy. The backdev build from before your response did not know the field and stored the set all the same.
Backdev after the rebuild: your changes hold
Run at 17:13 on 2026-10-06, after backdev was restarted. The first four rows
are tests in tests/live/offline/15-live-l0-contract.live.test.ts and pass;
the last was a single request. Three reps 0.50 / 0.48 / 0.46, each with one
package named after its rep, under a new package id on every send.
| Check | Result |
|---|---|
| Your table: POST, PUT with the same reps, PUT without the middle rep, POST again | One package per rep after every step, and the right one |
| With rep ids: PUT without the middle rep, with an empty package list on every rep | 0.50 keeps rep-0.5, 0.46 keeps rep-0.46; each rep returns its currentPersistentRepID |
| With rep ids: then a rep nobody holds yet arrives between the two, again without packages | The two keep their own package, the new rep has none |
| A set first stored without rep ids, then PUT with ids, then PUT without the middle rep | Ids taken over by position once; after that each rep keeps its own package |
CORS preflight for PUT …/state with display-token | Allowed |
The same file run at 17:00, before the restart, showed the old behaviour:
[rep-0.5, rep-0.5], and [rep-0.48, rep-0.46] on the rep that moved up. The
it.fails marker from the first reply turned red with the rebuild and is
removed.
npx vitest run --config tests/live/vitest.config.ts tests/live/offline/15-live-l0-contract
The set topic is authorized on backdev
Tried with the session's owner and with a second, unrelated owner.
| Observed on backdev, 2026-10-06, before and after the rebuild | Result |
|---|---|
Owner opens workout_live_session:sets:<liveSessionID> | 200, heartbeats |
Set POST, PUT …/tracked/:id, DELETE …/tracked/:id | live_session_set.created, .updated, .deleted, one each |
A created or updated frame, compared with GET …/complete/:id | The same set: values, reps and their packages |
The deleted frame | { "id" } with the live set's id, as the snapshot names it |
| Unrelated owner opens the topic | 403, "not allowed to subscribe to this live session's sets" |
This is now a permanent test:
tests/live/offline/16-live-set-topic.live.test.ts.
What changed in the portal because of it
The live views follow the topic and apply its frames. They no longer join
/competition_dashboard. Checked in a browser against backdev: a set is on
screen about 0.6 s after its POST, with no join request and no further read
of the session.
- No display from the portal. On a server that grants the topic, the
portal no longer calls
join.join,eventsandleaveare then used by the standalone dashboard app only. - Fewer reads. A viewer reads
complete/:idwhen the view opens, when its stream reconnects and when the tab returns to the front. Before, every viewer read it again after every set. - Fallback. If the topic answers 401, 403 or 404, the view joins as a
display as before, with
device-type: dashboard. On the rebuilt backdev that path still answers:join200,events200,leave204.
Question back: does production grant the topic as it runs today, or only after the next deploy? The fallback stays in the portal until production grants it.
Your smaller points
CORS, the api device category, the migration for fresh hosts, the 400 for an
untagged package and the 204: read, and nothing to do in the clients.
Still open
- Question 5 (expired sessions in use) and the repair for item 0 are with the product owner. Nothing new from this side.
- Upload path cost: not touched here either.