Skip to content

fix(hub): "riding" means a trainer is connected, not that anyone is pedalling #1016

Description

@janlauber

The friends list says a friend is riding when they are sitting still. Pair a trainer, walk away, and you stay marked as riding for as long as the tab is open.

Why

server/internal/hub/hub.go:1439, in the metric ingest:

rm.metrics[rider.ID] = m
rm.lastMetric[rider.ID] = now

Stamped on every metric message from the claim-holding screen, whatever the watts. A paired trainer publishes at roughly 1 Hz whether the cranks are turning or not — 0 W is a sample like any other.

hub.go:332, ridingLocked():

for id, at := range rm.lastMetric {
    if now.Sub(at) <= ridingWindow {   // 10s

So RidingIDs means "their trainer is connected and alive". The protocol comment says exactly that, honestly:

// Names with live metrics in the last few seconds — the watt dot.
Riding []string `json:"riding,omitempty"`

That is a real and useful signal. It is not riding.

The same word, two definitions

The client already has the stricter rule and uses it inside a room — web/src/lib/status.ts:

export function statusOfRider(rider: { away?: boolean; watts?: number }) {
    if (rider.away) return 'away';
    return (rider.watts ?? 0) > 0 ? 'riding' : 'online';
}

while every surface outside the room goes through statusOf(), which trusts the server's ridingIds. So one rider reads as riding on the friends page and online in the room they are standing in, from two definitions of one word.

The fix

Stamp a second timestamp, only when the rider is actually producing power, and derive riding from that:

now    lastMetric[id] = now        every sample, 0 W included
       riding = a sample within 10 s

after  lastWatts[id] = now         only when watts > 0
       riding = pedalled within 10 s

The hold matters. Do not test instantaneous watts — a rider coasting into a corner, freewheeling between intervals, or stopping to drink would flicker between riding and online several times a minute. Keeping the existing ridingWindow (10 s) as the hold means coasting holds the mark and sitting down loses it. That window is already a tuned constant; reuse it rather than inventing a second one.

lastMetric stays exactly as it is — it is load-bearing for trainer liveness and for the "watt dot" the in-room surfaces draw. This adds a signal; it does not repurpose one.

Check before assuming the client is right

statusOfRider's watts > 0 is instantaneous and has the same flicker exposure on the in-room surfaces. If it flickers there too, fix both to the same rule in this PR — one definition of riding, in one place, is the whole point of the change. status.ts's own docstring says four surfaces each invented their own dot and #807 unified them; this is the same lesson one level down.

Acceptance criteria

  • A rider with a paired trainer who is not pedalling reads as online, not riding, on the friends page, the room rail and every avatar badge.
  • A rider pedalling reads as riding, and keeps reading as riding through a few seconds of coasting.
  • The in-room "watt dot" / trainer-liveness signal is unchanged in behaviour — lastMetric is not repurposed.
  • riding means the same thing on every surface, in and out of a room.
  • Go table tests: samples at 0 W do not mark riding; watts then a 5 s coast still reads riding; watts then 15 s idle does not.
  • make protocol if any struct changes, both sides committed together.
  • go-review skill run — this is hub concurrency code under rm.mu, which is where the bugs live (chore(hub): hub.go is 1441 lines, 3.6× the ceiling, and it is where the concurrency bugs live #688).
  • make ci green.

Verify it for real

The verify skill with two riders (?as=) and, ideally, a real trainer per docs/HARDWARE-SESSIONS.md — a simulated trainer producing constant watts cannot demonstrate the bug, because the bug is what happens when watts are zero and samples keep arriving. Pair, sit still, and watch the other rider's friends list.

Deliberately not in scope

Related

#1017 (the friends surfaces this feeds), #807 (one dot, one vocabulary — the same lesson), #706 (away is said, never inferred), #610 (the trainer claim that gates this ingest), #688 (hub size and its concurrency bugs).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingroomsroom hub & realtime

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions