Feat/bing ai citations import - #191
Open
mlava wants to merge 133 commits into
Open
Conversation
cf-selfhost-deploy.sh writes cf-resources.json as a single line of JSON containing the operator's own Cloudflare resource IDs. It was committed in f3d6374, so `prettier --check .` failed on a clean tree — and since ci:check chains with &&, that stopped knip, tsc, and oxlint from running at all. Formatting the file would only hold until the next deploy rewrote it, and each contributor's copy wants their own account's IDs, so it is ignored rather than reformatted — the same treatment .env already gets. Local copies are untouched; deploys keep working. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix: stop tracking cf-resources.json so ci:check passes
Adds specs/0009 recording the Bing Webmaster Tools integration design, verified against the live API rather than the docs. Two unknowns blocked the design and were settled by scripts/bing-oauth-spike.ts: - Refresh tokens: public reports describe Bing rotating refresh tokens and then rejecting the rotated ones, which would be fatal here since Better Auth overwrites the stored token with whatever a provider returns. Not reproduced — Bing returns no refresh_token on refresh at all, so the original is preserved and survives reuse. genericOAuth is safe. - Account identity: Bing has no userinfo endpoint and issues no id_token, but the access token is base64url JSON carrying webmasteruid and webmasteremail. src/shared/bing.ts decodes it, so getUserInfo needs no network call. The spike script is retained: the refresh finding rests on a short observation window and specs/0009 calls for re-checking it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 1 of specs/0009: the three independent layers, built in parallel isolated worktrees and integrated here. - bing_connections across both dialects (SQLite/D1 + Postgres) with generated migrations, mirroring gsc_connections. - createBingClient: mints tokens via Better Auth, unwraps Bing's WCF `d` envelope, parses /Date(ms)/ values, and maps 401/403/404/429 to typed errors. Reads are free — no credit metering, same as GSC. - bingProviderConfig for genericOAuth with explicit endpoints (Bing has no discovery document) and a getUserInfo that decodes the access token instead of calling a userinfo endpoint that does not exist. - Encrypted-at-rest API key helpers for self-hosters, who cannot complete an OAuth flow locally because Bing rejects localhost redirect URIs. Two integration points were kept out of worker ownership and wired by hand: registering the provider in auth-config, and exporting the new table from the D1 barrel that drizzle-kit and the D1 client both read. GetRankAndTrafficStats rows are surfaced as-is: only GetUserSites was verified against the live API, so its field names are not yet pinned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 2a of specs/0009. Mirrors the GSC layering so the two integrations read the same way. BingConnectionRepository upserts on project_id, so re-selecting a site replaces the mapping rather than adding a second, and coalesces connected_account_email so a null never clobbers a stored value. BingService gates site selection on Bing's isVerified boolean (Bing has no permissionLevel string), and keeps GSC's disconnect semantics: the OAuth grant is unlinked only when the disconnector is the connector and no other project still uses that account. A dead grant surfaces as requiresReconnect rather than throwing, so one revoked connection cannot break the whole site list. Deliberately out of scope, both flagged rather than stubbed: API-key connections (the client is OAuth-only, so those rows raise instead of silently failing) and any reshaping of GetRankAndTrafficStats rows, whose field names are still unverified against the live API. Reads stay free — no credit metering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
BingService wrote connectedAccountEmail: null because the client had no way to reach it — Bing publishes no userinfo endpoint, which is what GSC calls for the equivalent. But the email is already a claim on the access token, so createBingClient.getConnectedEmail() decodes it with no extra network call. setSite and the grant listing now surface it, both best-effort: a missing or undecodable claim yields null rather than failing the connection or hiding the whole account list. Round 3's UI needs this for the "connected by" line. Splits BingService.test.ts, which crossed the 400-line lint cap once these cases were added; disconnect and grant-failure coverage now lives alongside it in BingService.disconnect.test.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 2b of specs/0009 — the layer that makes the client reachable. Server functions mirror gsc.ts: grant status, connection status, site listing, select, and disconnect, all project-scoped through requireProjectContext. Site selection gates on Bing's isVerified boolean rather than GSC's permissionLevel string. The MCP tool derives its table columns from the keys actually present on the returned rows instead of hard-coding them, because Bing's GetRankAndTrafficStats field names are still unverified — inventing Clicks/Impressions would produce a table that silently renders nothing. A missing connection or a revoked grant returns an actionable connect or reconnect message rather than surfacing as a fault, matching how the Search Console tools behave. Reads stay free — no credit metering. Registration in mcp/server.ts is wired here rather than by the worker, so no lane could collide on the shared tool registry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Live call against a verified site pins what GetRankAndTrafficStats actually
returns: one row per day carrying Date, Clicks, and Impressions, plus the
__type marker every WCF payload has. The date arrives as
/Date(1781852400000-0700)/ — the offset is informational, the milliseconds
are already UTC, which the existing parser handles.
Rows are now typed and mapped to { date, clicks, impressions } instead of
passed through as Record<string, unknown>. The MCP tool needs no change: it
derives columns from the keys present, which are now meaningful names rather
than Bing's PascalCase.
Adds a --step=call probe to the spike script, which is how the shape was
captured and how the next endpoint's shape should be.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The refresh behaviour now rests on two independent grants rather than one ten-minute window: no refresh_token returned on refresh, original reusable, observed before and after regenerating the OAuth client secret. The multi-day expiry complaint in the public reports remains untested and is called out as such. webmasteruid also survived the credential regeneration, confirming it keys the Bing account rather than the grant or the OAuth client. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round 3 of specs/0009 — the user-facing surface.
Bing gets its own page rather than a source toggle on Search Performance.
GetRankAndTrafficStats accepts no date range, no device or country filter,
and no paging, so sharing that page's chrome would advertise controls Bing
cannot honour. The page shows clicks/impressions totals and the daily rows,
and falls back to the connection card when the project isn't connected.
Adds getBingPerformance, which round 2b deliberately left out: not-connected
and dead-grant both resolve to { connected: false } so the page renders the
connect card instead of an error boundary.
The connect card mirrors the Search Console one, with two differences that
follow from Bing's API: sites are gated on isVerified rather than a
permission-level string, and an unconfigured deployment gets an explanatory
setup notice instead of a button, because Bing rejects localhost redirect
URIs and allows one redirect URI per client.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both of these passed while proving less than they claimed. schema-parity.test.ts never imported the bing schemas, so the D1/Postgres mirror guard silently skipped the new table — the 120 green cases covered only pre-existing schemas. Verified the fix bites by deleting a column from the Postgres mirror and confirming a precise failure. The getConnectedEmail suite sat outside the describe that owns the fetch stub, so "makes no network call" could not detect a network call: an added fetch would hit the real global and leave the mock uncalled. Moved it into the stubbed scope and verified by sneaking a fetch into the client, which now fails the test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…secret Typing the rank/traffic rows against the live API left stale claims behind in four places. The MCP tool still derived table columns dynamically to absorb variability that no longer exists, its model-facing description said the row shape "varies by site", and its empty state told callers to try a different date range — for an endpoint that accepts no date range at all. Columns are now fixed like every other MCP tool, and two tests that asserted the dynamic behaviour are replaced by ones asserting the real shape, including that an unparseable date renders as unknown rather than invented. Also stops logging grant.accountId on an unexpected fault. That value is the webmasteruid, which doubles as Bing's site verification code, so the inherited GSC logging line put a secret into Worker logs and Sentry — a lower-trust store than the database. Logs the Better Auth row id instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…code Three reviewers flagged the API-key lane as scaffolding. Verification agreed in part: the two crypto helpers had no caller and no column to write to, and were two one-line wrappers over better-auth's symmetricEncrypt that will be rewritten when the real storage lands — so they go. The auth_mode column stays. Removing it means regenerating the head migration and snapshot in both dialects, then adding a third migration to restore it when the lane lands; a NOT NULL discriminator is the right shape for a two-mode connection. The getPerformance guard stays too — authMode is read back from the database as a union, and refusing an unhandled member at a trust boundary beats silently building an OAuth client for a non-OAuth row. The real defect was the spec claiming API-key mode "is also supported" with the key "stored encrypted at rest in a dedicated column" — no such column exists. It now says deferred, and three other overclaims are corrected: the surface described top queries, top pages and crawl issues that v1 does not build; the MCP section promised a crawl-issues tool that does not exist; and the Context still called the refresh finding ten minutes of observation after Consequences recorded the replication. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reviewers flagged two duplications between the Bing and Search Console cards. Verification rejected the larger one and confirmed the smaller. REJECTED — merging BingSitePicker into SitePicker. The claimed nine-line diff is really 19 hunks, and "(no access)" vs "(not verified)" mean opposite things to a user: one is a permission problem, the other an action to take in Bing Webmaster Tools. A merged component needs a glyph plus ten label strings, which reads worse than two clear files. SitePicker is also consumed by shipped GSC onboarding, so refactoring it to land a Bing feature risks an existing activation flow for no user-visible benefit. APPLIED — the card chrome. StatusPill was byte-identical, IntegrationCard differed by its title and ConnectedState by a glyph and one label: three props, no configuration bag. Both cards lose ~107 lines. The card bodies stay separate, since their connect flows, setup warnings, and invalidation sets genuinely differ — Bing has no dashboard cache to invalidate because the card only appears in project settings. Also removes getBingGrantStatus and the GRANT_STATUS_KEY invalidations. No query ever registered that key, so every invalidation of it was a no-op; the GSC equivalent exists because onboarding and the re-engagement modal read it, and Bing has neither. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clicking Connect on a cloudflare_access deployment failed with "Could not
start Bing sign-in". Better Auth's oauth2.link needs a Better Auth session,
which cloudflare_access and local_noauth do not have — Search Console solves
this with a hand-rolled flow and Bing had no equivalent, so the card offered
a button that could not work in the mode this repo defaults to.
Adds the Bing counterpart of gsc/selfHostedOAuth.ts: HMAC-signed state, code
exchange, and an encrypted account row written exactly the way Better Auth's
setTokenUtil would, so getAccessToken reads it back unchanged. The one
Bing-specific difference is identity — no id_token and no userinfo endpoint,
so the account id comes from the access token's webmasteruid claim.
The redirect URI is {origin}/api/bing/oauth/callback. Bing permits one per
registered client and rejects localhost, so each deployment needs its own
client and a localhost dev server still cannot complete the flow.
Five review axes and two verification passes missed this because all of them
read code; it only surfaced when the button was actually clicked.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ationale Verified end to end on a cloudflare_access deployment: connect, site selection, and daily rows all work. The spec justified API-key mode with "a self-hoster cannot complete an OAuth flow" — that is now only true of localhost, since Bing refuses localhost redirect URIs but is perfectly happy with a self-hosted deployment on a real domain. The remaining use for the API-key lane is local development without a public tunnel. The setup notice now names BETTER_AUTH_SECRET, which gates the flow and whose absence shows up as "Setup required" with no explanation, and gives the exact callback path to register. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This handles token exchange, at-rest encryption, signed state, and an open-redirect guard, and had no coverage — the Search Console original it mirrors has none either, so this is new ground rather than parity. Eleven cases, built around driving the real authorize step to obtain a genuinely signed state so the callback tests exercise verification rather than a hand-made string. Covers: tampered state rejected, state issued for another user refused, an off-origin callbackURL collapsing to "/" instead of becoming an open redirect, denied consent returning without exchanging anything, the grant being keyed by webmasteruid, a stored refresh token surviving a re-link that returns none (Bing never returns one), and both failure paths — a rejected code exchange and an unreadable access token — storing nothing. The two security assertions were mutation-tested: removing the HMAC check and the same-origin guard each fails exactly the test that covers it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Live MCP output showed the offset moving between 08:00Z and 07:00Z across March — these are midnight in Bing's own reporting timezone (US Pacific, shifting with daylight saving), not instants. Formatting them in the viewer's timezone labels every row a day early for anyone at UTC-9 or further west; Honolulu and Anchorage render a 9 Feb bucket as 8 Feb. The helper moves to its own module so it can be tested without dragging the page's server-function imports into the test's module graph, and reads the day in UTC. Mutation-tested: dropping the UTC pin fails the tests under TZ=America/Anchorage. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Probed live so the next person does not have to. Both endpoints share one row shape carrying AvgImpressionPosition, so average position and a striking-distance view are reachable — but the rows are sampled at roughly 16 dates across five months, which makes query-level trends lumpy and a 28-day slice potentially empty. Also records that the traffic window varies by site and that its Date is a Pacific day bucket rather than an instant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Issues are disabled on the fork, so the follow-up lands where specs/0008 already puts this kind of note. Captures what the probed query/page endpoints make possible — striking distance first — and what still cannot be built honestly: no device or country dimension exists, and sampled query rows make a date-range control promise precision the data lacks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bing Webmaster Tools integration
…position tiles, get_bing_queries MCP tool Builds the spec's Future directions (specs/0009) on the sampled GetQueryStats/GetPageStats endpoints probed live on 2026-07-25: - bingClient: getQueryStats/getPageStats sharing one probed row schema - bingQueryReport: whole-window aggregation (impression-weighted position, AvgClickPosition ignored) and the 5-20 striking-distance band - BingService.getQueryReport + getBingQueryReport server fn - UI: CTR and avg-position tiles, half-split period deltas from the dense daily series, tabs for striking distance / queries / pages / daily, copy/save-as-keywords on the striking table - MCP: get_bing_queries (dimension, strikingDistanceOnly, limit) No date picker or device/country filters — Bing's API has none, and the query/page rows are sampled (~16 dates over ~5 months), so only whole-window aggregates are honest. No DB changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(bing): v2 panels — striking distance, queries/pages tables, tiles, get_bing_queries
Bing pads its daily window back before a site had any presence; those leading all-zero rows gutted the previous half and printed +6950%-style deltas on the tiles. Trim leading zero days before splitting, and suppress the delta entirely when the previous half carries under 1% of the current half's impressions — a ratio against noise is not information. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(bing): honest delta baseline for the half-split comparison
GetPageQueryStats probed live 2026-07-25: takes siteUrl plus a `page` query parameter (that exact name — the docs disagree with themselves) and returns the same sampled QueryStats row shape filtered to one page; an unknown page returns 0 rows. Recorded in specs/0009. - bingClient.getPageQueryStats + `--page`/`--pageParam` passthrough on the spike script's call step - BingService.getPageQueries + getBingPageQueries server fn, aggregated with the existing whole-window math - Pages tab: per-row "Queries →" action opens the queries driving that page, with a back button; drill-down resets on tab switch - get_bing_queries MCP tool: optional pageUrl input for the same drill-down (composes with strikingDistanceOnly) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
GetCrawlStats (dense daily series), GetCrawlIssues (0 rows on a healthy site — shape unpinned), GetLinkCounts and GetUrlLinks (param is `link`; single-object responses, inner shapes unpinned on a site with no inbound links). Probed 2026-07-25 against scholar-sidekick.com. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… in the legacy API Re-probed GetLinkCounts/GetUrlLinks with the integer page pager against a site whose portal shows 27 referring domains / 85 referring pages: still 200 + empty. Matches an unresolved Microsoft Q&A report. Recorded so nobody builds on them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(revenue): Revenue page with RapidAPI subscriptions and Stripe metrics
Verified live 2026-08-03 against a real hub: - Subscription status vocabulary is ACTIVE/DELETED (not CANCELED), and DELETED can appear with canceledAt null (plan switches) — only an explicit ACTIVE now counts as an active subscriber. - The subscriptions query silently ignores an unrecognized where.apiId and returns the caller's own subscriptions instead of erroring. Nodes now carry api.id and are scoped to the requested API, and setApi rejects an id that matches none of the returned nodes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(revenue): harden RapidAPI client against live Platform API behavior
Newer Stripe accounts have no account-default API version and reject
requests without a Stripe-Version header ('You did not provide an API
version', verified live 2026-08-03). Pin the current stable release.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(revenue): pin Stripe-Version header on API requests
Org-level keys reject requests that don't name a target account
('Please include the Stripe-Context header', verified live 2026-08-03).
Optional STRIPE_CONTEXT env passes the acct_ id through; account-level
keys keep working with it unset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two projects can use Stripe on different accounts under one org key: the connection row stores the target account id (an identifier, not a secret), the client sends it as Stripe-Context, and the connection card gains an account field. Replaces the instance-level STRIPE_CONTEXT env added earlier on this branch — wrong granularity. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(revenue): per-project Stripe account for organization keys
A retired one-off product's purchase history is still worth tracking (verified live: the real one-off product is archived). Archived products sort last and are labeled in the dropdown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(revenue): include archived Stripe products in the picker
…ucts Refunds live on PaymentIntents, not Checkout Sessions, so refunds from /v1/refunds are attributed to the one-off product via the session's payment_intent — with a capped per-refund session lookup for refunds whose purchase predates the 60-day sessions window. The one-off row gains Refunds and Net revenue tiles; a key without the Refunds read grant degrades to the old gross-only tiles with a hint instead of failing the panel. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(revenue): Stripe refunds and net revenue tiles
Pin the dashboard grid to fixed positions - Search performance | Bing performance on the first row, Revenue | Site audit on the second, with Backlink pulse below - replacing the data-first sort that reshuffled cards as data arrived. The MCP pitch still leads during onboarding. The new Bing card reuses getBingPerformance (shared query key with the Bing page) and shows clicks, impressions and CTR with half-vs-half deltas; avg position is omitted since it needs the slow sampled-stats call. The new Revenue card reuses getStripeRevenue and shows subscribers, est. MRR, purchases and net revenue (falling back to gross when the key lacks Refunds read access). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(dashboard): Bing and Revenue cards in a fixed 2x2 layout
…lookups The dashboard card and the Backlinks page hit the same DataForSEO summary endpoint but cached independently (24h visit-triggered snapshot vs 6h overview cache), so the card could lag the page by a day whenever the live index moved. Backlinks overview lookups - web page and get_backlinks_overview MCP tool - now record the summary as a dashboard snapshot when the target is the project's own domain. Zero extra DataForSEO spend: the overview response is reused, and a snapshot recorded here also satisfies the dashboard's daily freshness check, saving that path's metered call. Cache-served overviews keep their original fetchedAt, and a newer-than-latest guard drops writes that would move the card backwards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(dashboard): sync backlink snapshot from Backlinks-page overview lookups
DataForSEO's backlinks summary endpoint never returns new/lost counts, so the card's New links / Lost links tiles have shown em-dashes since they shipped. Deltas now come from diffing the latest snapshot against a previous-day baseline (the newest snapshot at least 20h older, scanned over recent rows since overview-synced snapshots can add several per day) - free, no extra DataForSEO calls. The card shows the deltas as percent subs under Ref. domains and Backlinks (matching the GSC and Bing cards) and fills the freed row with Rank and Broken links, which every snapshot already stores. The stamp names the baseline day. The always-null new/lost insert fields are dropped; the DB columns stay (nullable, no migration needed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(dashboard): replace dead New/Lost tiles with snapshot-diff deltas
The Bing tiles totalled Bing's entire reported window (~6 months) and compared its newer half against its older half, so a growing site showed four-digit deltas against its own near-zero past. The GSC surfaces frame everything as last 28 days vs the prior 28. last28DayReport replaces splitDailySeries: it cuts fixed 28-day windows from the daily rows Bing sent, anchored on the newest reported day (the endpoint accepts no date range, so the windows are client-side). Rows are day buckets stamped at midnight US Pacific; window membership uses rounded day-distance so the DST hour shift can't move a day across a window boundary. The delta is omitted when Bing's window doesn't span the full prior 28 days rather than comparing against a partial one. Applied to both the Bing performance page tiles (the daily tab still shows the full window) and the dashboard card, whose stamp now reads "last 28 days" like the GSC card. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(bing): last 28 days vs prior 28 tiles, matching the GSC report
get_backlinks_overview started calling waitUntil for its snapshot sync (7857308) but the tool-text-output mock only exported env, failing the suite on main. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RapidAPI support confirmed (2026-08-04) there are no platform APIs for public-marketplace Hub data — the subscriptions query is Enterprise Hub only, and the accidentally-public test endpoint we had verified against was retired to private. Subscriber numbers exist solely in Studio Analytics dashboards. The panel becomes manual snapshots: a rapidapi_snapshots table (one row per project per day, re-logging replaces), a log form + latest-vs- previous tiles + history on the Revenue page, and a get_rapidapi_snapshots MCP tool. The GraphQL client, connection card/settings section, and RAPIDAPI_* env vars are removed; rapidapi_connections is dropped. RevenuePage's shared tiles move to revenueParts.tsx to stay under the file-size lint budget. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(revenue): replace RapidAPI live queries with manual snapshots
Revenue moves out of the two-column grid to span the page width at the top, with its four tiles sharing one row on large screens. The grid below keeps fixed pairs: GSC | Bing, then Backlinks | Site audit. The MCP pitch still leads the grid during onboarding. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RapidAPI marketplace subscriptions are recurring revenue, but the snapshots only stored subscriber counts, so nothing could be turned into dollars. Snapshots now take an optional monthly plan price (USD cents - RapidAPI bills in USD only), and the report derives est. gross MRR (paying x price) and est. net MRR after RapidAPI's flat 25% marketplace fee, both from the latest snapshot only. The panel gains a "Plan price $/mo" form field (prefilled from the latest snapshot since the price rarely changes, kept after logging), two MRR tiles with the working shown as hints, and a Plan price history column. formatMoney takes optional fraction digits so $5.99-style plan prices don't round to whole dollars. Additive nullable column on both SQLite and Postgres; migrations generated and applied locally. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(dashboard): full-width revenue card above the paired cards
feat(revenue): plan price on RapidAPI snapshots with est. MRR tiles
The dashboard Est. MRR tile showed Stripe only. It now also queries the RapidAPI snapshots (shared query key with the Revenue page) and combines the sources: Stripe MRR + RapidAPI net MRR summed when Stripe bills in USD (RapidAPI is USD-only), with the breakdown as a sub note; a non-USD Stripe MRR keeps its own value and notes the RapidAPI figure alongside rather than summing across currencies. A project with RapidAPI MRR but no Stripe connection now shows the tile instead of the Stripe pitch, and a RapidAPI fetch failure only costs its contribution. The stamp reads "Stripe + RapidAPI" when both count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(dashboard): fold RapidAPI net MRR into the revenue card's Est. MRR
Bing Webmaster Tools' AI performance report (Copilot/AI-answer citations) has no API, only CSV exports. Adds a manual-import path mirroring the RapidAPI snapshot pattern: a daily Overview upserted per (project, date), and dated Pages/Queries snapshots (no per-row date, so history is kept across uploads) with a new "AI performance" tab on the Bing page and a get_bing_ai_citations MCP tool. See specs/0015. Co-Authored-By: Claude Sonnet 5 <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.
No description provided.