Skip to main content

Backend response 6: Live Hub, the open points of 13 October are built

Audience: frontend (enode-tracking, portal). In reply to: "Live Hub — what is still open on the server" of 2026-10-13, and the sections "What a display cannot read yet", "A set does not say which device sent it", "Asked 2026-10-12" and "Asked 2026-10-13" of live-hub-backend-handoff-open-2.md. Date: 2026-10-13.

Status: points 1 to 8 and the two frames you had not checked are built on bugfix/support_reports_handoff, on top of develop df12f00e. Not committed and not pushed yet; backdev has it once it is rebuilt from that working tree. Backend suite: 570 tests green, six of them new. One migration. Nothing here has been run against a client.

Built​

Your pointWhatDiffers from what you asked
1 · "Don't share"POST /workout_live_sessions/decline_sharing, sharingDeclined on the owner's devices read and on the left frame204 in every case. The read names the athlete
2 · Why a session endedendedBy and lastSetAt on the session and on live_session.updatedRead from the times, not stored. See below for an "End session" after the session had run out
3 · An ended session takes a displayThe guard is goneA display paired after the end reads for a day from its pairing
4 · A photoGET /live_displays/users/:id/imageAlso for the owner and for whoever a device names in athleteIDs
5 · CataloguesGET /live_displays/metric_categories; exerciseBase written out on exercise_definitions/:idThe base and its group come as objects, not as one id field
6 · Displays among the devicesPaired displays are left out of participatingDevicesNo
7 · The device of a setdeviceSessionID on a stored setTaken from the device-id header, as the set routes find the session
8 · A stream ends on unlinkThe set stream of a device that left is endedFound by its user, not by its device. See below
The session frame for a displaylive_session.updated on /live_displays/events in the shape of GET /live_displays/sessionNo
The device frame for a displaylive_session.device on /live_displays/events in the shape of GET /live_displays/devicesNo

Not built: the tour row of point 9 (it is data, see the end), and the free field on a display that presence would write.

1 · A declined "Share your training?"​

POST /workout_live_sessions/decline_sharing, token-protected, no body, for the device of the token.

CaseAnswer
The device is in a live session204. The answer is stored, then everything leave does: unlinked, device_session.live_session_changed to the device, live_session.device with left: true to the viewers. Storing and unlinking are one transaction
The device is in no live session204, nothing stored
No device session for the caller404, as leave

It is 204, not 200, in both cases, because that is what leave answers.

Stored: at most one row per live session and user: the session, the user, declinedAt on the server's clock, and the device's name at that moment. A second decline moves declinedAt and the name.

EventThe row
The same user calls acknowledge_sharing while linked to that sessionRemoved
The same user joins that session by codeRemoved
The session endsKept
The owner adds the device againKept
The session or the user is deletedRemoved
"Run again"Not copied; it is a new session

Read: GET /workout_live_sessions/:id/devices always has

"sharingDeclined": [
{ "userID": "…", "declinedAt": 1791900000000, "deviceName": "Mia's iPhone" }
]

newest first, an empty list when nobody declined, and also for an ended session. Same readers as before. It is not on GET /live_displays/devices and not on the session.

Push: the live_session.device frame of that leave carries "sharingDeclined": { userID, declinedAt, deviceName } beside deviceSessionID and left: true, on the owner's topic. A display's stream gets the same frame without it.

Your questions. (a) One row per session and user, as you need it. (b) The read names the athlete. That was decided on the backend side and not confirmed by the product owner; if it has to be the count only, say so, it is a small change.

2 · Why a session ended​

WorkoutLiveSessionReturnDto and the live_session.updated frame have:

  • endedBy: owner, expiration or idle. Absent while the session runs.
  • lastSetAt: when the last set was stored. Absent before the first set.

GET /live_displays/session and a display's live_session.updated have both too.

endedBy is not stored. Only "End session" writes an end time, so the rest follows from the times: a session without a stored end ended by whichever of the end date and the idle limit ran out first.

One case to know: if the owner presses "End session" on a session that had already run out, endedBy stays expiration or idle, while endedAt is the moment of the press. And because nothing is stored, an owner who changes the end date or the idle limit of an ended session can change what endedBy says.

3 · An ended session takes a new display​

POST /workout_live_sessions/:id/displays answers 200 for a session that is over, for its owner, with no time limit.

What changes for a token: until now a display's token died 24 hours after the session's end. A display paired after the end would then be dead at once for an older session, so its token is good for 24 hours from its pairing. A display paired before the end is unchanged.

4 · A photo for a display​

GET /live_displays/users/:id/image with display-token → { id, imageData }, the shape of /users/profileImage/:id, with the same ETag and If-None-Match (304).

WhoAnswer
Has a set in the display's session200
Stands in a platforms row of a linked device, as the lifter or as next200
Is named in athleteIDs of a linked device200
The session's owner200
Anyone else404
One of the above without a photo404
No token or a wrong one401

The last two rows of "who" are more than you asked for: a display shows those people too (athletes on a device row, user on the session).

platforms is kept in memory, so after a restart of the server a person who only stands on a station is a 404 until that device's next heartbeat.

5 · Catalogues​

  • GET /live_displays/metric_categories: the answer of /metric_categories, with its If-Modified-Since handling.

  • GET /live_displays/exercise_definitions/:id now writes exerciseBase out, and in it displayBaseExerciseGroup:

    "exerciseBase": {
    "id": "…", "identifier": "…", "movementType": "…",
    "displayBaseExerciseGroup": { "id": "…", "name": "…", "order": 0, "media": "…" }
    }

    Before, exerciseBase held its id only; exerciseBase.id is where it was. The group's id is exerciseBase.displayBaseExerciseGroup.id. Only the display's route does this; /exercise_definitions/:id is unchanged.

There is still no read of the normative base categories and no list of all exercise bases for a display. Say if the group on the definition is not enough.

6 · Displays among the session's devices​

participatingDevices of the owner's session no longer carries a paired display (a device session whose deviceId starts with display:). Displays of the old /competition_dashboard/join are still in it, as before.

7 · The device of a stored set​

deviceSessionID on reduced, complete, board, the single set, the display's board and sets/:id, and the created / updated frames.

  • Set when the set is first stored, by POST or by a PUT …/tracked/:id that creates it. No later update changes it, whoever sends it.
  • Absent for a set stored before the field.
  • It is the device session behind the request's device-id header, the same one the set routes find the live session by.
  • It is kept when that device signs out, so it can name a device that is no longer in any list.

8 · An open stream ends when its device is unlinked​

When a device leaves a session, by leave, by decline_sharing, by the owner removing it, by moving to another session, or because the session ended or was deleted, the open streams on that session's set topic are ended for the user signed in on that device.

The userThe stream
Owns the session or has access to its ownerStays open: they may follow it anyway
Still has another device linked to the running sessionStays open
Anyone else, so an athlete on their own phoneEnded

Two limits. The stream is found by its user, so an athlete with the same login on two phones that were both in the session loses both streams only when the last one leaves. And it works within one server process, which is what production and backdev run.

A new stream of that phone answers 403, as before.

The two frames on a display's stream​

/live_displays/events still carries everything it carried, under the same event names. Two events come in another shape:

EventShape on a display's stream
live_session.updatedGET /live_displays/session: no participatingDevices, the owner as id, name, hasImage
live_session.deviceA row of GET /live_displays/devices: athletes as people, no athleteIDs, joinedAt, joinedBy, appVersion, sharingAcknowledgedAt. For a device that left: { deviceSessionID, left: true }

Each arrives once. The owner's stream on /realtime/events is unchanged.

What an app from today notices​

Nothing is refused that was accepted. New in answers it reads: deviceSessionID on a set, endedBy and lastSetAt on a session, sharingDeclined on the devices read. Gone: paired displays in participatingDevices. An athlete's phone whose set stream is ended by the server sees its stream close after it left; its reconnect answers 403.

Database​

One migration, AddWorkoutLiveSessionSharingDeclines: the table extensions.workout_live_session_sharing_declines and a column device_session_id on extensions.workout_live_session_sets. It only adds.

Not built​

PointWhy
9 · The tour row portal-live on backdevIt is a row, not code: no migration fills product_guidance_items. It has to be created with POST /api/product_guidance_items (admin) on backdev, and on production before the release
A free field on a display that presence writesNot in your list of today; it needs a column
S1, step 2Waits for a week of step 1 on production
Removing join, state, events, leave of /competition_dashboardWaits for the portal's display page on production

Not tried​

The 24 hours of a display paired after the end running out, the photo of a person who only stands on a station, a stream ending when a session is deleted, a phone with two devices under one login, and anything with a real device or a real client. Your live tests 114 and 115 are the first run against it.