Skip to content

fix(catalog): the report tab stops dead-ending cross-account and anonymous (chat#1912 row 1) - #1915

Merged
sweetmantech merged 2 commits into
mainfrom
fix/catalog-report-dead-ends
Jul 30, 2026
Merged

fix(catalog): the report tab stops dead-ending cross-account and anonymous (chat#1912 row 1)#1915
sweetmantech merged 2 commits into
mainfrom
fix/catalog-report-dead-ends

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Closes row 1 of chat#1912.

Why

Measurements are account-scoped; the catalog is not. So GET /api/catalogs/{id}/measurements returns 404 in two very different situations, and the report tab collapsed both into one message:

No valuation found for this catalog. This catalog has no measured play counts linked to your account yet. Run a valuation from recoupable.dev, or add songs in the Manage songs tab and ask Recoup to measure them.

That is wrong in both directions:

  • For the owner mid-measurement it reads as "nothing here", so the customer re-runs the whole valuation. Reproduced on prod 2026-07-30; it is exactly what a live customer did on 07-27 (two playcount_snapshots runs, 209s apart).
  • For a different account it claims no valuation exists when one does, and the only next step throws a signed-in customer back to the marketing site at the moment of peak interest.

A third state was wrong too: anonymous visitors got "Something went wrong loading the measurements", because useCatalogMeasurements is gated on authenticated and the query never runs. A disabled query is not a failure.

What changed

  • lib/catalog/getCatalogReportState.ts — pure decision returning loading | signed-out | measuring | other-account | error | ready. The auth check deliberately precedes the error checks, since a disabled query leaves neither data nor error behind.
  • lib/catalog/getCatalogReportEmptyCopy.ts — copy per state, kept out of the component so the wording is testable.
  • hooks/useOwnsCatalog.ts — resolves ownership from the viewer's own catalog list, which is what separates "measuring" from "other-account". Exposes isResolved so a still-loading list is never mistaken for non-ownership.
  • CatalogReportEmptyState replaces CatalogReportError (deleted, no other callers).
  • While measuring, the report polls every 5s and resolves on its own. This is the part that removes the reason to re-run.

New copy:

State Says
signed-out "Sign in to see this report" + a Sign in button (opens Privy in place)
measuring "Measuring your catalog" + spinner, and explicitly "There is no need to run the valuation again."
other-account "This catalog was measured by another account" + "Go to your catalogs" → /catalogs
error unchanged wording, still points at the Manage songs tab

Deliberately not in this PR

The songs/report access mismatch. GET /api/catalogs/songs returns 200 unauthenticated, so the Manage songs tab serves a stranger all 24 tracks and ISRCs on the same page that withholds the valuation. The issue's Works-when step 4 covers it. Closing that is an api change and a product decision about whether catalogs are public, so it is not bundled into a chat-side copy-and-state fix. Row 1's other-account copy is written to be honest either way ("The songs are listed under Manage songs, but the play counts and valuation belong to the account that measured them"). Flagging explicitly so this does not merge as if the access model were settled.

Tests

TDD red→green (both RED as module-not-found first):

  • lib/catalog/__tests__/getCatalogReportState.test.ts — 8 cases, one per branch, including the anonymous no-error case and 500-vs-404.
  • lib/catalog/__tests__/getCatalogReportEmptyCopy.test.ts — 16 cases, including a guard that no state's copy or CTA contains recoupable.dev (the architecture decision recorded on the issue) and a house-style em-dash guard.

Full suite 306 passed / 77 files, tsc --noEmit clean.

Verification

Preview verification against the issue's four-step Works when script will be posted as a PR comment once the preview builds. Note it needs the api test branch synced with main first, since chat previews resolve to test-recoup-api.

🤖 Generated with Claude Code


Summary by cubic

Fixes the Catalog report tab so anonymous and cross‑account viewers no longer dead‑end, and owners mid‑measurement see clear progress with auto‑refresh. Refactors state and polling into composed hooks and addresses review feedback (chat#1912 row 1, chat#1915).

  • Bug Fixes

    • Added getCatalogReportState to clearly handle: loading, signed‑out, measuring, other‑account, error, ready; signed‑out no longer reads as an error and shows “Sign in” immediately.
    • While measuring, auto‑polls every 5s until ready to avoid duplicate valuations.
    • Missing‑measurement check keys off HTTP 404 status prefix (not substrings); unresolved ownership renders an error instead of guessing “other‑account”.
  • Refactors

    • Moved report data/state/polling into useCatalogReport and useMeasuringPoll, keeping CatalogReportContent presentational.
    • Introduced useOwnsCatalog with ownershipUnknown; moved empty‑state copy to getCatalogReportEmptyCopy.
    • Replaced CatalogReportError with CatalogReportEmptyState.
    • Added tests for state, copy, and ownership resolution.

Written for commit 0bb24e0. Summary will update on new commits.

Review in cubic

…ymous (chat#1912 row 1)

Measurements are account-scoped while the catalog is not, so the api returns
404 both when the owner's measurement has not landed yet and when someone
else measured the catalog. Both collapsed into "No valuation found for this
catalog ... Run a valuation from recoupable.dev", which sent a signed-in
customer back to the marketing site to re-run a valuation already in flight.
Anonymous visitors got "Something went wrong loading the measurements",
because the auth-gated query never runs and a disabled query read as failure.

Adds getCatalogReportState (loading | signed-out | measuring | other-account |
error | ready) and getCatalogReportEmptyCopy, so each case tells the truth and
keeps the customer in the app. While measuring, the report polls every 5s and
resolves on its own instead of leaving an empty page.

Replaces CatalogReportError with CatalogReportEmptyState.

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

cursor Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview Jul 30, 2026 5:25pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 36568226-0662-41a5-9929-3c1ec3ba0377

📥 Commits

Reviewing files that changed from the base of the PR and between 9fcd2aa and 0bb24e0.

⛔ Files ignored due to path filters (3)
  • hooks/__tests__/useOwnsCatalog.test.tsx is excluded by !**/*.test.* and included by hooks/**
  • lib/catalog/__tests__/getCatalogReportEmptyCopy.test.ts is excluded by !**/*.test.* and included by lib/**
  • lib/catalog/__tests__/getCatalogReportState.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (8)
  • components/Catalog/report/CatalogReportContent.tsx
  • components/Catalog/report/CatalogReportEmptyState.tsx
  • components/Catalog/report/CatalogReportError.tsx
  • hooks/useCatalogReport.ts
  • hooks/useMeasuringPoll.ts
  • hooks/useOwnsCatalog.ts
  • lib/catalog/getCatalogReportEmptyCopy.ts
  • lib/catalog/getCatalogReportState.ts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e293be08d2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread hooks/useOwnsCatalog.ts Outdated
Comment on lines +15 to +16
ownsCatalog: !!catalogId && !!data?.catalogs?.some((c) => c.id === catalogId),
isResolved: !isLoading,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep ownership unresolved until catalogs are known

Because isResolved is only !isLoading, every non-loading catalog-query state is treated as a definitive ownership answer, including when useCatalogs is disabled before userData.account_id exists, has errored, or is serving a still-fresh cached list from before an out-of-band catalog claim. In those cases a signed-in owner whose measurements endpoint returns the expected 404 for a freshly seeded catalog is passed through as ownsCatalog: false, so the report shows the cross-account copy and skips the 5s measuring poll until/unless the catalog list later refreshes. Keep this unresolved until the catalogs read has actually succeeded with current data, or surface an error instead of using absence from an unresolved list as proof of non-ownership.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread hooks/useOwnsCatalog.ts Outdated
Comment thread lib/catalog/getCatalogReportState.ts Outdated
Comment thread components/Catalog/report/CatalogReportContent.tsx Outdated
Comment on lines +59 to +66
// The owner's measurement is still running, so pull again instead of leaving
// them on an empty report to re-run the whole valuation (chat#1912 row 1).
const { refetch } = measurementsQuery;
useEffect(() => {
if (state !== "measuring") return;
const id = setInterval(() => refetch(), MEASURING_POLL_MS);
return () => clearInterval(id);
}, [state, refetch]);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OCP

  • actual: net new hooks added to existing component
  • required: put useEffects in a hook like useOwnsCatalog.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0bb24e0d.

The component no longer owns any of it. Composition instead:

  • hooks/useCatalogReport.ts composes the measurements, songs, auth and ownership reads and derives the single report state.
  • hooks/useMeasuringPoll.ts owns the while-measuring interval, so the useEffect lives in a hook rather than in the component.
  • CatalogReportContent is presentational again: it calls one hook and renders what the state describes. Its only remaining hook is the pre-existing useMemo for the release rollups.

Net effect on the component: usePrivy, useCatalogMeasurements, useCatalogReportSongs, useOwnsCatalog, getCatalogReportState and the polling effect all left it, and one useCatalogReport call replaced them.

…(chat#1915 review)

Review: net new hooks and a useEffect had been added straight into
CatalogReportContent. Extends by composition instead (OCP), so the component
is presentational again.

- useCatalogReport composes measurements, songs, auth and ownership, and
  derives the single report state
- useMeasuringPoll owns the while-measuring interval
- CatalogReportContent renders what the state describes

Also fixes three review findings:
- useOwnsCatalog derives resolution from isSuccess/isError, never !isLoading.
  useCatalogs is enabled: !!accountId && authenticated, and a disabled
  TanStack v5 query reports isPending true / isFetching false, so !isLoading
  was already true while the account resolved and told a signed-in owner their
  own catalog was measured by another account.
- a failed catalog list now reports ownershipUnknown and renders the error
  state rather than guessing "other-account"
- the missing-measurement check matches the HTTP 404 status prefix instead of
  any "404" substring, so a 500 whose body mentions 404 stays an error
- signed-out visitors no longer wait on the songs read before the sign-in CTA

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 7 files (changes from recent commits).

Confidence score: 4/5

  • In hooks/useCatalogReport.ts, a failed catalog-songs request can still produce a “ready” report with an empty releases table, which hides a real data-fetch failure and can mislead users into trusting incomplete results; include songsQuery.error in the derived error state so the report shows its error path instead.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="hooks/useCatalogReport.ts">

<violation number="1" location="hooks/useCatalogReport.ts:37">
P2: A catalog-songs request failure still renders a ready report with an empty releases table, rather than the report error state. Include `songsQuery.error` when deriving `error` so failed rollup data is surfaced instead of presented as no releases.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread hooks/useCatalogReport.ts
? measurementsQuery.isLoading || songsQuery.isLoading || !isResolved
: false,
hasMeasurements: !!measurementsQuery.data,
error: measurementsQuery.error,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A catalog-songs request failure still renders a ready report with an empty releases table, rather than the report error state. Include songsQuery.error when deriving error so failed rollup data is surfaced instead of presented as no releases.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useCatalogReport.ts, line 37:

<comment>A catalog-songs request failure still renders a ready report with an empty releases table, rather than the report error state. Include `songsQuery.error` when deriving `error` so failed rollup data is surfaced instead of presented as no releases.</comment>

<file context>
@@ -0,0 +1,52 @@
+      ? measurementsQuery.isLoading || songsQuery.isLoading || !isResolved
+      : false,
+    hasMeasurements: !!measurementsQuery.data,
+    error: measurementsQuery.error,
+    ownsCatalog,
+    ownershipUnknown,
</file context>

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Preview verification — 2026-07-30

Preview https://chat-6eev0xop0-recoup.vercel.app, confirmed built from head 0bb24e0d (deployment looked up by sha, not by branch).

Fixtures from chat#1912: catalog A = 41aa8ad9-a347-438f-b2b4-998f49f8d4ec (Ben's, measured by another account), catalog B = 006ca598-0ee3-480a-9e78-55bbe6354ff2 (the viewer's own), account B = 10c76dc5-360f-4678-86a1-efeda71d4f2e.

Results

# Check Documented / before Actual on preview
1 Signed in as another account, open catalog A "No valuation found for this catalog … Run a valuation from recoupable.dev" "This catalog was measured by another account", CTA Go to your catalogs/catalogs
2 Anonymous, open catalog A "Couldn't load this report / Something went wrong loading the measurements" "Sign in to see this report" + Sign in button that opens Privy in place
3 Measuring state progress within 1s, resolves without a re-run not reproduced — see below ⚠️
4 Songs and report agree on access both visible or both withheld GET /api/catalogs/songs 200 unauthenticated, GET /…/measurements 401 ❌ known gap, out of scope
5 Owner regression control (catalog B) full report unchanged: $1.6K band, 160K lifetime streams, 24 of 24 tracks measured, 24 releases
6 No off-app CTA anywhere n/a asserted programmatically: zero occurrences of recoupable.dev in page text or in any href, on both the anonymous and cross-account states
7 Signed-out gets the CTA without waiting on songs (review P3) skeleton until songs resolve CTA renders immediately, no skeleton

Screenshots

Anonymous on catalog A — the state that used to read "Something went wrong":

anonymous sign-in state

Signed in as account B on catalog A — the headline fix:

cross-account state

Account B's own catalog — regression control, report renders exactly as before:

owner report control

Row 3: the measuring state was not reproduced, and the acceptance criterion was wrong

I could not reach it on the preview, and checking why turned up a mistake in how chat#1912 specified this.

The Works-when step assumed a window where a freshly seeded catalog exists but has not been measured yet. That window does not exist on the seeding path. runValuationHandler measures first and calls createSnapshotCatalog afterwards, so the catalog row is created after its measurements. Measured on the recreation account: this run's song_measurements landed at 14:30:54.905, the catalogs row at 14:30:55.644 — measurements 739ms earlier. There is no point at which you can open a just-seeded catalog and find it unmeasured.

So the state is real but reachable by a different route: a catalog that has songs and no measurements for this viewer — songs added through Manage songs or a CSV upload, then measured later. That is worth verifying before the row is closed, and it needs a fixture I did not want to fabricate on prod data.

What is verified: the state machine that produces it is unit-tested (getCatalogReportState, 11 cases including this branch), and the three sibling states it shares that machine with all behave correctly live, which exercises the same code path.

Recommendation: keep row 1 open for the measuring case, and re-word the issue's Works-when step 3 to the CSV/Manage-songs route. I have not edited the issue yet — say the word and I will.

Row 4 is a real gap, deliberately not closed here

GET /api/catalogs/songs?catalog_id=41aa8ad9-… returns HTTP 200 with no credentials, so the Manage songs tab still serves a stranger all 24 of Ben's tracks and ISRCs on the same page that now correctly withholds the valuation. Closing that is an api change plus a product decision about whether catalogs are public, which is why this PR's other-account copy is written to be true either way. Flagging it so this does not merge as if the access model were settled.

Review findings

All four validated against the code before acting, none rubber-stamped.

Finding Verdict Action
@sweetmantech — OCP: net new hooks added to the component Valid useCatalogReport + useMeasuringPoll now own composition and the effect; component is presentational again
cubic P1 / codex P2 — disabled catalog query treated as a resolved empty list Valid, real bug useOwnsCatalog derives resolution from isSuccess/isError. useCatalogs is enabled: !!accountId && authenticated, and a disabled TanStack v5 query reports isPending true / isFetching false, so !isLoading was already true while the account resolved — a signed-in owner would have been told their own catalog belonged to someone else. The same trap is documented in SetupValuation.tsx, which is what confirmed it
cubic P2 — 404 matched anywhere in the error text Valid matches the HTTP 404 status prefix now, so a 500 whose body mentions 404 stays an error
cubic P3 — signed-out visitors wait on the songs read Valid signed-out visitors no longer gate on songsQuery; verified live as row 7 above

Also added, not from a review: a failed catalog list now reports ownershipUnknown and renders the error state instead of guessing "other-account".

Suite 314 passed / 78 files, tsc --noEmit and eslint clean.

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