feat(google-ads): surface LSA credit_state, call duration, and already-submitted re-files - #319
Closed
tonyswartz wants to merge 26 commits into
Closed
feat(google-ads): surface LSA credit_state, call duration, and already-submitted re-files#319tonyswartz wants to merge 26 commits into
tonyswartz wants to merge 26 commits into
Conversation
Deletes a tracker config with all of its keywords, runs, and snapshots. Children are removed explicitly in one atomic runBatch (snapshots, runs, keywords, config) so the operation works whether or not the SQLite connection enforces foreign keys, and identically on Postgres. Deletion is refused with CONFLICT while a rank check is genuinely in flight; a stale active-looking run (dead workflow instance, per reconcileActiveRankCheckRun) does not block and is swept up by the cascade. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(rank-tracking): add delete_rank_tracker MCP tool
- Google Ads OAuth connect flow (self-hosted + hosted) reusing the shared Google client; new google-ads provider registered for token refresh - google_ads_connections table (SQLite + Postgres) with per-project Ads account selection, incl. manager (login-customer-id) support - Read-only Ads API client (v25, googleAds:search only) and LSA reporting service: spend, lead totals, cost per charged lead, weekly-budget pacing - MCP tools get_local_services_performance / get_local_services_leads, wired into SAM and the tool catalog - Dashboard card (renders only when connected) + integrations settings card; pending Google API approval surfaces as an expected state, not an error - Env/preflight/health, GDPR erasure, workspace-merge wiring; self-hosting doc Ships dark: with no connection or GOOGLE_ADS_DEVELOPER_TOKEN nothing existing changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(google-ads): read-only Local Services Ads reporting
When account discovery returns no candidates (most commonly because the Google Ads permission checkbox was left unchecked on the consent screen, which surfaces as accountsUnavailable), the picker showed only an empty state with no way to redo the OAuth flow: the Disconnect button only renders after a successful connect, so the user was stranded. Show a Reconnect with Google button in the empty state, with copy that points at the unchecked-consent cause when Google refused the listing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(google-ads): offer reconnect from the empty account picker
- Per-customer discovery failures now log a structured skip (customer id, step, status, Google error code) instead of vanishing. - Grant-wide failures (developer token approval, API disabled) abort discovery from per-customer probes too, so they classify as access pending instead of rendering an empty picker. - If every accessible customer fails its probe, the listing throws and classifies instead of reporting a misleading empty account list. - Manager expansion includes enabled non-manager clients at any depth, not just level 1, and logs expansion counts. - Log granted OAuth scopes per grant during discovery. - Empty picker state names the connected Google account email. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(google-ads): surface real causes behind an empty account picker
Production logs (deploy d063494c, 00:23Z) show Google returning 403 CLOUD_PROJECT_NOT_APPROVED_FOR_PRODUCTION for every accessible customer while the Ads API application is unreviewed. The picker classified this as accounts-unavailable and told the user "no accounts found". Move the access-pending reason list into googleAdsErrors as a shared set, add CLOUD_PROJECT_NOT_APPROVED_FOR_PRODUCTION, and use it from both the account picker and the reporting error mapper so the two paths cannot drift. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…roject fix(google-ads): treat unapproved Cloud project as access pending
Two failures that both surface to the user as "connection expired, reconnect" when nothing is actually wrong with the connection. The Ads client memoized the access-token *promise*, so one transient refresh failure was cached for the life of that client instance. Every later request on it reused the rejected promise, which the reporting layer maps to reconnect-required. Drop the memo on failure so the next call retries; concurrent callers still share a single mint. disconnect() released the shared Google grant across four separate un-transacted calls: read the connection, delete it, ask whether any other project still used the grant, then delete the grant. A project connecting on the same Google login could land between the last two and lose its refresh token. The check now lives inside the DELETE as a NOT EXISTS predicate, so there is no gap to land in. That closes the check-then-act gap but not the longer one on the other side: setAccount spends several Google round trips in discovery, and a disconnect completing inside that window would leave the project holding a connection whose grant is already gone. setAccount now re-checks the grant after writing and undoes the write instead of saving a dead link. The repository test runs real SQLite over the production migration DDL — the whole point of the fix is which statement the existence check lives in, and a mocked builder chain passes either way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Tony Swartz <tswartz@gmail.com>
…ken-retry fix(google-ads): survive a token blip and a concurrent disconnect
Five things the picker got wrong, all of them quiet — no error, just a list that didn't match reality. An account reachable both directly and through a manager, or through two managers, rendered as two identical rows (and two identical React keys). Discovery now keeps one entry per customer, preferring the direct route since reporting through it needs no login-customer-id header. Discovery stops at 10 accessible customers and 25 candidates. Hitting either bound was invisible: the account you were looking for simply wasn't there. The bound now reports itself and the picker says the list is partial. A deployment with no GOOGLE_ADS_DEVELOPER_TOKEN was classified as "access pending" — the same copy as a real Google approval wait, which tells a self-hoster to sit and wait for something that will never arrive on its own. It is now its own state with its own copy. Saving a selection re-ran full discovery just to look the account up again. That is one API call per accessible customer, and a blip on any unrelated one could reject the account the user had picked from a list that was on screen a second earlier. It now verifies the single chosen account: two calls, and only that account's own failure can fail it. The picker sends the loginCustomerId it displayed; the server checks it against the managers the grant actually reaches before using it as a header, and every stored field still comes from Google's answer rather than the request. Finally, the selection was reconciled only when empty, so a refetch that dropped the selected account left the stale pick in place and armed "Use this account" with a customer no longer in the list. It now re-checks against what the picker is offering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Tony Swartz <tswartz@gmail.com>
fix(google-ads): make the account picker tell the truth
Spend rendered as `$${(micros / 1_000_000).toFixed(2)}` regardless of the
account's real currency, so a CAD or EUR account read as dollars — a
wrong number, not a formatting nit. It now formats in the account's own
currency, and falls back to a bare amount rather than implying dollars
when Google reports no currency code.
Lead totals stopped at 1,000 while spend stayed exact for the whole
window, so an account over that line showed understated lead counts and
an overstated cost per charged lead, with nothing on screen to say so.
The query now asks for one row past the cap — the only way to tell
"exactly 1,000 leads" from "we stopped at 1,000" — and the totals carry
a `truncated` flag. The card renders "1,000+", labels cost per charged
lead as a maximum, and explains why underneath.
The MCP performance tool reports the same caveat. A truncated total
reaching an LLM unlabelled is the same bug with a worse audience.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Tony Swartz <tswartz@gmail.com>
fix(google-ads): stop the dashboard card reporting made-up numbers
Google Ads API v25 rejects local_services_lead.credit_details with PROHIBITED_FIELD_IN_SELECT_CLAUSE, which 400s the leads query. Both LSA tools share fetchLeads, so leads AND performance failed with the generic google_ads_upstream_unavailable on every live call. Verified against the live API: the query succeeds once the field is removed. creditState was only ever populated from this field, so its always-null surface goes too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(google-ads): drop credit_details from the LSA leads query
Google evaluates segments.date in the Ads account's own time zone. Report windows were cut from the server's UTC clock, so after 5pm Pacific a "today" that hasn't started there was included and a real day dropped off the start. Existing connections keep UTC until the next verify, which is the same window they already have. Stores customer.time_zone (nullable) at account selection, from Google's answer, never the client. The reporting service owns the default 28-day window and the 7-day pacing window so the dashboard and MCP tools stop computing their own UTC ranges. Explicit startDate/endDate still win. Unknown or missing zones fall back to UTC. Item 7 from the Google Ads review batch. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Tony Swartz <tswartz@gmail.com>
fix(google-ads): build report windows in the account's own time zone
…er copy Every report-path Google Ads failure reached the user as a generic code, and Google's own explanation was thrown away before anything could log it: the client pulled the error code out of the response body and dropped the message. PR #9's field rejection ("The following field may not be used in SELECT clause: 'local_services_lead.credit_details'.") showed up as "Google Ads API error (400)" with nothing in Railway, and had to be isolated by replaying the app's queries against Google by hand. The client now keeps Google's message on GoogleAdsApiError as upstreamMessage, never shown to users. The reporting service logs google_ads.report_failed with the report, project, status, reason and that message before mapping to the user-facing error, which covers the dashboard card and both MCP tools. errorLogDetails moves next to the error class so account discovery's existing failure logs carry the message too. The account picker's access-pending copy said account listing would work once Google finished onboarding. Listing already works before approval; it's reading the accounts that waits on Google. The copy now says that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Tony Swartz <tswartz@gmail.com>
fix(google-ads): log Google's real error on report failures; fix picker copy
A rejected report request (the class of failure behind PR #9's prohibited field) still mapped to google_ads_upstream_unavailable. Users saw "temporarily unavailable" / "try again shortly" even though retrying the same query cannot succeed. 400s now map to google_ads_request_rejected. MCP tools get copy that retrying won't help; the dashboard card gets the same instead of the outage fallback. Google's own explanation stays in the log line from #11, never shown to users. Real outages (5xx, network) still use the unavailable bucket. Co-authored-by: Grok <grok@x.ai> Signed-off-by: Tony Swartz <tswartz@gmail.com>
fix(google-ads): stop calling a Google 400 a temporary outage
Wraps Google Ads API v25 LocalServicesLeadService.ProvideLeadFeedback so the LSA audit cron can auto-file high-confidence mismatches. There is no validate_only — every call is a live, once-per-lead survey. The tool refuses when lead_feedback_submitted is already true, probes credit_details.credit_state as a separate query (the parent field is SELECT-prohibited; the leaf may not be), and returns creditIssuanceDecision verbatim. Signed-off-by: Tony Swartz <tswartz@gmail.com> Co-authored-by: Grok <grok@x.ai>
…y-submitted re-files get_local_services_leads now returns credit_details.credit_state, lead_feedback_submitted, and the longest phone-call duration (local_services_lead_conversation.phone_call_details.call_duration_millis). The parent credit_details field stays off the SELECT (#9); the leaf is selectable on live v25. Extra fields and the conversation query fall back to null on a 400 so the list still returns. Recordings are not fetched. The performance path is unchanged. A ProvideLeadFeedback re-file that 400s RESOURCE_ALREADY_EXISTS (live 338: lead_feedback_submitted stayed false) maps to lead_feedback_already_submitted instead of "rejected report request." Signed-off-by: Tony Swartz <tswartz@gmail.com> Co-authored-by: Grok <grok@x.ai>
Author
|
Opened against the wrong remote — this is a self-host fork change for tonyswartz/open-seo, not an upstream contribution. Closing. |
ywwenjin163
pushed a commit
to ywwenjin163/open-seo
that referenced
this pull request
Sep 12, 2026
Postgres advisor flagged seq-scans and redundant indexes across both backends (D1 + Postgres): - add projects(organization_id) — org-scoped project listings seq-scanned - add account(account_id, provider_id) — better-auth sign-in lookup - add verification(expires_at) — expired-token cleanup range scan - drop saved_keyword_tag_assignments_keyword_idx — covered by unique (saved_keyword_id, tag_id) prefix - drop rank_snapshots_run_idx — covered by unique (run_id, tracking_keyword_id, device) prefix Mirrored in both schema dialects + parity-test required-index guard.
ywwenjin163
pushed a commit
to ywwenjin163/open-seo
that referenced
this pull request
Sep 12, 2026
…ges-table polish (#367) * Site audit P0: issue engine, incremental persistence, block detection Implements the P0 feature set from docs/site-audit-pm-research.md: - Issue engine: 24 issue types (shared registry with severity, explanation, how-to-fix). Per-page reporters run inside crawl steps; cross-page checks (duplicate titles/descriptions/content, broken internal links, redirect chains/loops, orphan pages) run at finalize as SQL over the persisted crawl. - New audit_links + audit_issues tables, audit_pages columns (depth, content hash, header signals, fetch class, sitemap flag); audit tables moved to src/db/audit.schema.ts. - Incremental persistence: pages/links/issues written to D1 inside each crawl-batch step with deterministic row ids + upserts (retry idempotent); slim step state; robots.txt checkpointed as step state for deterministic replay; merged progress steps keep a 10k-page crawl within the Workflows step budget. - Crawler: manual redirect handling with inline follow of normalization-equivalent redirects (slash-canonical sites), response header capture (X-Robots-Tag, Link rel=canonical), BFS depth, sitemap-last seeding, SSRF check on discovered links, honest "we were blocked" classification (403/429/cf-mitigated/challenge). - UI: Issues tab (default) with severity grouping, per-type explanations, drill-down, CSV/JSON/Sheets export, blocked banner. - MCP: run_site_audit, get_audit_status, get_audit_issues (severity- sorted, how_to_fix per issue), get_audit_pages. - Lighthouse strategies reduced to auto/none (legacy all/manual map on read); auto stays 10 URLs x 2 = 20 checks. - Self-healing: getStatus reconciles audits whose workflow instance errored/terminated without reaching mark-failed. Deploy notes: run db:migrate:prod (additive migration 0022); terminate running audits before deploying - the workflow step structure changed and in-flight instances cannot replay under the new code (a finalize guard fails them loudly instead of completing empty). * feat(onboarding): hide agent chat step; subscribe after intro steps (every-app#312) * feat(onboarding): hide agent chat step; subscribe after intro steps Remove the hosted-only strategy-chat diversion from the onboarding sequence. After the three intro questions, hosted users now hit the subscribe paywall directly, then return to the GSC and MCP connect steps. The chat route and components stay in place but unlinked, to be revisited later. Preserve the post-payment 'You're in!' interstitial by carrying checkout=success through validateSearch. * fix(onboarding): set checkout=success from subscribe route, not speculatively The previous redirect baked checkout=success into the onboarding return URL at the point needsSubscription is true — i.e. before the user had paid. It only worked because the subscribe route gates its redirect on actual access. Move the marker to the subscribe route's redirect-to-app path, where checkoutCompleted reflects a real returned-from-Stripe payment, so the 'You're in!' screen can never show pre-payment. * website: change link * fix(rank-tracking): unarchive config when re-adding an archived domain (every-app#313) * Unify dual-backend DB layer (D1 default + Postgres opt-in) (every-app#238) * D1 → Postgres data migration (ETL + runbook) (every-app#274) * Fix Postgres-only rank-tracking & site-audit workflow failures (every-app#317) * rank-tracking: raise per-project config limit from 20 to 100 (every-app#318) The cap was only a soft guard against runaway scheduled DataForSEO workload, not a hard product constraint. Bump it to 100 so projects tracking many domain/location combos aren't blocked. Co-authored-by: Claude <noreply@anthropic.com> * fix(db): add missing indexes and drop redundant ones (every-app#319) Postgres advisor flagged seq-scans and redundant indexes across both backends (D1 + Postgres): - add projects(organization_id) — org-scoped project listings seq-scanned - add account(account_id, provider_id) — better-auth sign-in lookup - add verification(expires_at) — expired-token cleanup range scan - drop saved_keyword_tag_assignments_keyword_idx — covered by unique (saved_keyword_id, tag_id) prefix - drop rank_snapshots_run_idx — covered by unique (run_id, tracking_keyword_id, device) prefix Mirrored in both schema dialects + parity-test required-index guard. * refactor(keywords): unify keyword-metric fetching behind one helper (every-app#320) * Fix production errors: onboarding crash hardening + DataForSEO spend/noise cleanup (every-app#282) * fix(ai-search): use valid Claude model_name and fail fast on unknown ones (every-app#323) DataForSEO dropped the Claude Sonnet 4.0 family from its llm_responses catalog, so model_name=claude-sonnet-4-0 was rejected with 'Invalid Field: model_name' while still billing the failed task. Point Claude at claude-sonnet-4-5 and validate every model_name against DataForSEO's accepted catalog before dispatching the paid call. * fix(mcp): 405 the standalone GET SSE stream to stop /mcp OOM (every-app#325) The stateless MCP server returns JSON on POST (enableJsonResponse) and pushes no server-initiated messages, so the optional standalone GET SSE stream serves no purpose. Left enabled, each GET holds an SSE stream open indefinitely (25s keepalive, no eventStore) and pins a fresh per-request McpServer (~5MB of tools + Zod schemas); a few dozen concurrent connected clients exceed the 128MB isolate limit. This was 100% of the /mcp exceededMemory OOMs (GET only; POST never OOMed). Return 405 (spec-compliant 'no standalone stream') before building the server, so GET allocates nothing. Also removes the bulk of the elevated GET canceled / responseStreamDisconnected outcomes. * Re-add free plan as the floor; remove subscribe gate (every-app#321) * Pin production to Postgres via committed Hyperdrive binding (#329) * Add Cloudflare Turnstile captcha on email signup (#326) * Triage production log errors: audit crash, Autumn webhook FK, PostHog capture, auth rate-limit IP, log noise (#327) * Add badseo.dev: a test site of deliberate SEO mistakes An open-source Cloudflare Worker that serves ~27 pages, each breaking one common technical-SEO rule (missing title, redirect loop, orphan page, thin content, and so on). It doubles as the end-to-end fixture for the OpenSEO site audit: every page declares the audit issues it should trigger, and scripts/run-audit.ts drives the real audit engine against a running copy to check that it does (36/36 checks, 25/25 issue types). Styled to match the OpenSEO marketing site (web/). Maintained-by-OpenSEO badge links back to openseo.so. * badseo.dev: logo in pill, footer/hover polish, SEO-optimized titles - Use the OpenSEO pine-tree logo (downscaled, base64-embedded, served at /openseo-logo.png) in a light chip inside the badge, replacing the ◎ glyph. - Footer band now fills to the bottom of the page (dropped the mismatched body padding strip) with room for the floating badge. - Index rows: remove the stray full-row underline and the stark white hover box; hover is now a soft cream tint with the name underlined. - Drop the "Maintained by OpenSEO" hero eyebrow; new H1 "A website demonstrating common technical SEO problems" and a cleaner subtitle. - Optimize homepage + catalog <title>/meta around real keywords from OpenSEO keyword research (technical seo issues KD25/vol170; technical seo checklist KD16/vol390), keeping meta lengths within limits. * Site audit P0 (1/3): issue engine, incremental persistence, block detection Server-side foundation of the P0 feature set from docs/site-audit-pm-research.md: - Issue engine: shared registry of issue types (severity, explanation, how-to-fix). Per-page reporters run inside crawl steps; cross-page checks (duplicate titles/descriptions/content, broken internal links, redirect chains/loops, orphan pages) run at finalize as SQL over the persisted crawl. - New audit_links + audit_issues tables, audit_pages columns (depth, content hash, header signals, fetch class, sitemap flag); audit tables moved to src/db/{,pg/}audit.schema.ts; migrations 0029 (D1) / 0006 (PG). - Incremental persistence: pages/links/issues written inside each crawl-batch step with deterministic row ids + upserts (retry idempotent); slim step state; robots.txt checkpointed as step state; merged progress steps keep a 10k-page crawl within the Workflows step budget. - Crawler: manual redirect handling with inline follow of normalization- equivalent redirects, response header capture (X-Robots-Tag, Link rel=canonical), BFS depth, sitemap-last seeding, SSRF check on discovered links, honest 'we were blocked' classification (403/429/cf-mitigated/ challenge). - MCP: run_site_audit, get_audit_status, get_audit_issues, get_audit_pages; limitTier resolved via shared AuditService.resolveAuditLimitTier. - Lighthouse strategies reduced to auto/none (legacy all/manual map on read). - Self-healing: getStatus reconciles audits whose workflow instance errored/ terminated without reaching mark-failed. The Issues UI and the badseo.dev e2e fixture site stack on top of this PR. Deploy notes: run db:migrate:prod (additive); terminate running audits before deploying — the workflow step structure changed and in-flight instances cannot replay under the new code (a finalize guard fails them loudly instead of completing empty). * Site audit P0 (2/3): Issues tab UI - Issues tab (new default) with severity grouping, per-type explanations and how-to-fix, drill-down to affected pages, CSV/JSON/Sheets export, and the 'we were blocked' banner when the crawl was challenged. - Tabs always render (Issues/Pages, Performance when Lighthouse ran); audit route search schema gains the issues tab and defaults to it. Stacks on claude/audit-p0-server (issue engine + persistence). * badseo.dev: render the badge logo as a white tree, no chip The silver source logo was invisible on the dark pill, so it sat in a white chip. Render it white via a CSS filter instead, so the tree fills the pill with no backing background. * badseo.dev: add build (typecheck) step before deploy - Add 'build'/'typecheck' scripts (tsc --noEmit); 'deploy' now runs the build before wrangler deploy. - Scope the tsconfig typecheck to the Worker source (src/); the e2e harness in scripts/ imports the main app and is run with tsx from the repo root. - Document the deploy flow and first-time custom-domain setup in the README. * badseo.dev: add trailing-slash redirect-cycle fixture + regression test Reproduces the 508 "Loop Detected" class of bug from every-app#61: a CMS-style page whose canonical URL ends in a trailing slash, with the non-slash form 301-redirecting to it. A crawler that strips trailing slashes turns the canonical /foo/ back into /foo, follows the 301 to /foo/, strips it again, and loops. - New fixture at /redirect/trailing-slash: the non-slash form (intercepted in index.ts on the raw path) 301s to the slash form, which is served as the canonical 200. - Harness asserts the page is crawled exactly once as a 200 with NO redirect loop, plus a dedicated "Trailing-slash cycle -> 200, no loop" guard. Verified the guard bites: temporarily disabling crawlPage's slash-canonical inline-follow makes both checks fail (redirect-loop, status 301); with it in place the harness is 38/38, 25/25 issue types. * Add webapp-testing skill (installed via /reload-skills) Vendors the anthropics/skills webapp-testing toolkit: real files under .agents/skills/webapp-testing, a symlink from .claude/skills/, and skills-lock.json pinning the source + hash. Matches how the other project skills are tracked. * Site audit: redesign issues tab as grouped table + calmer page header - Issues: single bordered table with severity sections (Critical/Warning/Info headers carry the counts), dot indicators instead of filled pills, plain right-aligned page counts, all rows collapsed by default; expanded rows get a severity-colored left rule - Removed the dead severity-count chips (they looked like filters but were inert spans) - Header: audited hostname is now the H1 with the status badge inline - Blocked banner: compact tinted panel instead of a full-size alert - Stats: hairline strip instead of four separate cards; issues stat shows a severity breakdown, Lighthouse tile hidden when no tests ran, dropped the orange issues-count coloring * audit: fix trailing-slash redirect cycle at the root (preserve slashes) Replaces the crawlPage inline-follow workaround with the root-cause fix, so we don't carry two fixes for the same bug (every-app#61). - normalizeUrl: stop stripping trailing slashes. A trailing slash is the canonical form on most CMSes, which 301 the non-slash version to it. Stripping rewrote the canonical URL into its own redirect source and looped (508). Now /path and /path/ are distinct and the redirect resolves normally. - crawlPage: remove the isSelfAfterNormalization inline-follow (+ now-unused resolveRawUrl). With slashes preserved it's dead code; a trailing-slash redirect is recorded as an ordinary hop. - add canonicalUrlKey (www/http/https-tolerant) and use it for the Lighthouse homepage match, which had the same redirect-mismatch vulnerability. - tests: preserve-trailing-slash + canonicalUrlKey unit tests; badseo harness guard is now fix-agnostic (canonical resolves to 200, no loop/error). Verified: 36 audit unit tests pass, tsc clean, badseo e2e 38/38. Reintroducing stripping makes the trailing-slash guard fail (redirect-loop), confirming the regression guard bites. * Audit: add no-outgoing-links + meta-description-too-short checks, catch empty H1s Two checks Ahrefs covers that we didn't, plus a fix: <h1></h1> now counts as missing. badseo.dev gains fixtures for all three (41 checks, 27/27 issue types covered). * Audit pages table: honest redirect/non-HTML rows, wrapped titles - 3xx rows show their redirect target (dim →) instead of a red 'missing' title, and dash out H1/Words/Images since nothing was analyzed - red 'missing' only when the engine actually flagged missing-title, so 200 non-HTML files (security.txt) read as blank, not broken - URL cells include the host when it differs from the audited site's, so apex→www redirect sources no longer render identically to their target - titles wrap to two lines (line-clamp) in a wider column instead of truncating at 220px; PagesTable moved to its own file (lint max-lines) * Audit pages table: canonical-host display, URL default sort, full title wrap - host prefix now compares against the site's predominant 2xx host, not the typed start URL — auditing apex 12port.com no longer prefixes every www row with the host - default sort by URL so the table opens as a site inventory instead of leading with redirects on error-free sites - titles wrap fully instead of clamping at two lines; long titles are the thing being audited, so their tails shouldn't be hidden * ci: exclude vendored skills from prettier; format test file --------- Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Tony asked for the remaining LSA improvements after 338 filed
FAIL_NOT_ELIGIBLE: matcher training on both survey halves, credit-eligible reasons only when true,credit_stateon the leads tool + weekly digest, and louder charged+unverifiable (conversation duration if Google will give it). Live 338 also showedlead_feedback_submittedstays false after a successful file; Google's real once-per-lead stop isRESOURCE_ALREADY_EXISTS.What
get_local_services_leadsnow returns:creditState(local_services_lead.credit_details.credit_state— the leaf is selectable on live v25; the parent stays off the SELECT per Dead/Broken Link Checker (All Metadata) #9)leadFeedbackSubmittedconversationDurationMillis(longest phone-call duration; does not fetchcall_recording_url)Extra SELECT fields and the conversation query fall back to null on a 400 so the lead list still returns. The performance/dashboard path is unchanged.
provide_lead_feedbackmaps a re-fileRESOURCE_ALREADY_EXISTS400 tolead_feedback_already_submittedinstead of "rejected report request."Cron prompts (outside this repo, already updated on disk):
VERY_SATISFIED/SERVICE_RELATED(orBOOKED_CUSTOMER) on high-confidence charged matchesDUPLICATEFAIL_NOT_ELIGIBLEcreditState(Google's ~30-day auto-review)Tests
At
3e6661ff68425745f65583983ed4d0f4d2c30128:tsc --noEmitclean,oxlint . --type-awareclean,vitest run143 files / 1216 tests.