diff --git a/backend/internal/latest/poller.go b/backend/internal/latest/poller.go index cf7be40..dbe9580 100644 --- a/backend/internal/latest/poller.go +++ b/backend/internal/latest/poller.go @@ -57,11 +57,12 @@ type Poller struct { // refuseUntil gates a Site's Lane after it refused twice in one run: no // Series of that Site is attempted again before this time (issue #100). + // The stamp is durable — the pass gate reads it from the store, so a + // restart does not forget the refusal; nothing of it lives in memory. // browserDownAt is when a browser Lane last lost the sidecar; the other // browser Lanes skip their passes for the next RefuseBackoff, so a // restarting Chrome does not stamp one Series per pass per Lane (story 20). mu sync.Mutex - refuseUntil map[string]time.Time browserDownAt time.Time // coverWG tracks in-flight cover work. Covers heal in the background so a // slow cover host cannot delay the next Series-page Poll; tests join it @@ -437,12 +438,8 @@ func (p *Poller) runLanePass(ctx context.Context, name string, paced bool) time. log.Printf("latest poll %s: due=%d checked=%d", name, len(due), fig.Checked) } if refusals >= 2 { - p.setRefusalBackoff(name, now.Add(RefuseBackoff)) // The refusal outlives the process: the durable stamp gates a restart, - // so a Site that just told us to back off is not re-probed. The - // in-memory twin is still written because the Lane status block reads - // it directly; the gate reads the durable stamp, so a restart does not - // forget the refusal. + // so a Site that just told us to back off is not re-probed. if err := p.Store.SetLaneRefusal(name, now.Add(RefuseBackoff).UnixMilli()); err != nil { log.Printf("latest poll %s: persist refusal: %v", name, err) } @@ -511,15 +508,6 @@ func (p *Poller) countEligible(site string) (int, error) { return p.Store.EligibleSeriesCount(site) } -func (p *Poller) setRefusalBackoff(name string, until time.Time) { - p.mu.Lock() - defer p.mu.Unlock() - if p.refuseUntil == nil { - p.refuseUntil = make(map[string]time.Time) - } - p.refuseUntil[name] = until -} - // setBrowserDown records when a browser Lane lost the sidecar. It is Poller // state rather than pass state so the other browser Lanes see it too. func (p *Poller) setBrowserDown(now time.Time) { diff --git a/backend/internal/web/admin_lanes.go b/backend/internal/web/admin_lanes.go index 36b940d..ae5fe73 100644 --- a/backend/internal/web/admin_lanes.go +++ b/backend/internal/web/admin_lanes.go @@ -84,13 +84,14 @@ func (h *Handler) lanesView() lanesView { return v } now := time.Now() - outcomes, err := h.store.LanePassOutcomes(time.Now().Add(-ownerWindow).UnixMilli()) + outcomes, err := h.store.LanePassOutcomes(now.Add(-ownerWindow).UnixMilli()) if err != nil { + // The rows are complete without the chips, so a failed outcome sum + // must not blank the table into "no data yet" — that is the confident + // wrong statement the page exists to avoid. Every row renders "none + // observed" instead, which is honest. log.Printf("admin lanes: outcomes: %v", err) - return lanesView{ - PollerOff: v.PollerOff, - BrowserConfigured: v.BrowserConfigured, - } + outcomes = nil } bySite := make(map[string]store.SiteOutcomes, len(outcomes)) for _, o := range outcomes { @@ -193,8 +194,11 @@ func laneState(p store.LanePass, now time.Time) (phrase string, good, attention case latest.SkipAsleep: phrase = "browser asleep" good = true - case latest.SkipDueQuery, latest.SkipEligibleCount: - phrase = "check failed" + case latest.SkipDueQuery: + phrase = "due query failed" + attention = true + case latest.SkipEligibleCount: + phrase = "eligible count failed" attention = true case latest.SkipNothingEligible: phrase = "nothing eligible" diff --git a/docs/adr/0012-persisted-lane-state.md b/docs/adr/0012-persisted-lane-state.md index f740522..fb592b7 100644 --- a/docs/adr/0012-persisted-lane-state.md +++ b/docs/adr/0012-persisted-lane-state.md @@ -39,6 +39,6 @@ confident zero. The poller still owns the writes: each pass exit records one row (ADR-0010), and a pass that returns before gathering figures carries the previous pass's numbers forward instead of recording zeroes. A skip is a stable wire string; -`asleep` never counts toward `Attention`. When polling is switched off the -page must say so, and the browser statusline appears only when a poller -actually answers. \ No newline at end of file +`asleep` never counts toward `Attention`. When polling is switched off +(`LATEST_CHAPTER_POLL_ENABLED` unset) the page must say so, and the browser +statusline appears only when polling is switched on. \ No newline at end of file