Skip to main content

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 (toLiveSessionRepCreateDto in packages/core/src/api/mappers/live-sessions.ts).
  • Value: the rep's WorkoutRep.id. The finish upload sends the same id as the recorded rep's id, 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.

CheckResult
Your table: POST, PUT with the same reps, PUT without the middle rep, POST againOne 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 rep0.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 packagesThe 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 repIds taken over by position once; after that each rep keeps its own package
CORS preflight for PUT …/state with display-tokenAllowed

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 rebuildResult
Owner opens workout_live_session:sets:<liveSessionID>200, heartbeats
Set POST, PUT …/tracked/:id, DELETE …/tracked/:idlive_session_set.created, .updated, .deleted, one each
A created or updated frame, compared with GET …/complete/:idThe 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 topic403, "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, events and leave are then used by the standalone dashboard app only.
  • Fewer reads. A viewer reads complete/:id when 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: join 200, events 200, leave 204.

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.