1.33.0 — Search Console, proven and reachable
Added
-
The app tells you an update exists, and offers to fetch it. It knew
before —checkIfDue()has had a one-a-day guard for versions — but it was
only called from the main view's.task, which runs once when the window
appears. An app left open for a week checked once, in that week's first
minute, and a release cut the next morning went unmentioned until somebody
quit and came back. And when it did know, the only place that said so was a
Settings pane somebody had to think to open.Now an hourly timer and
didBecomeActiveboth funnel through the same
day-old guard — the timer for an app left running, becoming active for the
laptop that was shut overnight, since a timer neither fires while asleep nor
keeps its schedule afterwards.A banner sits above the report rather than in front of it: a new version is
worth mentioning and never worth interrupting a crawl for. Dismissing it
dismisses that version for good, because a bar that returns every launch is a
bar people learn to ignore.Downloading shows a real fraction where GitHub sent a length and an
indeterminate spinner where it did not — a bar that sits at zero and jumps to
full is worse than one that admits it cannot say. It unpacks withditto,
which is what wrote the archive and what keeps the bundle's signature intact.It stops at the drag, deliberately. Replacing a running bundle safely
needs a helper process that outlives the app it is overwriting, which is
Sparkle's whole job; and a Homebrew install has one correct answer that is not
this one, since overwriting the bundle behindbrew's back leaves its records
describing a version that is no longer there. So a cask install gets the
brewcommand run in Terminal where it can be watched, and everything else
gets the file revealed in Finder. -
A Search Console pane in the macOS app, and the whole Help row is now the
click target rather than its chevron.DisclosureGrouponly hit-tests its own
triangle, which left a full-width row that looked clickable everywhere and
answered in one corner — that reads as the app being broken, not as a small
control.The pane takes the property and offers a sign-in, which runs the engine's own
--search-console-loginthrough the bundled Node. The token never passes
through the app and is never displayed: a token on screen is a token in a
screenshot. What comes back is the list of properties the account can read,
each with a button to use it, because a token that can read nothing looks
exactly like one that works until a run says the property was not found.The hosted Worker will not honour
?search-console=unless the runtime
setsALLOW_SEARCH_CONSOLE, and--servesets it only because it binds to
the loopback address. The credentials belong to whoever started the server, so
a deployed Worker accepting a property name would hand a stranger somebody
else's traffic data. Gated, shape-checked, and tested. -
A Help pane in the macOS app. Written as the questions the app actually
raises — why a crawl takes minutes, why the page count differs from the
sitemap's, where the score is, why performance is blank until it is switched
on, what leaves the machine — rather than a tour of the controls. A control
that needs explaining is better renamed; a decision is what needs saying,
and this app makes several that surprise people.Folded away rather than expanded: a wall of prose in a settings window is a
wall nobody reads, and a list of questions is scannable. -
--search-console-login. The three Search Console credentials were
documented for a year and there was never a way to obtain the third, which is
the actual reason--search-consolehad never run against the live API: not
the code, the paperwork in front of it.Loopback OAuth, which is what Google calls the installed-app flow — a desktop
client may redirect to any port on 127.0.0.1 without registering it, so this
listens on an ephemeral one and the browser does the signing in. Read-only
scope. The refresh token is written to~/.config/seo-audit/.envat mode
600and never printed, because a token echoed to a terminal is a token in a
scrollback buffer. It rewrites that one line and leaves the PageSpeed key
alone, which is tested, because clobbering somebody's key to save a token
would be a poor trade.Then it lists the properties the account can read. A token that can read
nothing looks exactly like one that works, right up until an audit says the
property was not found.Not a GitHub Action input, deliberately: it opens a browser, and a flag CI
can accept and never satisfy is worse than no flag.src/options.mjsrecords
the same answer for the macOS window.The parts that can be wrong quietly are separate exported functions with
tests — the authorisation URL, the token exchange, the file rewrite — and the
loopback flow itself is exercised end to end with only Google faked,
including that a reply carrying the wrongstateis refused and writes
nothing.
Fixed
-
Search Console could never read its credentials from the dotfile. It had
its own copy of the loader thatpsi.mjsuses, and its copy built the pattern
withnew RegExpand a template literal, where\\ssurvives as an escaped
backslash rather than as whitespace. The regex compiled to
/^\\s*GSC_CLIENT_ID\\s*=.../m— a literal backslash followed bys— so it
could not match a line of a real.env. It never threw. Only environment
variables ever worked.Nothing caught it because the only tests for that path injected credentials
and used a fake API, which is exactly what "never run against the live API"
hides. There is now one loader inconfig.mjsthat both callers use, and it
is tested against a dotfile with the whitespace a hand-edited file actually
has — including a name that is a prefix of another, which the broken pattern
would also have got wrong.