Skip to content

Staging - #827

Merged
zakius merged 38 commits into
mainfrom
staging
Sep 12, 2026
Merged

zakius merged 38 commits into
mainfrom
staging

Conversation

@zakius

@zakius zakius commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

No description provided.

zakius and others added 30 commits September 12, 2026 19:31
Fix decoding of quoted feed charset labels
Restore removed subscriptions from repeated OPML imports
The accessibility tree exposed every app dialog unnamed (#817): the
<dialog> had no label and no association with the heading or prompt
text it already shows. It now takes its accessible name via
aria-labelledby from the cheat sheet's heading, or from the visible
prompt/message text — which already identifies the item in delete
confirmations. Browser specs pin the name for all three variants.
Name dialogs from their visible heading or prompt
Clearing the search box while its request was still in flight left the
response free to repopulate the list (#815). exitSearch() now gives its
in-flight request the same supersession treatment select() and
runSearch() give theirs -- abort plus a fresh guard token -- so a late
response is dropped and the cleared view holds. The no-selection branch
also resets the paging state and the loading signal, which the aborted
request can no longer clear for itself. Browser spec pins the behaviour
with a held-then-released response.
Pressing o on a narrow screen opened the reader pane while the list
holding focus was display:none'd, dropping focus to <body> and making
the second o unreachable (#816). Opening now focuses the reader pane
(preventScroll, so the reader's scroll position survives), and the pane
listens for o itself to hand focus back to the same article row -- the
only place the key can be heard once the list is hidden. Browser spec
pins the round trip at 390px.
…ults

Drop a search response that lands after the field was cleared
Two simultaneous confirmations of the same reset link both succeeded,
each setting a different password, last write deciding (#809): the
token check ran as a lookup before a write keyed only on users.id. The
spend now rides in the UPDATE's WHERE clause inside the same
transaction -- the row lock serialises the racers, the second re-check
matches nothing, and the route answers the loser with the same
'This reset link is no longer valid.' any stale link gets. Sessions
drop only with a winning commit. An integration test races two
completions against a real database.
Move keyboard focus with the o pane toggle
Automatic cleanup deletes article rows straight through SQL, so an old
unread newsletter could vanish while its source kept counting it: the
badge said unread, and the list had nothing to show (#812). Each prune
now returns the ids of the sources it shrank, and the cleanup job feeds
them to recomputeUnreadCounts -- the same recount every other article
change already goes through. The worker wiring is pinned by a unit
test, the counter correction by a database integration test.
A push notification arriving while a source's feed job was running was
answered ok and then dropped: the enqueuer's per-source job id made
BullMQ dedupe the add away, and nothing remained to fetch the new
content until the next poll -- for a verified websub source, up to a
day (#813). A request landing on a merely-queued poll kept that poll's
old payload, so a manual refresh silently lost its cache bypass.

enqueueSource now looks before it adds: a waiting or delayed job gets
its payload rewritten in place with the requested flags, and an active
job records the request in a Redis marker that the worker consumes when
the run ends, folding everything that arrived into one follow-up
refresh. Marker writes are ordered so a cache bypass always dominates.
Deferred runs keep their id and are skipped -- their retry reaches the
request through the rewritten payload instead.
Recount unread totals for the sources cleanup prunes
Each of these runs the real oxlint binary against a temp fixture, and
bun's default 5s per-test timeout measured the whole spawn, not the
lint. On a loaded machine -- parallel test workers, language servers,
other checkouts -- cold spawns blew past it and the suite flaked.
20s per test is still far under what a hang would take.
Make the first password-reset completion the only one
An activation link older than a day was a dead end: the account stayed
inactive, registering again answered success and sent nothing, and the
password reset could not reach an account that never activated (#810).
Re-registration now reads the address's own state and answers only in
the mailbox: a pending registration gets a fresh activation link, and
an active account gets a notice -- the same success body either way,
so the browser still cannot tell who exists -- carrying a reset link,
the way in that never assumes the password still works.

The page behind an activation link no longer activates on visit either:
a mail preview or link scanner following the URL found the token spent
by the on-mount POST. Activation now happens on the button, and the
reset page was already submit-driven. Token digesting moves to a
shared helper, since registration now stores reset digests too.
Answer a refresh that lands on a running feed job with a follow-up
…very

Recover accounts stuck behind an expired activation link
Brings staging up to main's tip so the release PR can merge (#827).
The follow-up refresh #813 promises never ran. runPendingRefresh called
enqueueSource from inside the processor, where BullMQ still holds
ParseSource-<id> as active -- so enqueueSource took its own active branch
and wrote the pending marker straight back instead of queueing anything.
The marker ping-ponged and the content still waited for the next poll,
which for a verified websub source is up to a day: the exact symptom the
issue is about. The unit test could not see it, stubbing enqueueSource
out entirely.

The id cannot be free until the processor returns, so the run answers
the requests itself: take the marker, parse once more with the flags
they asked for. A deferral or a failure leaves the marker for the next
run, as before. This also drops a dead branch -- parseSource only
rethrows HttpDeferredError, so the non-deferred catch never ran.

Also: the re-registration path stored its fresh activation token after
mailing it, so a failed write mailed a token the row never took and put
the address back in the dead end #810 fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zakius and others added 2 commits September 12, 2026 22:07
Fold a mid-run refresh into the run instead of re-enqueueing it
A staging run's unit-test job hung at its first step and sat there for
hours: no timeout-minutes meant GitHub's 6-hour default applied. Every
later staging run then parked in docker-build's await_turn, which counts
any older run still in flight, so one stuck runner stalled the whole
release queue -- visible as "CI is stuck" with nothing failing.

The caps are well above the real runtimes (lint ~20s, compile ~15s, unit
~30s, browser ~1m), so they only ever fire on a hang. Job names and ids
are untouched: they are pinned as required checks on main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zakius and others added 6 commits September 12, 2026 23:32
Cap the quality jobs so a hung runner cannot hold the release queue
Free usage is exhausted and the next invoice is real money. Every job now
runs on GitHub-hosted runners, which are free for a public repository:
blacksmith-{2,4}vcpu-ubuntu-2404 -> ubuntu-latest, and
useblacksmith/setup-docker-builder -> docker/setup-buildx-action.

The image build could not follow that straight across. Blacksmith built
both architectures natively; ubuntu-latest would have to emulate arm64
through QEMU, which turns the Bun build from a minute into most of an
hour. So each architecture gets its own runner -- ubuntu-latest and
ubuntu-24.04-arm, both free here -- pushes an untagged blob, and reports
its digest. publish_images keeps its id and its name, and now assembles
the two digests into the manifest that claims the sha tag. It refuses to
publish unless both legs arrived: a one-arch manifest would strand
whichever node runs the other, and promote would copy it onward.

Layer caching moves with it. Blacksmith cached implicitly; type=gha does
not, and needs a scope per target and architecture, or the three targets
evict each other on every run.

Job-level timeouts fill in everywhere they were missing, 15 minutes
unless the work needs longer -- the same gap that let a hung unit-test
job hold the staging release queue for an afternoon.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The options page opened by reading chrome.storage.sync, and only wired up
its inputs once that resolved. `load` fires when the script starts, not
when the read lands, so a change arriving in that window reached no
listener and was dropped in silence -- the field kept a value the page
had already decided to reject, with nothing said.

Too narrow for a person to hit by typing, wide enough for a test: the
browser suite failed on it the first time it ran on a runner with
different timing.

Listeners now attach synchronously and the stored values fill in
afterwards. A change in the window before hydration reverts to an empty
field rather than the stored address, which is what the field already
shows at that instant.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…orage

Attach the options listeners before reading storage
@zakius
zakius merged commit 7485286 into main Sep 12, 2026
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant