Only sidecar-down ever writes a Site=='' row, so the loop's empty-Site
clear for the other three conditions was a no-op delete on every pass.
Move it out of the per-condition loop, scoped to ConditionSidecarDown.
A fresh database whose browser Lanes skipped from day one has no
sidecar-reaching pass, so sidecarDownSince returned Since 0 and the notice
rendered the age since the epoch. Report the window itself: 'at least
twelve hours' is the honest floor the condition guarantees.
FaultsFrom judges three more faults at the shared twelve-hour OwnerWindow:
a no-browser-route Site's refusing run (browser-backed Sites excluded), a
sidecar no Lane has reached (one site-wide fault), and a Site where more
than half of Series hold an old no-chapter failure row. recordPass fills
the inputs from three new store reads (RefusingSince, SidecarOK,
NoChapterShare), each failing independently and never failing a pass, and
the clear loop forgets the empty-Site row too so a lifted sidecar-down
fires again on return.
The Series list row's fact line gains the failure word and its age in one
mark before the Notes chips, spelled through outcomeWord — the one
vocabulary shared with the Lanes chips. The detail page adds one sentence
beside the correction control while a Reader's number stands behind a
failure past the owner window, without naming the Reader. Each Lanes Site
name is a plain link to that Site's failing Series; the outcome chips stay
unlinked, and outcomeChips records why the promise is withdrawn.
The Site's own completed marker (issue #168) joins the owner's surface as
an informational hint, never a decision:
- store: SeriesFilterSiteCompleted ('site_completed') sits beside Finished
in the filter vocabulary; its predicate carries the same finished_at
guard as the clock-driven filters, because the Site's stamp keeps
standing after the owner retires the row. The projection and SeriesPage
GROUP BY carry site_completed_at so the detail page can age it.
- web: the filter joins the Series list select as 'Site says completed'
and rides the tail of seriesFilterOrder, so the landing stats figure
comes for free and sits after the finished figure.
- detail: one hint line beside the Finish control - 'the site says this
work is completed (since 3d ago)' - rendered only while the stamp
stands and no Finish stands; the Finish control itself is untouched.
- tests: filter's finished exclusion and un-finish return to the Poll
query (store); figure at zero and nonzero, hint present/absent, and the
Finish control's markup byte-identical with a hint present (web).
Rollout note: the owner_notices table starts empty, so the first pass after deploy sends for conditions already true — correct per one-row-per-episode; say so rather than have it reported as a bug. Prod step: create the webhook, set DISCORD_WEBHOOK_URL on the deployment, redeploy — unset is silent by design, and without that step the feature ships dark. Security invariants preserved: the webhook address is a secret in the class of TOKEN_KEY (never logged, never on a config-printing line), and the owner gate is unchanged.
Add series.site_completed_at (epoch-ms, default 0): the durable answer
#168's predicates compute. checkOne writes it after the successful read
on the zero/non-zero transition only, so a Series still completed keeps
its original stamp (the age #170 prints is 'since the Site first said
so'), one that stopped is zeroed, one that became completed is stamped.
The due query projects the column, so the transition is a field
comparison on the snapshot checkOne already holds.
Refused, unreachable and errored reads reach nothing (AC4 is placement,
not a guard); the write sits before the no-chapter and unchanged-number
returns, so a completed page whose chapter did not change still writes.
Store failure logs and carries on — the outcome word never changes.
Series-level like Latest Chapter: a bookmark's updated_at is never
touched. #170 is the surface; nothing reads the column yet.
Spec review flagged the live-verification acceptance criterion as
unproven in the diff: the brief's adapter rule wants the probe
recorded, not just the date. Each fixture comment now names how the
live body was fetched on 2026-08-22 — curl probe for asura,
demonic and lightnovelworld; cleared Chrome tab (CDP sidecar) for
comix, kagane and novelfull, including the challenge/403 fallback
story for novelfull. Also renamed lnwCompletedRe to lnwStatusRe for
symmetry with the other site-marker vars.
site gains a Completed predicate answering whether a fetched body
carries the Site's own completed value: asura escaped props status,
demonic info-block Status pair, comix scoped detail entry (shared
comixDetailQuery helper with the cover read), kagane
publication_status only, novelfull status link, lightnovelworld
JSON-LD creativeWorkStatus. siteCompletedFrom dispatches it; an
absent hint stays false. Fixtures trimmed from live pages fetched
2026-08-22; TestSiteCompletedFrom covers completed, ongoing,
selector-removed, and challenge bodies per site, the kagane
upload/publication divergence, and an unknown site.
Local stack: `docker compose up` (bookmark-api + postgres only; `postgres-data` named volume, `restart: unless-stopped`). No browser — without `BROWSER_WS_URL` the poller logs and skips kagane and comix. To run one: `cd chrome && BROWSER_BIND_ADDR=172.17.0.1 docker compose up -d --build`, then `BROWSER_WS_URL=ws://172.17.0.1:9222` in the root `.env` (bridge gateway, so the API container can name it by IP).
Local stack: `docker compose up` (bookmark-api + postgres only; `postgres-data` named volume, `restart: unless-stopped`). No browser — without `BROWSER_WS_URL` the poller logs and skips kagane and comix. To run one: `cd chrome && BROWSER_BIND_ADDR=172.17.0.1 docker compose up -d --build`, then `BROWSER_WS_URL=ws://172.17.0.1:9222` in the root `.env` (bridge gateway, so the API container can name it by IP).
**Clean up Docker after testing.** Storage on the dev machine is scarce, so
anything you started for a test you also tear down before calling the work
done: `docker compose down -v` for a stack you brought up, `docker rm -f -v`
for a container you ran by hand (`-v`, or the image's anonymous data volume
survives). Then check `docker volume ls` and `docker system df` for leftovers
and reclaim them — a dangling volume nobody notices is the leak that fills the
disk. Never remove the `postgres-data` volume of a stack the user is actually
running, and never blanket-`docker system prune` their images or build cache.
Live CDP proof (needs that browser and network, skipped otherwise):
Live CDP proof (needs that browser and network, skipped otherwise):
`SMOKE_BROWSER_WS_URL=ws://<ip>:<port> go test -run 'TestSmokeKagane|TestSmokeComix' ./internal/latest`
`SMOKE_BROWSER_WS_URL=ws://<ip>:<port> go test -run 'TestSmokeKagane|TestSmokeComix' ./internal/latest`
— fetches a real kagane and comix cover and chapter list. A red run means the challenge is
— fetches a real kagane and comix cover and chapter list. A red run means the challenge is
# Host header is an IP address or "localhost" — confirmed 2026-08-03,
# Host header is an IP address or "localhost" — confirmed 2026-08-03,
# independent of chromedp's own dial logic. The same trap that used to
# independent of chromedp's own dial logic. The same trap that used to
# force a pinned Docker IP now forbids the tailnet name.
# force a pinned Docker IP now forbids the tailnet name.
BROWSER_WS_URL:${BROWSER_WS_URL:-}
#
# Owner-notice webhook (issue #171). Empty default, never a
# Owner-notice webhook (issue #171). Empty default, never a
# required-guard: unset means the whole path is off, so a local stack
# required-guard: unset means the whole path is off, so a local stack
# runs exactly as it does today. An env var not listed here never
# runs exactly as it does today. An env var not listed here never
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.