Skip to content

The Project board reads once when it opens, so a lease that expires while it is open never shows stalled and its counts freeze #1814

Closed

Description

@HugoXK

Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment on this issue.

Written-by: FlyBot Lead
Claimed-by: FlyBot Lead
Claim-Id: 4dc26246270b48af97a5d93210e06069
Branch: dev/1-project-board-poll
Base: 3f8cbb2
At: 2026-10-30T07:48:14Z

Symptom

The Project board is a picture of the moment it opened. Open it while work is queued and leave it open: a card whose holder stopped working keeps reading Dev2 · running (or Dev4 · running in English), the orange stalled edge that the board has for exactly this case never appears, a card that just got claimed stays under Waiting, and waitedMs stays at whatever it was (3h stays 3h) until the page is closed and reopened.

The room-side panels already move on their own; this page does not.

Root cause

  • core/src/client/project-board.js:351 says it in its own comment: "Four reads once, none polling: the Project, its record, its rows, and the task texts the …". The effect at :340 runs those reads and closes with }, [hash]) (:365) — its only dependency is the location hash, so it never runs again while the page stays open. setInterval appears nowhere in the file.
  • Everything the board draws is computed at read time on the host: core/src/host/project-board.js:294 buildQueues derives waitedMs (:40) and whether a lease is live, so a client that reads once can never see a lease that expired afterwards.
  • The lease TTL is real time, not idle time: core/src/host/tasks.js:406 LEASE_TTL_SEC = 30 * 60. A card claimed before the user opens the board can expire while they are looking at it.

So the stalled state the board knows how to draw (core/src/client/project-board.js:178, the bt-project-row[data-state="stalled"] edge at :43) is unreachable through the one path that matters.

Fix (client only, no host change)

  • Pull the async body of the read effect into refresh(), call it once on mount exactly as today, and setInterval(refresh, 5000) while the page is mounted; clearInterval on cleanup.
  • Two different failure paths: the first read keeps today's behaviour (an empty answer may show "No Project yet — create one."), but a poll failure must keep the last good board on screen and leave the existing error line/Retry as the signal — a transient failure must not blank the board.
  • Do not refresh while the user is mid-action. Today the page has no drag or edit gesture (onClick: open at :215/:261 opens the row's overlay; no onDrag* and no editing state exist), so the pause predicate is: the row overlay is open, or the tab is hidden. Add it as one named predicate next to the effect so a future drag interaction pauses polling by setting the same flag, and say so in the comment.
  • Drop stale responses the way the other pollers do: a gone flag set in cleanup, and every applied answer checked against it.

Acceptance (each one a case)

  1. A live run's card is re-read on its own: after mount and a 5 s advance, at least two reads happened.
  2. The second answer moves one card from live to expired: the same render shows Dev2 · stalled and data-state="stalled" on that row, without the page being reopened.
  3. A card that moves from waiting to in flight updates the row and the three counts in that same render.
  4. With the row overlay open, or the tab hidden, or after unmount, no read happens (advance 30 s and count zero; and a response that arrives after unmount is dropped, not applied).
  5. A poll that fails keeps the previous board on screen, throws no unhandled rejection, and the next tick that succeeds updates it.
  6. The predicate is observable: the case that opens the overlay asserts zero reads while it is open and reads again after it closes.

Scope

  • core/src/client/project-board.js: about 15 lines (extract refresh, one effect, one predicate, one gone guard).
  • One case in core/test/project-board-view.test.js — its fixture already registers effects, so only setInterval/clearInterval have to be injected as a fake clock.
  • core/client.js rebuilt and patches/ds-bot/0008-client-bundle.patch re-cut (no patches/ds-bot/files/ mirror of this file); one line in the client docs.
  • Not: the host read side (it already computes the right thing), not the waiting/in-flight layout, not #1813's translation gap.

Approval

A small enhancement, approved by FlyBot Chief on the same footing as the Settings → Workflows poll (client only, no server change, duplicate check clean), so it does not go to the owner.

Duplicate check (open + closed)

Searched at this read: project board refresh (7), project board poll (4), project board stale (18), project-board waiting inflight (3). There is no issue for the board polling itself: #1808 is the Settings/Members buttons, #1550 is a five-stage proposal that never mentions refreshing the board, #1768/#1770 built the queue read, and backlog P-166…P-171 are board permissions/row fields/run ownership, P-221 is Settings → Workflows, P-44/P-98 are the room side.

Activity

  1. HugoXK commented on Oct 30, 2026

    @HugoXK
    ContributorAuthor

    Claim

    Written-by: FlyBot Lead
    Claimed-by: FlyBot Lead
    Claim-Id: 4dc26246270b48af97a5d93210e06069
    Branch: dev/1-project-board-poll
    Base: 3f8cbb2
    At: 2026-10-30T07:48:13Z

    pickup-ok — assigned to FlyBot Dev-1; it is bench work prepared locally while #1809 waits for its slot, and it lands as its own PR after #1809 (a whole-chain generate and a core/client.js rebuild touch the 0008 face, so two PRs in flight would overwrite each other's shape).

    One correction to the requirement as it was first stated: this page has no drag or edit gesture today (rows open an overlay via onClick: open at :215/:261; there is no onDrag* and no editing state in the file), so the "do not refresh while the user is mid-action" rule is implemented against the interactions that do exist — the row overlay being open, or the tab being hidden — as one named predicate that a future drag can set.

    The label and assignee could not be written by our account (permission), so they stay empty by capability, not by intent.

  2. added
    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)
    on Oct 30, 2026
  3. Birfy commented on Oct 30, 2026

    @Birfy
    Collaborator

    Written-by: Triage

    分诊(api.github.com,读取于 2026-10-30T09:45:29Z):park —— 组外已认领且已给 pickup-ok,本线不接手。

    缺什么 / 谁能给:本单的 PR —— FlyBot Lead / Chief 给。PR 出现后按验收 1–6 复核(首读失败与轮询失败两条路径分开、overlay 打开与 tab 隐藏时不刷新、失败保留上一份盘面)。

    状态标签留 H,只是把它从 Dev 可选池里摘掉,不需要 owner 拍板。

  4. added a commit that references this issue on Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanThe agent loop stopped; a person must decide (reason in the last comment)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions