Frontend reply: Live Hub, phase L0
Audience: backend developer (enode_backend_v1). In reply to: the
backend response to live-hub-backend-handoff.md
(2026-10-06). Date: 2026-10-06.
Thank you — and item 0 was worth asking for. This document has three parts: what was observed on backdev, one defect that needs a fix before this ships, and the answers to your six questions.
Backdev runs the new code, and the contract holds
Your response says the changes are not deployed. They are on backdev now: the probe that showed a duplicated set this morning shows one set this afternoon. A contract test ran against backdev with a disposable account and passes:
npx vitest run --config tests/live/vitest.config.ts tests/live/offline/15-live-l0-contract
| Observed on backdev, 2026-10-06 | Result |
|---|---|
| The same set POSTed twice | One set, with its values |
| PUT for a set whose POST never arrived | The set exists, answer 200 |
| Edit that removes the middle rep of three | Two reps, each with its own values |
PUT session with a new name | Changed |
Set DTO: exerciseDefinitionID, currentPersistentSetID, order, createdAt, updatedAt | Present |
Device-session DTO: workoutLiveSessionName | Present |
POST …/end | 200, endedAt set, no tracking devices left |
| Device session after the end | workoutLiveSessionID is null |
| Set POST after the end | 204, nothing stored |
| Owner reads the ended session | Still readable, unchanged |
PUT { config } alone | Config stored; view, devices untouched |
Not observed from here: the dashboard endpoints, the realtime frames, reads by an unrelated user, expiry by date, and item 0 (finish after a live edit).
One defect: sensor packages are appended, not replaced
Observed on backdev. It is the second test in the same file, written with
it.fails: it passes while the defect exists and turns red when it is fixed.
What the app sends. With every create and every update, each rep comes
with its sensor package again, under a new package id each time
(toDataPackageCreateDtos in packages/core/src/api/mappers/sessions.ts
mints the id on every call). The app has never sent an edit without them.
What the server does with it (three reps 0.50 / 0.48 / 0.46, each with
one package named after its rep):
| Step | Rep packages afterwards |
|---|---|
| POST | [0.50] [0.48] [0.46] |
| PUT, same three reps | [0.50, 0.50] [0.48, 0.48] [0.46, 0.46] |
| PUT, middle rep removed | [0.50, 0.50, 0.50] and, on the rep with value 0.46: [0.48, 0.48, 0.46] |
| POST again (repeat) | one more copy on each |
Two consequences:
- Growth. A rep gains one copy of its package per edit and per repeated create. These are the largest rows of the feature.
- The wrong bar path. After a rep in the middle is removed, the rep that
moves up keeps the removed rep's package in first place. The portal charts a
rep's first package (
live-session/technique.ts), so it would draw the bar path of a rep that no longer exists next to the values of another.
Ask: when the body carries packages for a rep, they replace that rep's packages. Keep the existing ones only when the body carries none for that rep. That keeps your reason for position matching (an edit that omits packages does not lose the bar path) and fixes both points. The same on a POST that replaces a set.
Answers to your six questions
1. Does today's portal send a Bearer token on PUT /competition_dashboard/:id/state?
It does not call that route at all. In this repository only
packages/core/src/live-session/dashboard-bridge.ts talks to
/competition_dashboard, and it uses join, events and leave — each with
the token join returned. Nothing breaks for the portal on deploy.
The standalone competition-dashboard app is not in this repository and was not
checked. If it is a browser app: the new display-token request header needs
to be in the CORS allow-list, or its PUT …/state fails the preflight.
2. Does every tracked PUT send the complete rep list in the original order?
Complete: yes, with one definition of "complete". Every PUT carries all valid reps of the set, each with its measurements and its sensor package. A rep the athlete or the validator ruled out is left out, because the wire has no validity flag. So a rep can disappear from the middle, as in the test above.
Order: the reps are in the app's own array order. Please do not derive the
order by sorting on created:
- the eccentric and the concentric phase of one repetition are two reps on the wire and can carry the same timestamp;
createdcan be absent.
Match the body's array order against the rows in the order they were stored.
A better key, if you want one. The app has a stable id per rep
(WorkoutRep.id). The rep DTO has a currentPersistentSetID that the server
never maps; a currentPersistentRepID beside it would let the server match by
id and fall back to position for app builds that do not send it. Say the word
and the app sends it.
3. PUT that creates a set: are the defaults right?
Yes. The app's update body always carries work, order and side, so those
defaults never apply to it. created as the time of the request is fine.
4. participatingDeviceIDs on an ended session is ignored silently. Right?
Right, keep it. The portal's editor sends the device list with every save, so an error would block renaming an ended session.
The portal now reads endedAt: on an ended session the editor says that
devices can no longer be added, the card and the header read "Ended …", and
an empty ended session says so instead of asking for devices.
5. expirationDate: what does the portal send, and are expired sessions reused?
- What it sends: the optional "Ends" field of the editor. Empty means no date (null). A date in the past is refused when it is entered.
- Whether coaches reuse sessions past that date: the code cannot say, and nothing stopped them. Until today the portal showed "Ends" with a date on a date long past and the session kept working.
So this needs a number before the deploy to production. Please count the
sessions whose expiration_date is in the past and that received a set after
it, for example in the last 30 days.
Proposal for the product owner to decide: in the migration, clear
expiration_date on existing sessions that received a set after their date
in the last 30 days, and let the rest end. Then nothing a coach still uses
stops on deploy, the long-dead sessions release their devices, and the rule
applies to every date set from now on.
6. 404 for refused reads rather than 403. Does it matter?
No. Reads do not go through the outbox. A refused read becomes "could not be loaded" in the portal and in the app's standings. For writes, the outbox drops an operation on any 4xx except 401, 408 and 429, which it retries.
What changed in the clients because of your response
All in the working tree of enode-tracking, branch feature/announcements,
not committed.
exerciseDefinitionIDis read from the top level of the set DTO and of a realtime frame, withexercise.exerciseDefinitionIDas the fallback for a server without it.- A 204 is no longer counted as a delivered set. The app showed "n sets
delivered" for sets the server had answered with 204. It now drops such an
operation, does not count it, and re-reads its device session — which, with
your changes, reports
workoutLiveSessionID: nullfor an ended session. - The session name comes from
workoutLiveSessionNameon the device session; the owner-only read is the fallback. - End session is in the portal: the live view's menu, with a confirmation.
- Untagged sensor packages are left out of a live push. Found while
probing: a package with
dataPackageTypeID: nullmakes the whole set answer 400. The app produces such packages while its type catalogue is not loaded (an offline start), and the outbox would then drop the set. The live mapper now sends the set without that package. No server change asked for.
Still open on your side
- The package defect above.
- The count for question 5.
- Whether
workout_live_session:sets:<id>is authorized on backdev (your question 1 back to us): the portal still joins as a display instead. To be tried from the portal next. - The dashboard device category on a fresh host, as you noted.
- An estimate of how many finished workouts lost values through item 0. The product owner will want that number.