mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
e81e79e5f2
Adds two panels to the Audience analytics tab: what share of a creator's followers were active in the last 30/60/100 days, and where those followers are, as a doughnut. Requested in #civitai-testers — total follower counts say little when accounts go dormant. Both answers are a per-user lookup over the creator's follower ids. Reading them from the source tables is not affordable: pageViews is 4.58B rows sorted by (time, pageId, userId), so a follower-id filter cannot skip granules — measured 5.59 GiB / 2.19 s per 10,000-id chunk over 100 days, and a 53,304-id follower set needs six chunks. So this adds default.user_activity_rollup, a per-user last-seen + last-known-country table keyed by userId, against which the same probe is a primary-key seek. The rollup merges on max/argMax, both idempotent, so the refresh job re-scans an overlapping window instead of tracking a watermark and a catch-up after a missed run cannot double anything. That is a property of the engine choice, not of rollups generally — the migration says so next to a warning against adding a count-shaped column. DDL AND BACKFILL ARE HAND-APPLIED, and must land before this deploys: src/server/clickhouse/migrations/2026-09-04-user-activity-rollup.sql. Disclosure rules are enforced server-side, in redactReach and presentReach, not in the component: SvelteKit serialises the whole load return into __sveltekit_data and __data.json, so a template-side filter still publishes "one follower in NO" to View Source. Panels are withheld under 25 followers; countries under 5 followers fold into "Other". An empty or stale rollup is treated as an outage, not as data. Every follower without a rollup row counts dormant, so an unbackfilled table would otherwise render a confident "0.0% active" — assertRollupUsable refuses a table that is empty or more than 12 hours behind, and the page says "unavailable" instead. Also: - register Chart.js DoughnutController + ArcElement in the shared @civitai/ui Chart, which carried line and bar only, and export CHART_PALETTE_SIZE so the slice cap cannot drift out of step with the palette - correct analytics.ts's claim that UserEngagement has no usable index for "everyone who follows X"; prod carries UserEngagement_type_targetUserId_idx INCLUDE (userId), and this change depends on it - docs: six lines across three creator-studio page specs said @civitai/ui has no chart primitive, contradicting the checklist's own C1 entry; the svelte-app-standard rule sending ClickHouse roll-ups behind /api/* did not match any analytics tab, all of which read them in load Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 KiB
12 KiB
Creator Studio — implementation checklist
Single source of truth for status. Everything open — build items, decisions, blockers, bugs, deferred work — is tracked here. Detail and rationale live in the reference docs (linked inline); this file is the index of what's left, not the why.
Reference: page specs (dashboard.md, models.md, analytics.md, earnings.md, licensing.md, settings.md,
join.md) · pre-implementation-decisions.md (A/B/C decision log) ·
owner-rollup-handoff.md + cdc-koen.md (A1) ·
questions-*.md (answered Q&A) · feedback-justin-round-2.md
(review notes).
Legend: [x] done · [ ] not started · [~] partial · 🚧 blocked on a dep · 🟢 needs a decision ·
⏭ deferred / lower-priority · 🐛 flagged bug (upstream, not ours to fix).
Shipped
Shared / shell
- App scaffold (SvelteKit spoke), auth gate, nav, layout, favicon
- Membership resolver (Creator Program gate, B1) + moderator test simulator (
cs-test-membershipcookie) - Monetization module —
setLicensingFee/bulkSetLicensingFee/ apply-default-by-type (kysely); early-access write client → main-app REST endpointPOST /api/v1/model-versions/early-access - Axiom logging (
handleErrorhook) - Redis read-through cache —
@civitai/rediscreateRedisCacheBuilder/createSysRedisCacheBuilder→ spokecache.ts; range-scaled TTL - Charting primitive (C1) — Chart.js
Chartwrapper in@civitai/ui(SSR-safe;chartColor()theme bridge) @civitai/buzzwired (shim +BUZZ_ENDPOINT) — authoritative buzz-account reads (cash), not ClickHouse- Content/creator analytics reads module (
analytics.ts, userId-keyed) + earnings/cash reads (earnings.ts,cash.ts) - Empty / unavailable / non-CP state hardening across dashboard,
/earnings,/analytics
/models — Model management ⭐
- Grouped table (models → versions, drafts included); search / fee filter / sort / pagination (URL-driven)
- Set / adjust / clear licensing fee — single + inline, fractional (0.01), CP-gated; status Off/Active/Paused
- Bulk fee editing (
?mode=bulk) — per-version + select-all, confirm dialog; apply-default-by-type - Non-commercial base-model guard; ownership + CP gate re-checked server-side
- Full early/paid-access config editor (B12, full parity) — per-version drawer → main-app endpoint
- States: empty, filtered-empty, non-member notice, error toasts
/ — Dashboard
- Shell: headline stat cards + section link cards + member badge
- Content-activity row (30d reactions/followers/images/posts/profile views,
getContentTotals) - Buzz earned (30d) + CP cash cards (ready / pending / withdrawn, authoritative via
@civitai/buzz); cash cards gated to CP members
/earnings — Earnings by source (A1 Part 1)
- By-source breakdown (comp / license / tip / access / cosmetic) — reads
buzzTransactionsdirectly (already owner-keyed);licenseFee+'27'filter, early-access prefix, cosmeticsell - Per-currency faithful display (B8/D1, no conversion/merge) — currency cards + source×currency table
- Buzz-only per-currency trend chart (real buzz colors); Redis-cached; 7/30/90d + day/week controls
- CP cash panel — authoritative ready / pending / withdrawn (buzz service +
CashWithdrawal), USD (cents÷100), matches the Buzz dashboard; Buzz Dashboard link-out (C6: one cash home)
/analytics — Basic analytics
- Content/creator section (B4b): reactions/followers/images/posts/profile views over time + top-images; Redis-cached
- Date-range control — 7/30/90d presets + day/week granularity (URL-driven)
- Zero-activity + unavailable empty states
- Audience tab — follower active reach (share of followers active in the last 30/60/100 days) +
follower country doughnut.
getFollowerReach($lib/server/follower-reach.ts) probes the newdefault.user_activity_rollup(per-user last-seen + last-known country) with the creator's follower ids from Postgres. Refreshed by theuser-activity-rollupcron (*/30,src/server/jobs/user-activity-rollup.ts); DDL + backfill are hand-applied —src/server/clickhouse/migrations/2026-09-04-user-activity-rollup.sql, and they must land before the job or the page deploys. Panels are suppressed under 25 followers and countries under 5 followers are folded into "Other" (disclosure rule, enforced server-side inredactReach); an empty or >12h-stale rollup renders "unavailable" rather than a confident 0%. - (route moved
/earnings/analytics→/analytics)
/settings — Payout & settings
- Membership / tier status card; Payout (Tipalti) status card
- Payout unlock (#16) — "Set up payouts" prompt unlocks once settled cash ≥ $50 (
getCreatorCash) - Fee defaults — read-only info per B9 (becomes a link to the templates list once pricing-templates.md ships)
/join — Membership upsell
- CP-framed upsell (B1), capability comparison, CTA; CP-member redirect to
/; reusableJoinUpsell; nav aligned to CP membership
Open — needs a product decision (Justin)
- 🟢 #23 — Early-access reframing. Pre-check answered — the backend already enforces manage-only.
mergeEarlyAccessConfigUpdate(model-version.service.ts:341) throws "You cannot add early access on a model after it has been published"; on a published version with existing EA you can only loosen terms (no price↑, no timeframe↑, no donation-goal change). So enabling-on-published is impossible server-side — the studio drawer currently lets you try and eats a 400. Remaining work is UX in/models(disable "enable EA" for published-without-config versions; allow manage-only), not a product decision. (feedback #23) - 🟢 B13 — Publish / schedule a version in v1? Recommended default: fast-follow (not v1). (decisions B13)
Blocked — backend / main-app dependency
- 🚧 A1 Part 2 — owner-keyed
modelVersionId→ownerUserIddictionary (Koen / CDC). Unblocks per-model earnings,/analyticsmodel section (generations/downloads/top-models), and the dashboard top-earning-models widget. Ask is written: cdc-koen.md. In-spoke fallback is viable now (Postgres version-ids →IN()over pre-aggregated daily MVs; ~820ms for a 349-version creator, capped for mega-creators) — see "Deferred" below. - 🚧 A4 — Sell access indefinitely. Reuse early-access uncapped; needs the main-app representation (nullable
timeframe/indefiniteflag). Write path already exists (the B12 endpoint). (decisions A4) - 🚧
licenseFeetype is'27'(ingest bug, Justin owns). We filtertype IN ('licenseFee','27')until the MV fix + backfill lands, then drop'27'. (owner-rollup-handoff §🔴) - 🚧 B4 — owner-keyed daily SummingMergeTree MVs (perf, not correctness). The content-analytics 90-day reads scan raw event tables (cached, but heavy). Backend MVs would remove the raw-scan load. Not blocking any feature. (feedback #10)
Flagged bugs — upstream (shape the numbers, not ours to fix)
- 🐛 Access sales always credit yellow — confirmed bug; fix is forward-only, historical rows stay yellow. (owner-rollup-handoff §D1)
- 🐛 Cosmetic creator payouts are best-effort — a failed payout leaves no row, so
/earningscan under-report cosmetic revenue with no signal. (owner-rollup-handoff §payment-path bugs) - 🐛 ClickHouse
buzzTransactionsmirror gap — CH showed a pending-cash balance the buzz service didn't; flag to whoever owns the buzz→CH sync. Doesn't affect the studio (cash reads go to the buzz service).
Deferred / unblocked builds (buildable when prioritized)
- 🚧 Pricing templates — creator-authored fee + paid-access defaults targeting model type × base model/ecosystem, pre-filling the main-app version form. Plan: pricing-templates.md. Resolves #17; needs no product decision (see the B9 note there). A per-type localStorage stopgap already ships in the main app and is retired by phase 4 of that plan. Blocked twice over: on paid-access unification (templates store a gate, and its shape is changing), and on a deferred decision about the targeting axis — Justin proposed creator-named price tags instead of model-type × base-model targeting, and that review waits on the
DonationGoalkeep-or-drop question. See Price tags. - 🟢 Paid access: discounts + a free-unlock pledge — reconcile early access and permanent paid access into one system, with a scheduled discount ladder and an irrevocable free-unlock pledge. Plan: paid-access-decay.md — structure settled 2026-09-02 (17 questions answered, nothing schema-shaped open), then reopened 2026-09-03 by a creator thread on early-access-to-paid-access switching — Q18 and Q19 were superseded 2026-09-03 by donation-goals.md, which answers the donation-goal half differently: a met goal makes a model free to everyone for 30 days, not permanently, and 30 days is now the only guaranteed free — which invalidates this doc's permanent
PaidAccessGuaranteeand is tracked as D1. Q20 is still open. Escrow was explored and parked.DonationGoalmay be dropped entirely pending Justin's decision, which would remove three of the four rules. Blocks pricing templates until built. - ⏭ Model analytics via the in-spoke fallback — Postgres version-ids →
IN()overdaily_resource_generation_counts/daily_downloads/buzz_resource_compensation, capped by version count, Redis-cached. Ships per-model usage/earnings + top-models for small/moderate creators now; swap the read to the A1 dictionary when it lands (UI/contract unchanged). (Scoped + measured; paused by request.) - ⏭ #24 — Bulk "select all matching" + base-model filter (
/models) — highest-value remaining bulk-fee build. (feedback #24) - #11 — Synchronized crosshair across charts — DONE: added a
pluginsprop to the@civitai/uiChartwrapper + acreateSyncedCrosshair()plugin (shared hover index across charts sharing a date axis); wired across the/analyticscharts. - ⏭ #4 — Dashboard charts — low priority, no spec (Justin: "low priority, deferred";
dashboard.mdonly floats an earnings-summary sparkline, not content). A reactions chart was tried and reverted — wrong metric for an earnings/at-a-glance surface. If revisited, do a buzz-earned sparkline (viagetEarningsSeries). - Lifetime "total comments received" stat — DONE:
getAllTimeTotalssurfaces all-time reactions + comments as a context line on/analytics(the one place comments appear — no fast period-scoped source). Comments come from Postgres (Thread.commentCountover the image's thread and its replies); theimage_metrics_userrollup it originally read is dead and returned 0 for every creator newer than userId 9.66M. ClickHouse has no correct substitute — see the note ongetAllTimeTotalsbefore reaching for thecommentstable, whoseentityIdis a comment id, not an image id. - ⏭ Access-config drawer URL-addressable (
?version=shallow routing) —/modelspolish.