package latest import "time" // LaneState is the administrative page's view of one Poll Lane (issue #102): // what the Lane's last completed pass saw. Due and Gap are filled in as the // pass computes them, so a pass that returned before reaching a figure (Lane // in refusal backoff, no fetcher) records a zero in its place. type LaneState struct { Site string Due int LastRun time.Time Gap time.Duration Clamped bool Refusing bool Browser bool } // Status is the owner's page snapshot of the whole poller (issue #102). type Status struct { Lanes []LaneState BrowserConfigured bool BrowserReachable bool } // LaneStatus returns a copy of the poller's Lane state for the owner's page. // Only Sites that have completed a pass appear — a restart therefore renders // "no data yet" instead of confident zeroes — in the same order Run iterates. // Refusing is derived at snapshot time from the refusal backoff, not stored, // so a Lane that cooled down between passes reports false without a new pass. // BrowserReachable mirrors the Lanes' own gate: the sidecar is down only // within the refuseBackoff window since its last loss. func (p *Poller) LaneStatus() Status { p.mu.Lock() defer p.mu.Unlock() lanes := make([]LaneState, 0, len(p.laneStates)) now := p.Now() for _, name := range laneNames() { st, ok := p.laneStates[name] if !ok { continue } st.Refusing = now.Before(p.refuseUntil[name]) lanes = append(lanes, st) } configured := p.BrowserFetch != nil reachable := configured if reachable && !p.browserDownAt.IsZero() && now.Sub(p.browserDownAt) < refuseBackoff { reachable = false } return Status{Lanes: lanes, BrowserConfigured: configured, BrowserReachable: reachable} } // recordLaneState stores one Lane's last pass for LaneStatus. Called deferred // from runLanePass so every return path records, even a pass that refused. func (p *Poller) recordLaneState(st LaneState) { p.mu.Lock() defer p.mu.Unlock() if p.laneStates == nil { p.laneStates = make(map[string]LaneState) } p.laneStates[st.Site] = st }