Pressing Leta produced mails=25, documents=0 on a real two-mailbox run.
Nothing was found because nothing was searched: every request came back
429 "Too many concurrent requests for user".
Two bugs, and the second is the one that matters.
The search fanned out with Promise.all over every message id at once, one
Gmail request per message, per connection. Gmail enforces a per-user
concurrency ceiling as well as a daily quota, and this sailed past it long
before any volume worth worrying about. It now runs through a pool of five
per connection, which is comfortably under and still finishes a page of
results in a couple of round trips.
The catch turned each refusal into an empty array, with a comment saying
one mailbox's failure must not become the company's. Right instinct, wrong
consequence: an empty array is also what an empty mailbox returns, and the
manual hunt loop stops on fetched === 0 because that is its signal for
"the mailboxes hold nothing more for what is open". So a rate-limited
search told the user their receipts do not exist, and stopped looking.
searchFailureCount() now separates "could not look" from "nothing there".
The run route reports it, and the loop treats a pass with failures as
failed rather than finished, so pressing again is the obvious next move
instead of a pointless one.
This is the failure this feature exists to catch, happening inside the
feature: silence that reads as an answer.
Restoring the unbounded fan-out fails one test; removing the failure
counter fails three.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(inbox): run the receipt hunt from the page it fills
The hunt could only be started from Settings. The page where a person
notices that receipts are missing had no way to go and look for them, and
the button that fixes it sat behind a different navigation item. That seam
is the kind that makes a working feature look broken.
"Leta i mejlen" now sits in the Underlag header, next to Ladda upp, and
only when a mailbox is actually connected: offering it otherwise promises
something it cannot do.
The loop moves into a shared hook rather than being copied. It belongs to
neither surface, and two implementations of "when does a run stop" would
eventually disagree about the one thing that matters, which is that a pass
finding nothing new means the mailboxes hold nothing more for the
purchases still open.
Each pass refreshes both lists, so a run fills the page as it goes instead
of all at once at the end. That matters more here than in Settings: a pass
can attach a document to a purchase, which moves a row out of "saknar
underlag" and into the inbox, and watching that happen is the feedback
that the button did something.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(inbox): only offer the hunt when a mailbox can actually be searched
Counting connection rows does not answer whether anything is searchable. A
revoked or expired connection is still a row, and the hunt skips it, so the
button promised a search that would return nothing on every pass.
A dead mailbox that still looks healthy is the exact failure this feature
exists to surface. Starting by doing it in its own header would be a poor
joke.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>