1e6f1e985d
Closes #102.
The only operational surface was /healthz and a fold-out roster inside the owner's own reading page. This adds /admin: an owner-only page carrying the Reader roster and one row per Poll Lane.
- **Poller seam.** `latest.Poller` records each Lane's last pass (`Site`, `Due`, `Checked`, `LastRun`, `Gap`, `Clamped`, `Browser`) and answers `LaneStatus()`; the page reads that snapshot, never a table. A pass that returns before computing its figures (refusal backoff, sidecar down) carries the previous pass's figures forward rather than recording zeroes, and a Lane that has never reached a pace renders no gap at all. Refusal and sidecar reachability are derived at snapshot time.
- **Owner gate at registration.** Every route reaching past the acting Reader lives in `adminRoutes()` and is wrapped in `requireOwner` when it is registered, so a missing gate is visible in the route list rather than hidden in a handler. `web.AdminPatterns()` is what the gate test walks, so a new route cannot be added without being tested. A non-owner gets 404, never 403.
- **Nil poller is a first-class state.** `main.newRouter` takes the reporter as an interface and converts a nil `*Poller` to a nil interface; no poller and no completed pass both render "No data yet" with the reason spelled out, rather than confident zeroes.
- **Roster moved** off the reading page onto /admin, with the Sighting counters and a confirm-gated `Clear marks` control. #103 fills those counters, so on delivery they read zero for everyone - deliberate ordering.
- **One accent, `--patina`** (verdigris, both colour branches): the far side of the wheel from ember's crimson and clear of the archive blue. Ember still means new chapter only; revocation still wears --danger.
Verification: `go vet ./...` and `go test ./...` green (Docker-backed); admin page screenshotted at 1100px and 390px in both colour schemes. Reviewed on both axes (spec, standards); findings on the accent hue, zero-figure honesty and three tests that could not fail are fixed in 58014eb.
Reviewed-on: #107
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
75 lines
2.7 KiB
Go
75 lines
2.7 KiB
Go
package latest
|
|
|
|
import "time"
|
|
|
|
// LaneState is the administrative page's view of one Poll Lane (issue #102):
|
|
// what the Lane's last pass saw. Due, Gap and Checked are filled in as the
|
|
// pass computes them; a pass that returned before reaching a figure (refusal
|
|
// backoff, sidecar down) carries the previous pass's figures forward rather
|
|
// than overwriting them with zeroes the page would state as fact.
|
|
type LaneState struct {
|
|
Site string
|
|
Due int
|
|
LastRun time.Time
|
|
Gap time.Duration
|
|
// Checked is how many Series this pass actually read. A Lane with Series
|
|
// due and nothing checked has stopped working; one with nothing due is
|
|
// merely quiet, and the page must not draw the two the same (story 13).
|
|
Checked int
|
|
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.
|
|
// A pass that never reached the pace (Gap zero) keeps the last pass's figures:
|
|
// the Lane's due count and gap did not become zero because this pass declined
|
|
// to look, and the row's own marks say why it declined.
|
|
func (p *Poller) recordLaneState(st LaneState) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
if p.laneStates == nil {
|
|
p.laneStates = make(map[string]LaneState)
|
|
}
|
|
if prev, ok := p.laneStates[st.Site]; ok && st.Gap == 0 {
|
|
st.Due, st.Gap, st.Clamped, st.Checked = prev.Due, prev.Gap, prev.Clamped, prev.Checked
|
|
}
|
|
p.laneStates[st.Site] = st
|
|
}
|