Admin dashboard: pages, Lane observability, poll pass log, per-Series intervention (#134) #148

Merged
sulthan merged 22 commits from spec-134 into main 2026-08-22 08:27:20 +07:00
6 changed files with 14 additions and 11 deletions
Showing only changes of commit d72c48295d - Show all commits
+1 -1
View File
@@ -134,7 +134,7 @@ unchanged read is exactly the Sighting worth deferring a Poll on.
rest.
**Refusals and browser loss are Lane-local.** Two `errChallengeHeld` in a pass
stop that Site for `refuseBackoff` while other Lanes continue. An
stop that Site for `RefuseBackoff` while other Lanes continue. An
`errBrowserInterrupted` (remote Chrome restarted) sets a shared Poller flag so
the *other* browser Lanes skip their passes for the same window — otherwise a
restarting Chrome stamps one Series per Lane per pass, burning rests on
-1
View File
@@ -4,7 +4,6 @@ import (
"log"
"net/http"
"strconv"
"time"
"bookmarkmanager/backend/internal/store"
)
+4
View File
@@ -81,6 +81,10 @@ func (h *Handler) lanesView() lanesView {
passes, err := h.store.LatestLanePasses()
if err != nil {
log.Printf("admin lanes: latest passes: %v", err)
// No evidence of a lost sidecar reads as reachable, per the same rule
// browserReachable applies: a store failure must not condemn the
// browser. The empty table already says no Lane has recorded a pass.
v.BrowserReachable = true
return v
}
now := time.Now()
+2 -3
View File
@@ -14,9 +14,8 @@
<div class="sechead">
<h2 class="sec">Poll Lanes</h2>
<p class="statusline">
{{if .PollerOff}}Polling is switched off in this deployment: no Lane
runs, and Latest Chapter comes from the userscripts alone.
{{else}}Browser: {{if not .BrowserConfigured}}<span class="mark-faint">not configured</span>{{else if .BrowserReachable}}<span class="mark mark-strong">reachable</span>{{else}}<span class="mark mark-strong bad">unreachable</span>{{end}}{{end}}
{{if .PollerOff}}Polling: <span class="mark-faint">off</span>
{{else}}Browser: {{if not .BrowserConfigured}}<span class="mark-faint">not configured</span>{{else if .BrowserReachable}}<span class="mark mark-strong">reachable</span>{{else}}<span class="mark bad">unreachable</span>{{end}}{{end}}
</p>
</div>
{{if .Rows}}
+2 -1
View File
@@ -343,7 +343,8 @@ func newLatestPoller(s *store.Store, cfg LatestPoll, fetch, browser latest.Fetch
// HTTP client cannot be built. Any problem here is logged and skipped: this
// feature going missing degrades the service to userscript-only latest-chapter
// tracking, which is exactly how it behaved before. It returns the running
// Poller, or nil when there is none — the admin page's Lane status reads it.
// Poller, or nil when there is none; the caller starts it for its own sake —
// the Lanes page reads the pass log, so no return value is wired anywhere.
func startLatestPoller(ctx context.Context, s *store.Store, cfg LatestPoll, browser latest.Fetcher) *latest.Poller {
if !cfg.Enabled {
log.Println("latest-chapter poller: disabled by config")
+5 -5
View File
@@ -885,7 +885,7 @@ func TestAdminPageWithoutAPollerSaysSo(t *testing.T) {
cfg Config
want, unwant string
}{
{"polling switched off", testConfig(), "Polling is switched off", "not configured"},
{"polling switched off", testConfig(), `Polling: <span class="mark-faint">off</span>`, "not configured"},
{"browser not configured", lanesConfig(), "not configured", "Polling is switched off"},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -986,7 +986,7 @@ func TestBrowserConfigAndReachabilityDerived(t *testing.T) {
router, st := newWebTestServer(t, lanesConfig())
seedPass(t, st, store.LanePass{Site: "kagane", RanAt: now.UnixMilli(), Skip: latest.SkipNoFetcher})
body := lanesBody(t, router, st)
if !strings.Contains(body, "not configured") || strings.Contains(body, "unreachable") {
if !strings.Contains(body, `mark-faint">not configured`) || strings.Contains(body, "unreachable") {
t.Errorf("unset BROWSER_WS_URL must read as not configured:\n%s", body)
}
})
@@ -997,7 +997,7 @@ func TestBrowserConfigAndReachabilityDerived(t *testing.T) {
router, st := newWebTestServer(t, cfg)
seedPass(t, st, store.LanePass{Site: "kagane", RanAt: now.Add(-time.Minute).UnixMilli(), Skip: latest.SkipSidecarDown})
body := lanesBody(t, router, st)
if !strings.Contains(body, "unreachable") {
if !strings.Contains(body, `bad">unreachable`) {
t.Errorf("recent sidecar-down passes must read as unreachable:\n%s", body)
}
})
@@ -1008,7 +1008,7 @@ func TestBrowserConfigAndReachabilityDerived(t *testing.T) {
router, st := newWebTestServer(t, cfg)
seedPass(t, st, store.LanePass{Site: "kagane", RanAt: now.Add(-time.Minute).UnixMilli(), Checked: 3})
body := lanesBody(t, router, st)
if !strings.Contains(body, "reachable") {
if !strings.Contains(body, `mark-strong">reachable`) {
t.Errorf("clean browser passes must read as reachable:\n%s", body)
}
})
@@ -1019,7 +1019,7 @@ func TestBrowserConfigAndReachabilityDerived(t *testing.T) {
router, st := newWebTestServer(t, cfg)
seedPass(t, st, store.LanePass{Site: "kagane", RanAt: now.Add(-30 * time.Minute).UnixMilli(), Skip: latest.SkipSidecarDown})
body := lanesBody(t, router, st)
if !strings.Contains(body, "reachable") {
if !strings.Contains(body, `mark-strong">reachable`) {
t.Errorf("a sidecar-down older than the backoff must read as reachable:\n%s", body)
}
})