You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Tracking issue for redesigning the chat.recoupable.dev homepage from an empty chat greeting into a valuation-first dashboard ("Catalog HQ"), plus the funnel regressions found in the 2026-07-06 fresh-account walkthrough of the live .dev funnel (marketing valuation → chat handoff). Follows chat#1801 (valuation → onboarding loop, shipped on the .com domains 2026-06-18) and chat#1819 (.com→.dev migration, shipped 2026-06-30). Status 2026-07-07: the Catalog HQ core is live on prod — the valuation hero (chat#1852) and tasks module (chat#1853) render on chat.recoupable.dev (verified signed-in: Ana Bárbara $8.1M / 154 tracks over 5 completed run rows); the data path (docs#267 → database#42 → api#763) is merged. Remaining: chat#1854 (command bar), marketing#42/#43 polish, and the video-parity items below (added ahead of the launch video going out). All findings verified live on .dev with fresh accounts (sweetmantech+jul62026…@gmail.com, artist Del Water Gap). (Out-of-scope business context, stated once: the downstream goal is converting valuation leads into retained accounts; this issue only builds the surfaces and fixes the handoff.)
Decision 2026-07-06 (sweetman): the chat homepage pivots from chat-first to valuation-first. We are leaning away from Recoup-provided chat toward bring-your-own-agent, so the homepage's job is to show the work products (catalog value, task runs, sent emails), not an empty prompt box. Chat stays on the homepage as a command bar under the dashboard, not as the hero. Social/streaming stats are a supporting strip (they explain valuation movement), not a pillar.
Decision 2026-07-06 (sweetman, evening): artist scoping happens inside the measurements endpoint — an optional artist_account_id query param filtering via catalog_songs ∩ song_artists over the account's single catalog. Per-artist catalogs were prototyped live the same evening and rejected (they duplicate link rows and bake a workaround into the data model; the 15 test catalogs were removed). This supersedes chat#1852's client-side artist-matching (findArtistCatalog + songs sampling), which gets deleted once the filter ships. Same decision folds the 1,000-row pagination fix into the filter PR (same read path).
Decision 2026-07-07 (sweetman): the v2 read shape is amended before merge — server-side loop-to-exhaustion is rejected as an implicit scaling posture. Aggregates (measured_song_count, total_streams, valuation) are computed by a single SQL aggregate (whole-scope, constant-size — irreducible server work since a band over one page is meaningless); the measurementsarray is caller-paginated (page/limit + the sibling catalogs/songs pagination envelope). The hero reads the count field and fetches no rows; row-level consumers (chat#1803 landing) page. Supersedes the loop in api#763's first draft; docs#267/api#763/chat#1852 amended in place.
Decision 2026-07-07 (sweetman): REST shape — path for identity, query for modifiers. The endpoint moves to GET /api/catalogs/{catalogId}/measurements (required resource id in the path, matching the repo's measurements family: research/albums/{id}/measurements, research/tracks/{id}/measurements); query params are only the optional modifiers artist_account_id, page, limit (all snake_case — also removes the camelCase catalogId inconsistency). No default-catalog fallback when the id is absent — implicit resource selection is the same unintentional-scaling category as the rejected exhaustion loop. Amended into docs#267/api#763/chat#1852 pre-merge.
CLOSED 2026-07-08 — superseded by chat#1859. The homepage stack this issue tracked (hero, tasks module, prompt chips + the measurements data path) is fully live on prod, and the Catalog HQ launch video shipped on all four platforms 2026-07-07. Every remaining open item and in-flight PR (the P1 canonical-artist quartet, run-title trio, drift-guard pair, intent resume, marketing polish, and the carried funnel/platform items) moved verbatim to #1859, which carries the new goal: the fresh-customer funnel delivering what the video promised. Nothing was dropped.
Goal
A customer who ran a valuation on recoupable.dev opens chat.recoupable.dev and sees their catalog's estimated value (the same number marketing showed them), its change over time once a second snapshot exists, and what their agents did (task runs + the emails those runs sent) — with one authentication, not two. Concretely:
The marketing Privy session is recognized on chat.recoupable.dev (re-do #1801 Phase 1b for the .dev apex — config regression from the migration).
chat/app/page.tsx (components/Home/HomePage.tsx, greeting in components/Chat/ChatGreeting.tsx) renders the Catalog HQ: valuation hero → tasks/emails module → chat command bar.
The marketing valuation card stops undermining the CTA (collapse the $0-release tail) and the marketing header reflects auth state (signed-in indicator + logout).
PRs (updated 2026-07-08 — full homepage stack live on prod; 9 open: 6 video-parity + marketing#42/#43 + mono#45; P1 song_artists linkage + intent-resume unstarted)
Base-branch correction (2026-07-06): both api and chat have retired the test-branch PR flow since this issue's plan was drafted — api#750 (merged 2026-07-03) and chat commit 004b9de3 (PR chat#1843) say PRs target main directly. The bases below reflect reality and supersede the original test plan.
Root AGENTS.md: retire the stale test-branch PR guidance (the doc drift behind the base-branch correction above); rides along: the internal issue-workflow skills section
Video-parity: prefer the new "Weekly valuation + streams report" /agents template (created in prod, id ad0a233b…, valuation + WoW deltas + linked sources — fixes the deeper content gap, not just wording)
main
🔄 open — 127 tests; fresh-account live pass still blocked by the Privy test-app cap
Funnel fix: pending intent persisted to sessionStorage at the auth gate, resumed on auth flip AND mount (survives the remount that killed the ref path); 15-min staleness guard
main
🔄 open — TDD (80/80), build green; live fresh-signup pass pending
tbd — marketing
Funnel fix: label in-progress valuations as partial on the card (completed runs reconcile — verified twice; only the partial-as-final display remains)
main
⬜ not started — see Open — funnel regressions
n/a — GoDaddy DNS + Vercel
Ops: recoupable.com 307 → recoupable.dev restored (was a parking page)
—
✅ done 2026-07-07 — config, no PR — see Done (flip 307→308 pending)
Merge sequencing:Privy config → docs#265 → api#757 (✅ all done 2026-07-06) → docs#267 (✅ merged 2026-07-07) → database#42 (✅ merged 2026-07-07) → api#763 (✅ merged 2026-07-07) → chat#1852 (✅ merged 2026-07-07) → chat#1853 (✅ merged 2026-07-07) → chat#1854 (✅ merged 2026-07-07 21:37Z, 84747809) — the homepage stack is fully live on prod. marketing#42 + #43 are independent and can land anytime. After api v2 merges, re-run the authenticated artist-switching pass (per-artist values differ) before promoting the chat stack. In-flight fleets (2026-07-08): titles docs#268 → api#764 → chat#1857 (contract-first; chat is regression-free either side of the api deploy) · fixture pair api#765 + marketing#44 same-day · P1 quartet docs#269 → api#768 → marketing#46 only after api#768 is DEPLOYED (it deletes linkArtistToAccount, the only thing creating roster artists for funnel signups today — shipping it first means new signups get no roster artist, no artist carry-over, no hero: a regression worse than the bug) · database#43's migration is already live (repro flipped), the PR is the repo record · marketing#45 independent.
What we saw (context — fresh-account walkthrough, 2026-07-06)
Marketing aha moment works: Del Water Gap → $1.4M (range $959K–$2.0M), 378M lifetime streams, 23 tracks / 18 of 36 releases measured, per-release values.
The card ends with a long tail of $0 / "0 streams" release rows directly above the "Get the full report with Recoup →" CTA.
After OTP Tech322/desktop #2: the catalog materialized ("Found 67 songs in catalog" — vs marketing's 23/36; counts don't reconcile) but the song list renders empty ("Hiding items with missing info") and the $1.4M appears nowhere in the app.
Homepage: "Ask me about Del Water Gap" over an empty chat box (artist carry-over works — keep it). Tasks: bare "You have no scheduled tasks for this artist." Marketing header still shows Sign In / Sign Up after authenticating.
Done
Re-enable the cross-subdomain Privy cookie session on apex recoupable.dev (config, no PR).
✅ Done 2026-07-06 (Privy dashboard, sweetman) — HttpOnly cookies flipped to "ready" on the prod Privy app (cm4t193tx03w553u94ib9ucnh) for the .dev apex, per the #1801 Phase 1b playbook. Root cause: Phase 1b had been configured for apex recoupable.com; the .dev migration (#1819) only added chat.recoupable.dev to allowed origins, so each subdomain authenticated independently. Verified end-to-end on prod 2026-07-06 with a second fresh account (sweetmantech+jul620261135am@gmail.com, empty cookie jar): one OTP on recoupable.dev → Del Water Gap valuation → "Get the full report" → landed on chat.recoupable.dev/catalogs/740d5050…authenticated, no second modal (signed-in avatar, catalog rendered). Mechanism proof: POST https://privy.recoupable.dev/api/v1/sessions → 200 fired from the chat origin; privy-token absent from document.cookie (HttpOnly working; only the non-sensitive privy-session flag is JS-visible); authenticated api.recoupable.dev calls (/api/accounts, /api/artists, /api/catalogs/songs) all 200. This was the double-OTP drop-off observed with the morning account (…jul620261053am@).
docs: GET /api/catalogs/measurements?catalogId= contract (docs#265).
✅ Shipped 2026-07-06 (merged to docs main, commit 100a41fd). Documentation-driven: this is the spec api#757 implements — closes the data gap Close the valuation → onboarding loop: cross-subdomain Privy auth + materialize valuation as catalog + authenticated /catalogs handoff #1801/chat#1803 deferred (no endpoint returned play counts by ISRC/catalog; no valuation band existed).
Added the OpenAPI path + schemas (per-ISRC latest measurements array; valuation { low, mid, high } derived at read time; total_streams + catalog_age_years for auditability) and the reference page api-reference/songs/catalog-measurements.mdx + nav entry, following the docs#243 pattern. 4 files, +186/−16; the OpenAPI JSON diff was verified purely additive (every pre-existing path/schema byte-identical). Verified live 2026-07-06:https://docs.recoupable.dev/api-reference/songs/catalog-measurements → 200 post-deploy, and the page is listed in llms.txt ("Get Catalog Measurements").
api: implement GET /api/catalogs/measurements (api#757).
✅ Shipped 2026-07-06 (merged to api main, merge commit 7ed9338c; live on prod 21:07:54Z — api.recoupable.dev/api/catalogs/measurements answers the documented 400 unauthenticated probe). Implements the docs#265 contract exactly; unblocks the chat#1852 hero and the chat#1803 landing polish.
Account from bearer (no IDOR — foreign catalogs 404, verified with an admin bearer against another account's catalog); catalog → latest song_measurements per ISRC; valuation band derived server-side with the exact marketing/lib/valuation/computeCatalogValuation.ts constants ($0.0035/stream, gross-up 1.25/1.4/1.6, NLS 0.85×0.75, 10/13/16× multiples); catalog age from the source run's album release dates via a batched Spotify lookup (new lib/spotify/getAlbums.ts), default_5y fallback, surfaced as catalog_age_years. TDD throughout; full suite 3,923 tests / tsc 0 new errors. Verified on the PR preview (results, reconciliation): 400/401/404/OPTIONS all as documented; authed 200 on a seeded [TEST] catalog returned 5 per-ISRC latest playcounts (from that day's snapshot — global measurement reuse confirmed), band {low 103,227, mid 150,299, high 211,410}, total_streams 18,505,795; the low band reproduced by hand-arithmetic to rounding; never-measured songs verified omitted. Known drift (pre-existing, non-blocking): 400s carry an undocumented missing_fields array from the shared catalogs validator — docs follow-up suggested in the reconciliation comment.
docs: v2 measurements contract (docs#267).
✅ Shipped 2026-07-07 (merged to docs main 12:59Z, head acb9068). The contract api#763 implements: GET /api/catalogs/{catalogId}/measurements (path for identity per the 2026-07-07 REST decision), optional artist_account_id filter + response echo, whole-scope aggregates (measured_song_count, total_streams, valuation), caller-paginated measurements (page/limit ≤100, new CatalogMeasurementsPagination schema), snake_case params throughout. Sweetman's KISS review (short endpoint description) applied in 29231ea, replied on-thread. Verified: OpenAPI JSON parse-validated; diff structurally confirmed limited to the measurements path/schemas.
api: v2 measurements implementation (api#763 + database#42).
✅ Shipped 2026-07-07 (api merged to main 13:28Z, merge 9a6cd0e3; database RPCs merged d080fe90 and already live on the shared DB; api test re-synced b97b33d8 so chat previews serve the route). Implements docs#267 exactly: GET /api/catalogs/{catalogId}/measurements with optional artist_account_id filter + response echo, whole-scope aggregates via the get_catalog_measurements_aggregate/_page RPCs (single SQL aggregate — the 1,000-row cap is structurally gone), caller-paginated rows (limit ≤100), and validator-owned auth per review (SRP; auth-first like the research-family validators — unauthed malformed params now 401). Verified on the final preview (cfed6de4), full authed pass (results): every scope exact to independent SQL ground truth — whole catalog 2,679 / 16,308,837,441 / mid $132,456,300 (vs $39.3M on the capped build, a 3.4× correction); Elvis Crespo 322 / 1,799,402,184 / $14,614,295; September Mourning 40 / 21,325,663; pagination envelope math exact (limit=1 → 40 pages; page=2&limit=10 → 268 pages); echo null/uuid per scope; 401/400/404/OPTIONS all per contract; docs ↔ api ↔ live reconciled with zero drift. Suite 3,940 / tsc 0 new.
chat: homepage valuation hero — artist-scoped, inside ChatGreeting (chat#1852).
✅ Shipped 2026-07-07 (merged to chat main 15:35Z, merge 58e2071b). Three review-driven reworks landed inside the PR: (1) v2 endpoint scoping — the hero passes artist_account_id and requires the response echo before rendering an artist-labeled value (deploy-skew safe); (2) sweetman's KISS/OCP/SRP review — the hero renders inside ChatGreeting (the component chat.tsx already delegates its empty-state header to), so chat.tsx/NewChatBootstrap/HomePage are byte-identical to pre-PR main, zero prop drilling, all context via provider-backed hooks; (3) the no-data branch shows the greeting plus a "Get a free catalog valuation →" CTA instead of a dead end. Every empty-chat surface (/, /chat, session chats) gets the same single flow. Verified live on the final preview (hero pass): switching artists changes the value — Ana Bárbara $8.1M / 154 tracks → Apache $6M / 208 tracks, both exact to SQL ground truth; network shows GET /api/catalogs/{id}/measurements?artist_account_id=…&limit=1 (count-only fetch) with the echo gate satisfied; /chat renders the identical header. CTA branch unit-covered (no zero-data artist existed on the fixture account to show it live).
chat: homepage tasks module — "your label at work", beneath the hero (chat#1853).
✅ Shipped 2026-07-07 (merged to chat main 18:55Z, merge 4f581091). Recent runs from the existing GET /api/tasks/runs (no new api surface) with click-through to run detail; the one-click starter task is sourced from the existing /agents templates per review (findStarterTemplate prefers "Weekly Performance Dashboard", falls back to any Report tag, card hides when none — the hand-written prompt is deleted); per the valuation-first review the module composes into the empty-state header beneath the hero, leaving HomePage byte-identical to main (zero shared-component changes, like 1852). Verified live (results, ordering): module renders 5 real completed runs above nothing and below the $8.1M hero (DOM order valuation → label-at-work → input), run click-through works, hero regression-free. Starter-card click not exercisable live (the test account has run history, so the empty state is unreachable) — template-verbatim payload covered by the 121-test suite. Fast-follow noted: run rows all read "Scheduled Task" — GET /api/tasks/runs lacks the per-run task title.
chat: suggested prompt chips — the command-bar layer (chat#1854).
✅ Shipped 2026-07-07 (merged to chat main 21:37Z, merge 84747809). Slimmed twice at review to the issue's converged standard: the modules prop died when hero/tasks moved into ChatGreeting, and per the KISS review the chips need no props either — HomeSuggestedPrompts is fully self-contained (module relevance from provider-backed hooks, prefill via useVercelChatContext().setInput, self-hiding), rendered inside ChatGreeting. chat.tsx/NewChatBootstrap/HomePage byte-identical to pre-PR main; the layout hardening was dropped as YAGNI. Verified live (results): DOM order hero → tasks → chips, three artist-personalized chips, chip click prefills the full prompt with no auto-send. Architecture note (the pattern of this whole stack): all three homepage PRs were conceived as "add a capability to the chat component" and each ended as "compose a self-contained, provider-fed component into the header the chat component already renders." The extension point was always already there — future homepage modules should start from that shape.
Ops: recoupable.com was a GoDaddy parking page — redirect restored.
✅ Fixed 2026-07-07 (sweetman, GoDaddy DNS → Vercel; config, no PR). Found the same evening walking the funnel from the old domain: recoupable.com/lander served GoDaddy's "parked free / Get This Domain" ad page — every pre-2026-06-30 social link dead-ended there. Verified 2026-07-07 23:30Z:https://recoupable.com → 307 → https://recoupable.dev/ (200); www and chat.recoupable.com also resolve to .dev. Follow-up nit: flip the 307 (temporary) to 301/308 permanent in Vercel domain settings so .com link equity transfers.
Open — funnel regressions (fix first)
P1: funnel accounts' roster artist is a socials-only duplicate — the artist-scoped hero hides. (→ docs#269 · database#43 · api#768 · marketing#46; backfill live on prod, repro verified fixed: 4b934253… filtered → 67 / 557,513,905) (restored 2026-07-08 — this item and the two below were lost in a concurrent body edit; original evidence in the walkthrough comment)
Why: found 2026-07-07 in the fresh-account walkthrough (sweetmantech+july720261741@gmail.com, Del Water Gap, catalog 4b934253…). The homepage called the endpoint correctly and got 200s, but the artist-scoped read returned zero: unfiltered → 67 measured / 557,513,905 streams / $2.06M mid; ?artist_account_id=99e626c0… (the account's own Del Water Gap artist) → 0 / 0 / $0 (echo correct). Funnel materialization writes catalog_songs but no song_artists rows tied to the account's artist, so the video-driven customer completes the funnel and the homepage shows the greeting fallback — the exact drop the launch video says is fixed. Related but distinct from the roster auto-fill design item (that's pre-funnel accounts; this is funnel accounts).
Fix (corrected by the 2026-07-08 audit — the original fix would duplicate links): the songs are already linked to the canonical artist (7647f901…, 67/67); the defect is marketing/lib/valuation/linkArtistToAccount.ts minting a duplicate artist account per signup (POST /api/artists {name}, no lookup). Fix: find-or-reuse the canonical artist by Spotify id at link time (api-side find-or-attach on POST /api/artists given a Spotify profile); backfill: repoint funnel-created duplicate roster artists to their canonical twin (match on Spotify profile URL).
Done when: a fresh funnel account's homepage hero shows the same value as their completed valuation card (repro above returns 67/$2.1M with the artist filter); existing funnel catalogs backfilled.
Post-auth intent drop: "Value my catalog" does not resume after the OTP — the lead must click it a second time. (→ marketing#45)
Why: 2026-07-07 walkthrough — clicking "Value my catalog" opens Privy; after the code, the modal closes back to the idle homepage (artist chip still selected) and nothing runs until the button is clicked again. Every funnel conversion pays this extra step; some fraction won't realize they must re-click.
Fix (sharpened by the 2026-07-08 audit): a resume mechanism exists (useCatalogValuation's pendingRun ref + auto-fire on authenticated — the 07-06 walkthrough auto-resumed through it) but a ref dies on remount, which fresh-signup auth flows trigger. Persist {artistId, action} to sessionStorage at the auth gate and resume from storage, not the ref.
Done when: search artist → Value my catalog → OTP → the valuation runs without further clicks.
Reconcile the track counts: the marketing card's partial measurement vs the catalog's final count.
Why: the numbers a lead sees before and after the handoff disagree, which reads as broken on a financial product. Root-caused 2026-07-06 by comparing two runs of the same artist: the morning run's card stalled at "23 tracks / 18 of 36 releases / $1.4M (range $959K–$2.0M)" while measurement continued server-side (the materialized catalog got all 67 songs); the afternoon run completed in-card and showed "67 tracks / 557M streams / $2.1M (range $1.4M–$2.9M)". So the card can present a partial measurement as final — the count mismatch and a materially different dollar figure are both progressive-measurement display bugs on the marketing card, not materialization bugs.
Fix: on the marketing valuation card, distinguish in-progress from complete (keep the live progress indicator until all releases resolve or time out; label a partial result as partial, e.g. "based on 18 of 36 releases so far"), and reconcile the final card state with what materializes.
Done when: a valuation that is still measuring is visibly labeled partial; a completed card's track count matches the materialized catalog count for the same snapshot; two same-day runs of the same artist produce the same completed valuation.
Ops: chat previews' Privy test app is at its user cap (blocks fresh-account preview testing).
Why: found 2026-07-06 while verifying chat#1852 — chat *.vercel.app previews authenticate against the test-mode Privy app (cmc52us2g…), which now refuses new users ("test application that has reached its user limit"). Existing users still sign in. Every fresh-account verification on a chat preview is blocked until this is lifted; also note previews call test-recoup-api.vercel.app, whose test branch is no longer auto-synced since the PR flow retired (manually synced b059655d on 2026-07-06 for the 1852 pass) — consider repointing non-prod NEW_API_BASE_URL at api.recoupable.dev.
Fix: upgrade the Privy test app to production (or raise the cap) in the Privy dashboard; decide the preview api-base question.
Done when: a brand-new email can sign into a chat preview; preview api base has a documented owner/sync story.
api: the artistName filter on GET /api/catalogs/songs no-ops on unlinked songs.
Why: found 2026-07-06 during chat#1852 live verification — ?catalog_id=7d3e5c35…&artistName=Ana+Bárbara&limit=1 returned total_count: 6 (every song, artists:[null]) for a catalog the artist has no songs in. Latent since api#681 relaxed song_artists!inner to a LEFT-join in selectCatalogSongsWithArtists: the filter can no longer exclude rows whose artist link is null/other. Any consumer treating the filter as an artist-membership test gets false positives (chat#1852's first fix did). No longer load-bearing for the hero once the v2 artist_account_id filter ships — still a correctness bug for other consumers.
Fix: apply the artistName predicate so non-matching and null-linked rows are excluded (filtered subquery or .not("song_artists", "is", null)-style constraint on the joined relation) while keeping the api#681 behavior for the unfiltered read (artist-less songs still listed). RED test first: filtered query on a catalog with unlinked songs returns 0.
Done when: the Ana Bárbara request above returns total_count: 0; unfiltered reads still include artist-less songs; chat can drop the client-side workaround (lib/home/isArtistCatalogMatch.ts note updated).
Open — architecture: auto-fill the account catalog (so the hero has data)
Design: how do we auto-fill an account's initial catalog — for new AND existing accounts — with songs + artists, so the homepage valuation renders? (raised by sweetman 2026-07-06)
Why: the hero only lights up for funnel-claimed catalogs. Verified 2026-07-06 on a real existing account with a 15-artist roster: zero of those artists had any rows in songs/song_artists — the songs graph is only populated by the valuation/measurement pipeline, so every pre-funnel account sees an empty homepage forever unless something backfills their roster's music.
Prototype (ran manually today, works): for each roster artist — Spotify id from account_socials → albums via the auth-free GET /api/spotify/artist/albums proxy → POST /api/research/measurement-jobs {source:"current", scope:{album_ids}} → add captured ISRCs to the account catalog via POST /api/catalogs/songs. 15 artists / 880 releases accepted without hitting the per-org job cap.
Design questions to settle: trigger (on artist-add? on first login? backfill cron for existing accounts?), cost control (880 releases for one account — cap per artist? albums-only vs singles?), catalog shape (one account catalog vs per-artist catalogs — interacts with the artist-scoped hero), idempotency/refresh cadence (re-measure for the value-over-time delta), and ownership semantics (roster artist ≠ owned catalog; see the diligence-buyer persona).
Done when: a design note on this issue answers the questions above and links a follow-up implementation tracker.
Data bug: 10 of 15 roster artists' Spotify ids are stored lowercased — album resolution 502s.
Why: Spotify ids are case-sensitive base62; account_socials → socials.profile_url rows like open.spotify.com/artist/1c22gxh30ijlofxhflz9df (Elvis Crespo) fail the albums proxy (502), while the true id 1c22GXH30ijlOfXhfLz9Df resolves. Found 2026-07-06; recovered all 10 by name-searching the Spotify search proxy and matching lower(id). Any feature resolving artist → Spotify by stored id breaks for these rows.
Fix: find where profile URLs get lowercased on write (likely a normalize step), stop case-flattening the path segment, and backfill existing rows (the lower(id) match trick makes the backfill mechanical). Note (2026-07-08 audit):api#768's socials-at-ingest is case-preserved, fixing the forward path for funnel-attached artists — this item narrows to the normalize-on-write culprit elsewhere + the backfill of existing lowercased rows.
Done when: stored artist Spotify ids resolve against the albums proxy; a spot-check SQL shows no all-lowercase artist ids that fail resolution.
Why: the hero only lights up for funnel-claimed catalogs. Verified 2026-07-06 on a real existing account with a 15-artist roster: zero of those artists had any rows in songs/song_artists — the songs graph is only populated by the valuation/measurement pipeline, so every pre-funnel account sees an empty homepage forever unless something backfills their roster's music.
Prototype (ran manually today, works): for each roster artist — Spotify id from account_socials → albums via the auth-free GET /api/spotify/artist/albums proxy → POST /api/research/measurement-jobs {source:"current", scope:{album_ids}} → add captured ISRCs to the account catalog via POST /api/catalogs/songs. 15 artists / 880 releases accepted without hitting the per-org job cap.
Design questions to settle: trigger (on artist-add? on first login? backfill cron for existing accounts?), cost control (880 releases for one account — cap per artist? albums-only vs singles?), catalog shape (one account catalog vs per-artist catalogs — interacts with the artist-scoped hero), idempotency/refresh cadence (re-measure for the value-over-time delta), and ownership semantics (roster artist ≠ owned catalog; see the diligence-buyer persona).
Done when: a design note on this issue answers the questions above and links a follow-up implementation tracker.
api P1: GET /api/catalogs/measurements silently caps at 1,000 rows — believed FIXED by the v2 SQL aggregates (api#763, merged 2026-07-07): the band is now computed by a whole-scope RPC, not a row-limited read. Pending one verify-and-close (audit note 2026-07-08): authed GET /api/catalogs/7d3e5c35…/measurements?limit=1 should report measured_song_count 2,679 and a band consistent with 16.31B streams; then move this to Done.
Why: found 2026-07-06 on a real 2,679-song roster catalog: the endpoint returned exactly 1,000 measurements / 4.83B streams / $39.3M mid, vs SQL ground truth 2,679 songs / 16.31B streams / ≈$132.5M mid at the same formula. The latest-measurements read hits the Supabase default row limit and computes total_streams + the band over the truncated set — no error, no flag; the chat hero literally renders "1000 tracks measured". (repro + numbers)
Fix: paginate the read to exhaustion (.range() loop) before dedupe/derivation; RED test with a >1,000-song fixture.
Done when: the [TEST] roster catalog (7d3e5c35…) returns 2,679 measurements and a band consistent with 16.31B streams; the hero count matches the SQL count.
chat: the hero's client-side artist match only samples page 1 (50 songs) — RESOLVED by the v2 server-side artist_account_id filter shipped in chat#1852: lib/home/isArtistCatalogMatch.ts no longer exists on chat main (verified 2026-07-08) and the 1852 Done note's artist-switching pass (Ana Bárbara $8.1M / Apache $6M, exact to SQL) covers the acceptance. Move to Done on next grooming pass:
Why: on the 2,679-song roster catalog, Apache has 208 songs in the catalog but none in the first 50, so the artist-scoped hero stayed hidden for him (worked around in the demo via the catalog name). Fine for funnel-sized catalogs; wrong for roster-scale ones.
Fix: the durable fix is server-side — the api artistName filter repair above makes one filtered pageSize=1 call correct again; swap isArtistCatalogMatch back to it once fixed (the code notes this).
Done when: an artist with songs anywhere in a large catalog lights the hero; the client-side sampling workaround is deleted.
Open — video/experience parity (launch content vs. prod, added 2026-07-07)
The Catalog HQ launch video (produced 2026-07-06, publishing 2026-07-07) makes claims a new customer will test within minutes of watching. Prod verification 2026-07-07 (signed-in pass, screenshots in the strategy workspace sweetman/content/catalog-hq-launch/assets/screens/) found the claims true, with the gaps below — each is a place the on-screen experience underdelivers vs. the video.
api + chat: homepage run rows all read "Scheduled Task" — surface the real task title. (→ docs#268 · api#764 · chat#1857)
Why: the video's tasks-module mockup shows named runs ("Weekly valuation + streams report"); prod shows five identical "Scheduled Task" rows (verified live 2026-07-07). GET /api/tasks/runs exposes only taskIdentifier (all runs share the generic runner identity), so getTaskDisplayName has nothing better to render. Same underlying gap as the run → sent-email linkage (chat#1832).
Fix: api first (contract-first): thread the originating scheduled_actions.title onto each run in GET /api/tasks/runs; then chat renders it in HomeRunRow with the current label as fallback.
Done when: the signed-in homepage lists runs by their task titles; a run with no resolvable title still renders (generic fallback), none crash.
Why: the video's integrity beat says the numbers "cannot drift" between the valuation and the app. Today that's true by discipline, not structure: api#757/create new artists on signup or when no artists is present #763mirror the formula from marketing/lib/valuation/computeCatalogValuation.ts (runValuationFlow.ts still computes client-side). Two implementations of one formula will eventually diverge — and the divergence would land exactly on the number we told customers cannot drift.
Fix: either marketing reads the band from GET /api/catalogs/{catalogId}/measurements once a catalog exists, or both consumers import one shared derivation package; pick in a design note (interacts with the progressive-measurement fix above — partial in-card state still needs client math).
Done when: one implementation of the band formula exists in prod code paths, or the two are guarded by a shared golden-test fixture that fails CI on divergence.
chat: starter-task card — video-consistent template, content included. (→ chat#1856; template ad0a233b… live in prod /agents — audit found the old preferred template's prompt had NO valuation at all, so this fixes content, not just wording)
Why: the video's empty-state mockup names "Weekly valuation + streams report for {artist}, Mondays"; the shipped card sources its label from the /agents template (findStarterTemplate prefers "Weekly Performance Dashboard"). A viewer arriving from the video hits a differently-named starter. Low priority — but the fresh-account empty state is exactly the surface every video-driven signup lands on, and it's still unverified live (blocked on the Privy test-app cap item above for previews; prod fresh-account pass pending).
Fix: either rename/point the preferred template so the starter reads as a valuation + streams report, or accept and note the wording. Verify the empty state live with a fresh account while at it.
Done when: a fresh account's homepage shows the starter card with wording consistent with the launch video, verified live; one-click create works.
Open — chat homepage: Catalog HQ (in merge order; each PR user-testable)
Open — marketing polish (independent)
Valuation card: collapse the $0 release tail. (→ marketing#42)
Why: the last content a lead scrolls past before the CTA is a wall of "$0 · 0 streams" rows (18 unmeasured releases for Del Water Gap) — it reads as failure at the exact conversion moment.
Fix: group unmeasured releases into one row: "+18 releases with no measurable streams"; keep measured releases sorted by value.
Done when: a valuation with unmeasured releases shows no $0 rows; the collapsed row states the count.
Marketing header: signed-in state + logout. (→ marketing#43)
Why: after Privy auth the header still shows Sign In / Sign Up; there is no way to log out of the marketing surface (had to clear cookies to test the funnel fresh).
Fix: header reflects the Privy session (avatar/email + "Open app → chat.recoupable.dev" + logout via the Privy SDK).
Done when: a signed-in visitor sees their session state on recoupable.dev and can log out; signed-out visitors see the current CTA pair.
Architecture decisions
Homepage = dashboard-first ("Catalog HQ"); chat = command bar. Decided 2026-07-06 (sweetman). The BYO-agent strategy means the homepage shows work products (valuation, task runs, sent emails) regardless of which agent produced them; an empty prompt box serves neither new nor returning users. Supersedes the chat-first home implicitly assumed since the workflow cutover (chat#1747).
All homepage data flows through api.recoupable.dev endpoints — no direct Supabase reads from chat (repo standard: lib/supabase/[table]/[fn].ts server-side only, API for cross-app data).
api: POST /api/catalogs materialization (api#677), GET /api/catalogs/songs, GET /api/tasks/runs (see chat#1818), measurement jobs api/lib/research/measurement_jobs/*, tables catalogs, catalog_songs, playcount_snapshots, song_measurements.
Tracking issue for redesigning the
chat.recoupable.devhomepage from an empty chat greeting into a valuation-first dashboard ("Catalog HQ"), plus the funnel regressions found in the 2026-07-06 fresh-account walkthrough of the live.devfunnel (marketing valuation → chat handoff). Follows chat#1801 (valuation → onboarding loop, shipped on the.comdomains 2026-06-18) and chat#1819 (.com→.devmigration, shipped 2026-06-30). Status 2026-07-07: the Catalog HQ core is live on prod — the valuation hero (chat#1852) and tasks module (chat#1853) render onchat.recoupable.dev(verified signed-in: Ana Bárbara $8.1M / 154 tracks over 5 completed run rows); the data path (docs#267 → database#42 → api#763) is merged. Remaining: chat#1854 (command bar), marketing#42/#43 polish, and the video-parity items below (added ahead of the launch video going out). All findings verified live on.devwith fresh accounts (sweetmantech+jul62026…@gmail.com, artist Del Water Gap). (Out-of-scope business context, stated once: the downstream goal is converting valuation leads into retained accounts; this issue only builds the surfaces and fixes the handoff.)Goal
A customer who ran a valuation on
recoupable.devopenschat.recoupable.devand sees their catalog's estimated value (the same number marketing showed them), its change over time once a second snapshot exists, and what their agents did (task runs + the emails those runs sent) — with one authentication, not two. Concretely:chat.recoupable.dev(re-do #1801 Phase 1b for the.devapex — config regression from the migration).GET /api/catalogs/measurements?catalogId=endpoint surfaces per-ISRC play counts + a derived valuation band (the data plumbing deferred in Close the valuation → onboarding loop: cross-subdomain Privy auth + materialize valuation as catalog + authenticated /catalogs handoff #1801 / chat#1803).chat/app/page.tsx(components/Home/HomePage.tsx, greeting incomponents/Chat/ChatGreeting.tsx) renders the Catalog HQ: valuation hero → tasks/emails module → chat command bar.$0-release tail) and the marketing header reflects auth state (signed-in indicator + logout).PRs (updated 2026-07-08 — full homepage stack live on prod; 9 open: 6 video-parity + marketing#42/#43 + mono#45; P1 song_artists linkage + intent-resume unstarted)
recoupable.dev(re-do #1801 Phase 1b)GET /api/catalogs/measurementscontract (per-ISRC latest measurements + derived valuation band)mainGET /api/catalogs/measurements(formula mirrored frommarketing/lib/valuation/computeCatalogValuation.ts)mainGET /api/catalogs/{catalogId}/measurements— filter + echo + whole-scope aggregates + caller-paginated rowsmainget_catalog_measurements_aggregate+get_catalog_measurements_pageRPCs (STABLE, latest-per-ISRCDISTINCT ON)maind080fe90) — migration was already live on the shared DB (applied via MCP, version20260707120000); repo record now in step0e358f04): path rename + RPC aggregates + caller pagination + filter/echomain9a6cd0e3) — serving prod: the chat#1852 hero readsGET /api/catalogs/{id}/measurements?artist_account_id=…&limit=1live (network-verified 2026-07-07)mainmainHomeSuggestedPromptsin the header, zero core chat changesmain$0/0-stream release tailmainpnpm buildgreenmainpnpm buildgreen; live signed-in pass pending OTPtest-branch PR guidance (the doc drift behind the base-branch correction above); rides along: the internal issue-workflow skills sectionmainTaskRunResponsegains nullabletitle(contract)mainfilter[schedule]lookup againstscheduled_actions(Trigger list rows carry no schedule ref — verified)mainHomeRunRowrenders the real title,getTaskDisplayNamefallbackmaina2c9737f…) + lockstep tests in both repos — divergence fails CI (design note; true shared package deferred)mainad0a233b…, valuation + WoW deltas + linked sources — fixes the deeper content gap, not just wording)mainmainmainmainlinkArtistToAccount— the duplicate mint (claim attach is the roster source of truth; sole caller was the funnel)mainmainmainWhat we saw (context — fresh-account walkthrough, 2026-07-06)
chat.recoupable.dev/catalogs/{id}→ second Privy OTP required (the Close the valuation → onboarding loop: cross-subdomain Privy auth + materialize valuation as catalog + authenticated /catalogs handoff #1801 cookie session was configured for apexrecoupable.com; Fix domain-change breakages: recoupable.com → .dev across the onboarding pipeline #1819 only addedchat.recoupable.devto allowed origins). Fixed + verified same day — see Done.Done
Re-enable the cross-subdomain Privy cookie session on apex
recoupable.dev(config, no PR).✅ Done 2026-07-06 (Privy dashboard, sweetman) — HttpOnly cookies flipped to "ready" on the prod Privy app (
cm4t193tx03w553u94ib9ucnh) for the.devapex, per the #1801 Phase 1b playbook. Root cause: Phase 1b had been configured for apexrecoupable.com; the.devmigration (#1819) only addedchat.recoupable.devto allowed origins, so each subdomain authenticated independently.Verified end-to-end on prod 2026-07-06 with a second fresh account (
sweetmantech+jul620261135am@gmail.com, empty cookie jar): one OTP onrecoupable.dev→ Del Water Gap valuation → "Get the full report" → landed onchat.recoupable.dev/catalogs/740d5050…authenticated, no second modal (signed-in avatar, catalog rendered). Mechanism proof:POST https://privy.recoupable.dev/api/v1/sessions → 200fired from the chat origin;privy-tokenabsent fromdocument.cookie(HttpOnly working; only the non-sensitiveprivy-sessionflag is JS-visible); authenticatedapi.recoupable.devcalls (/api/accounts,/api/artists,/api/catalogs/songs) all 200. This was the double-OTP drop-off observed with the morning account (…jul620261053am@).docs:
GET /api/catalogs/measurements?catalogId=contract (docs#265).✅ Shipped 2026-07-06 (merged to docs
main, commit100a41fd). Documentation-driven: this is the spec api#757 implements — closes the data gap Close the valuation → onboarding loop: cross-subdomain Privy auth + materialize valuation as catalog + authenticated /catalogs handoff #1801/chat#1803 deferred (no endpoint returned play counts by ISRC/catalog; no valuation band existed).Added the OpenAPI path + schemas (per-ISRC latest measurements array;
valuation { low, mid, high }derived at read time;total_streams+catalog_age_yearsfor auditability) and the reference pageapi-reference/songs/catalog-measurements.mdx+ nav entry, following the docs#243 pattern. 4 files, +186/−16; the OpenAPI JSON diff was verified purely additive (every pre-existing path/schema byte-identical).Verified live 2026-07-06:
https://docs.recoupable.dev/api-reference/songs/catalog-measurements→ 200 post-deploy, and the page is listed inllms.txt("Get Catalog Measurements").api: implement
GET /api/catalogs/measurements(api#757).✅ Shipped 2026-07-06 (merged to api
main, merge commit7ed9338c; live on prod 21:07:54Z —api.recoupable.dev/api/catalogs/measurementsanswers the documented 400 unauthenticated probe). Implements the docs#265 contract exactly; unblocks the chat#1852 hero and the chat#1803 landing polish.Account from bearer (no IDOR — foreign catalogs 404, verified with an admin bearer against another account's catalog); catalog → latest
song_measurementsper ISRC; valuation band derived server-side with the exactmarketing/lib/valuation/computeCatalogValuation.tsconstants ($0.0035/stream, gross-up 1.25/1.4/1.6, NLS 0.85×0.75, 10/13/16× multiples); catalog age from the source run's album release dates via a batched Spotify lookup (newlib/spotify/getAlbums.ts),default_5yfallback, surfaced ascatalog_age_years. TDD throughout; full suite 3,923 tests /tsc0 new errors.Verified on the PR preview (results, reconciliation): 400/401/404/OPTIONS all as documented; authed 200 on a seeded
[TEST]catalog returned 5 per-ISRC latest playcounts (from that day's snapshot — global measurement reuse confirmed), band{low 103,227, mid 150,299, high 211,410},total_streams18,505,795; the low band reproduced by hand-arithmetic to rounding; never-measured songs verified omitted. Known drift (pre-existing, non-blocking): 400s carry an undocumentedmissing_fieldsarray from the shared catalogs validator — docs follow-up suggested in the reconciliation comment.docs: v2 measurements contract (docs#267).
✅ Shipped 2026-07-07 (merged to docs
main12:59Z, headacb9068). The contract api#763 implements:GET /api/catalogs/{catalogId}/measurements(path for identity per the 2026-07-07 REST decision), optionalartist_account_idfilter + response echo, whole-scope aggregates (measured_song_count,total_streams,valuation), caller-paginatedmeasurements(page/limit≤100, newCatalogMeasurementsPaginationschema), snake_case params throughout. Sweetman's KISS review (short endpoint description) applied in29231ea, replied on-thread.Verified: OpenAPI JSON parse-validated; diff structurally confirmed limited to the measurements path/schemas.
api: v2 measurements implementation (api#763 + database#42).
✅ Shipped 2026-07-07 (api merged to
main13:28Z, merge9a6cd0e3; database RPCs mergedd080fe90and already live on the shared DB; apitestre-syncedb97b33d8so chat previews serve the route). Implements docs#267 exactly:GET /api/catalogs/{catalogId}/measurementswith optionalartist_account_idfilter + response echo, whole-scope aggregates via theget_catalog_measurements_aggregate/_pageRPCs (single SQL aggregate — the 1,000-row cap is structurally gone), caller-paginated rows (limit≤100), and validator-owned auth per review (SRP; auth-first like the research-family validators — unauthed malformed params now 401).Verified on the final preview (
cfed6de4), full authed pass (results): every scope exact to independent SQL ground truth — whole catalog 2,679 / 16,308,837,441 / mid $132,456,300 (vs $39.3M on the capped build, a 3.4× correction); Elvis Crespo 322 / 1,799,402,184 / $14,614,295; September Mourning 40 / 21,325,663; pagination envelope math exact (limit=1→ 40 pages;page=2&limit=10→ 268 pages); echonull/uuid per scope; 401/400/404/OPTIONS all per contract; docs ↔ api ↔ live reconciled with zero drift. Suite 3,940 / tsc 0 new.chat: homepage valuation hero — artist-scoped, inside ChatGreeting (chat#1852).
✅ Shipped 2026-07-07 (merged to chat
main15:35Z, merge58e2071b). Three review-driven reworks landed inside the PR: (1) v2 endpoint scoping — the hero passesartist_account_idand requires the response echo before rendering an artist-labeled value (deploy-skew safe); (2) sweetman's KISS/OCP/SRP review — the hero renders inside ChatGreeting (the component chat.tsx already delegates its empty-state header to), sochat.tsx/NewChatBootstrap/HomePageare byte-identical to pre-PRmain, zero prop drilling, all context via provider-backed hooks; (3) the no-data branch shows the greeting plus a "Get a free catalog valuation →" CTA instead of a dead end. Every empty-chat surface (/,/chat, session chats) gets the same single flow.Verified live on the final preview (hero pass): switching artists changes the value — Ana Bárbara $8.1M / 154 tracks → Apache $6M / 208 tracks, both exact to SQL ground truth; network shows
GET /api/catalogs/{id}/measurements?artist_account_id=…&limit=1(count-only fetch) with the echo gate satisfied;/chatrenders the identical header. CTA branch unit-covered (no zero-data artist existed on the fixture account to show it live).chat: homepage tasks module — "your label at work", beneath the hero (chat#1853).
✅ Shipped 2026-07-07 (merged to chat
main18:55Z, merge4f581091). Recent runs from the existingGET /api/tasks/runs(no new api surface) with click-through to run detail; the one-click starter task is sourced from the existing /agents templates per review (findStarterTemplate prefers "Weekly Performance Dashboard", falls back to any Report tag, card hides when none — the hand-written prompt is deleted); per the valuation-first review the module composes into the empty-state header beneath the hero, leaving HomePage byte-identical to main (zero shared-component changes, like 1852).Verified live (results, ordering): module renders 5 real completed runs above nothing and below the $8.1M hero (DOM order valuation → label-at-work → input), run click-through works, hero regression-free. Starter-card click not exercisable live (the test account has run history, so the empty state is unreachable) — template-verbatim payload covered by the 121-test suite. Fast-follow noted: run rows all read "Scheduled Task" —
GET /api/tasks/runslacks the per-run task title.chat: suggested prompt chips — the command-bar layer (chat#1854).
✅ Shipped 2026-07-07 (merged to chat
main21:37Z, merge84747809). Slimmed twice at review to the issue's converged standard: themodulesprop died when hero/tasks moved into ChatGreeting, and per the KISS review the chips need no props either —HomeSuggestedPromptsis fully self-contained (module relevance from provider-backed hooks, prefill viauseVercelChatContext().setInput, self-hiding), rendered inside ChatGreeting.chat.tsx/NewChatBootstrap/HomePagebyte-identical to pre-PRmain; the layout hardening was dropped as YAGNI.Verified live (results): DOM order hero → tasks → chips, three artist-personalized chips, chip click prefills the full prompt with no auto-send.
Architecture note (the pattern of this whole stack): all three homepage PRs were conceived as "add a capability to the chat component" and each ended as "compose a self-contained, provider-fed component into the header the chat component already renders." The extension point was always already there — future homepage modules should start from that shape.
Ops:
recoupable.comwas a GoDaddy parking page — redirect restored.✅ Fixed 2026-07-07 (sweetman, GoDaddy DNS → Vercel; config, no PR). Found the same evening walking the funnel from the old domain:
recoupable.com/landerserved GoDaddy's "parked free / Get This Domain" ad page — every pre-2026-06-30 social link dead-ended there.Verified 2026-07-07 23:30Z:
https://recoupable.com→ 307 →https://recoupable.dev/(200);wwwandchat.recoupable.comalso resolve to.dev. Follow-up nit: flip the 307 (temporary) to 301/308 permanent in Vercel domain settings so.comlink equity transfers.Open — funnel regressions (fix first)
P1: funnel accounts' roster artist is a socials-only duplicate — the artist-scoped hero hides. (→ docs#269 · database#43 · api#768 · marketing#46; backfill live on prod, repro verified fixed:
4b934253…filtered → 67 / 557,513,905) (restored 2026-07-08 — this item and the two below were lost in a concurrent body edit; original evidence in the walkthrough comment)sweetmantech+july720261741@gmail.com, Del Water Gap, catalog4b934253…). The homepage called the endpoint correctly and got 200s, but the artist-scoped read returned zero: unfiltered → 67 measured / 557,513,905 streams / $2.06M mid;?artist_account_id=99e626c0…(the account's own Del Water Gap artist) → 0 / 0 / $0 (echo correct). Funnel materialization writescatalog_songsbut nosong_artistsrows tied to the account's artist, so the video-driven customer completes the funnel and the homepage shows the greeting fallback — the exact drop the launch video says is fixed. Related but distinct from the roster auto-fill design item (that's pre-funnel accounts; this is funnel accounts).7647f901…, 67/67); the defect ismarketing/lib/valuation/linkArtistToAccount.tsminting a duplicate artist account per signup (POST /api/artists {name}, no lookup). Fix: find-or-reuse the canonical artist by Spotify id at link time (api-side find-or-attach onPOST /api/artistsgiven a Spotify profile); backfill: repoint funnel-created duplicate roster artists to their canonical twin (match on Spotify profile URL).Post-auth intent drop: "Value my catalog" does not resume after the OTP — the lead must click it a second time. (→ marketing#45)
useCatalogValuation'spendingRunref + auto-fire onauthenticated— the 07-06 walkthrough auto-resumed through it) but a ref dies on remount, which fresh-signup auth flows trigger. Persist{artistId, action}tosessionStorageat the auth gate and resume from storage, not the ref.Reconcile the track counts: the marketing card's partial measurement vs the catalog's final count.
Ops: chat previews' Privy test app is at its user cap (blocks fresh-account preview testing).
*.vercel.apppreviews authenticate against the test-mode Privy app (cmc52us2g…), which now refuses new users ("test application that has reached its user limit"). Existing users still sign in. Every fresh-account verification on a chat preview is blocked until this is lifted; also note previews calltest-recoup-api.vercel.app, whosetestbranch is no longer auto-synced since the PR flow retired (manually syncedb059655don 2026-07-06 for the 1852 pass) — consider repointing non-prodNEW_API_BASE_URLatapi.recoupable.dev.api: the
artistNamefilter onGET /api/catalogs/songsno-ops on unlinked songs.?catalog_id=7d3e5c35…&artistName=Ana+Bárbara&limit=1returnedtotal_count: 6(every song,artists:[null]) for a catalog the artist has no songs in. Latent since api#681 relaxedsong_artists!innerto a LEFT-join inselectCatalogSongsWithArtists: the filter can no longer exclude rows whose artist link is null/other. Any consumer treating the filter as an artist-membership test gets false positives (chat#1852's first fix did). No longer load-bearing for the hero once the v2artist_account_idfilter ships — still a correctness bug for other consumers..not("song_artists", "is", null)-style constraint on the joined relation) while keeping the api#681 behavior for the unfiltered read (artist-less songs still listed). RED test first: filtered query on a catalog with unlinked songs returns 0.total_count: 0; unfiltered reads still include artist-less songs; chat can drop the client-side workaround (lib/home/isArtistCatalogMatch.tsnote updated).Open — architecture: auto-fill the account catalog (so the hero has data)
Design: how do we auto-fill an account's initial catalog — for new AND existing accounts — with songs + artists, so the homepage valuation renders? (raised by sweetman 2026-07-06)
songs/song_artists— the songs graph is only populated by the valuation/measurement pipeline, so every pre-funnel account sees an empty homepage forever unless something backfills their roster's music.account_socials→ albums via the auth-freeGET /api/spotify/artist/albumsproxy →POST /api/research/measurement-jobs {source:"current", scope:{album_ids}}→ add captured ISRCs to the account catalog viaPOST /api/catalogs/songs. 15 artists / 880 releases accepted without hitting the per-org job cap.Data bug: 10 of 15 roster artists' Spotify ids are stored lowercased — album resolution 502s.
Why: Spotify ids are case-sensitive base62;
account_socials → socials.profile_urlrows likeopen.spotify.com/artist/1c22gxh30ijlofxhflz9df(Elvis Crespo) fail the albums proxy (502), while the true id1c22GXH30ijlOfXhfLz9Dfresolves. Found 2026-07-06; recovered all 10 by name-searching the Spotify search proxy and matchinglower(id). Any feature resolving artist → Spotify by stored id breaks for these rows.Fix: find where profile URLs get lowercased on write (likely a normalize step), stop case-flattening the path segment, and backfill existing rows (the
lower(id)match trick makes the backfill mechanical). Note (2026-07-08 audit): api#768's socials-at-ingest is case-preserved, fixing the forward path for funnel-attached artists — this item narrows to the normalize-on-write culprit elsewhere + the backfill of existing lowercased rows.Done when: stored artist Spotify ids resolve against the albums proxy; a spot-check SQL shows no all-lowercase artist ids that fail resolution.
Why: the hero only lights up for funnel-claimed catalogs. Verified 2026-07-06 on a real existing account with a 15-artist roster: zero of those artists had any rows in
songs/song_artists— the songs graph is only populated by the valuation/measurement pipeline, so every pre-funnel account sees an empty homepage forever unless something backfills their roster's music.Prototype (ran manually today, works): for each roster artist — Spotify id from
account_socials→ albums via the auth-freeGET /api/spotify/artist/albumsproxy →POST /api/research/measurement-jobs {source:"current", scope:{album_ids}}→ add captured ISRCs to the account catalog viaPOST /api/catalogs/songs. 15 artists / 880 releases accepted without hitting the per-org job cap.Design questions to settle: trigger (on artist-add? on first login? backfill cron for existing accounts?), cost control (880 releases for one account — cap per artist? albums-only vs singles?), catalog shape (one account catalog vs per-artist catalogs — interacts with the artist-scoped hero), idempotency/refresh cadence (re-measure for the value-over-time delta), and ownership semantics (roster artist ≠ owned catalog; see the diligence-buyer persona).
Done when: a design note on this issue answers the questions above and links a follow-up implementation tracker.
api P1:— believed FIXED by the v2 SQL aggregates (api#763, merged 2026-07-07): the band is now computed by a whole-scope RPC, not a row-limited read. Pending one verify-and-close (audit note 2026-07-08): authedGET /api/catalogs/measurementssilently caps at 1,000 rowsGET /api/catalogs/7d3e5c35…/measurements?limit=1should reportmeasured_song_count2,679 and a band consistent with 16.31B streams; then move this to Done.total_streams+ the band over the truncated set — no error, no flag; the chat hero literally renders "1000 tracks measured". (repro + numbers).range()loop) before dedupe/derivation; RED test with a >1,000-song fixture.[TEST]roster catalog (7d3e5c35…) returns 2,679 measurements and a band consistent with 16.31B streams; the hero count matches the SQL count.chat: the hero's client-side artist match only samples page 1 (50 songs)— RESOLVED by the v2 server-sideartist_account_idfilter shipped in chat#1852:lib/home/isArtistCatalogMatch.tsno longer exists on chatmain(verified 2026-07-08) and the 1852 Done note's artist-switching pass (Ana Bárbara $8.1M / Apache $6M, exact to SQL) covers the acceptance. Move to Done on next grooming pass:artistNamefilter repair above makes one filteredpageSize=1call correct again; swapisArtistCatalogMatchback to it once fixed (the code notes this).Open — video/experience parity (launch content vs. prod, added 2026-07-07)
api + chat: homepage run rows all read "Scheduled Task" — surface the real task title. (→ docs#268 · api#764 · chat#1857)
GET /api/tasks/runsexposes onlytaskIdentifier(all runs share the generic runner identity), sogetTaskDisplayNamehas nothing better to render. Same underlying gap as the run → sent-email linkage (chat#1832).scheduled_actions.titleonto each run inGET /api/tasks/runs; then chat renders it inHomeRunRowwith the current label as fallback.marketing: one valuation derivation — golden-fixture guard shipped, shared package deferred. (→ api#765 · marketing#44 · design note)
marketing/lib/valuation/computeCatalogValuation.ts(runValuationFlow.tsstill computes client-side). Two implementations of one formula will eventually diverge — and the divergence would land exactly on the number we told customers cannot drift.GET /api/catalogs/{catalogId}/measurementsonce a catalog exists, or both consumers import one shared derivation package; pick in a design note (interacts with the progressive-measurement fix above — partial in-card state still needs client math).chat: starter-task card — video-consistent template, content included. (→ chat#1856; template
ad0a233b…live in prod /agents — audit found the old preferred template's prompt had NO valuation at all, so this fixes content, not just wording)findStarterTemplateprefers "Weekly Performance Dashboard"). A viewer arriving from the video hits a differently-named starter. Low priority — but the fresh-account empty state is exactly the surface every video-driven signup lands on, and it's still unverified live (blocked on the Privy test-app cap item above for previews; prod fresh-account pass pending).Open — chat homepage: Catalog HQ (in merge order; each PR user-testable)
Open — marketing polish (independent)
Valuation card: collapse the
$0release tail. (→ marketing#42)$0rows; the collapsed row states the count.Marketing header: signed-in state + logout. (→ marketing#43)
recoupable.devand can log out; signed-out visitors see the current CTA pair.Architecture decisions
song_measurements), computed server-side in the new endpoint so marketing and chat share one formula. Revisit only if we need historical bands the snapshots can't reproduce./catalogs/{id}), per the Close the valuation → onboarding loop: cross-subdomain Privy auth + materialize valuation as catalog + authenticated /catalogs handoff #1801 decision — the homepage hero serves return visits. Revisit after the hero ships (if home shows the same valuation, the deep link may become redundant); any change gets a dated callout here.api.recoupable.devendpoints — no direct Supabase reads from chat (repo standard:lib/supabase/[table]/[fn].tsserver-side only, API for cross-app data).Source references
.devmigration; Privy allowed-origins fix only), chat#1803 (open —/catalogs/[id]landing polish; shares the measurements-endpoint dependency).chat/app/page.tsx→components/Home/HomePage.tsx, greetingcomponents/Chat/ChatGreeting.tsx; tasks UIchat/app/tasks/*,chat/lib/tasks/*.POST /api/catalogsmaterialization (api#677),GET /api/catalogs/songs,GET /api/tasks/runs(see chat#1818), measurement jobsapi/lib/research/measurement_jobs/*, tablescatalogs,catalog_songs,playcount_snapshots,song_measurements.marketing/components/valuation/*,marketing/hooks/useGetFullReport.ts,marketing/lib/config.ts(appUrl).cm4t193tx03w553u94ib9ucnh; Close the valuation → onboarding loop: cross-subdomain Privy auth + materialize valuation as catalog + authenticated /catalogs handoff #1801 Phase 1b closure note documents the cookie + DNS procedure end-to-end.