mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
fd1a5009e71701f16163db9021414e7968a73b8e
1176 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6b58d1fa89 |
fix(models): cascade nsfw flip to version rollups (#2288)
When Model.nsfw was true, every ModelVersion.nsfwLevel was stamped to nsfwBrowsingLevelsFlag (60 = R|X|XXX|Blocked). The old Model trigger only enqueued a Model-level UpdateNsfwLevel job on nsfw flip, so after a true->false flip the versions stayed at 60. updateModelNsfwLevels then bit_or'd the stale 60s back to 60 and the WHERE clause found no diff — model frozen, hidden from .com search. Trigger now enqueues a ModelVersion UpdateNsfwLevel job for every Published version under the model whenever Model.nsfw changes. The update-nsfw-levels cron processes versions before models in the same tick (see updateNsfwLevels batch order), so the rollup reads fresh version data. Also swapped the prior `!=` comparison to IS DISTINCT FROM for NULL-safety. Backfill: a hidden debug endpoint at src/pages/api/testing/backfill-stale-nsfw-rollups.ts (guarded by WEBHOOK_TOKEN) enqueues UpdateNsfwLevel jobs for the existing stuck cohort (~3,198 models / ~5,264 versions). Supports count, enqueue (with dryRun / limit / modelId), and verify actions. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a5e252c033 |
fix(model-file-scan): correct HiDream-O1 baseModel to match ecosystem key (#2289)
Training-completion code wrote `ModelVersion.baseModel = 'HiDream O1'`
(with space) since the ecosystem shipped on 2026-05-13. stringifyAIR
hands the string to getRootEcosystem(), which throws (canonical key is
`HiDream-O1` with hyphen). The try/catch silently keeps the raw string,
lowercases it, and emits `urn:air:hidream o1:lora:civitai:...` to the
orchestrator. AIR segments are colon-separated and don't allow spaces;
the orchestrator's URN parser falls back to `unknown:unknown:...` and
the scan workflow fails with HTTP 400 ("Resource ... does not exist or
is not valid.").
100% of HiDream-O1 scan submissions have been failing since 2026-05-13
under the orchestrator-only scan path (Phase 3 removed the legacy
HTTP fallback on 2026-05-11). Backfill the persisted rows so the
scan-files-fallback job (every 5 min) picks them up on its next tick
and resubmits with the corrected baseModel.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
b03829a8a0 |
feat(oauth): require Origin allowlist for public clients (#2285)
* feat(oauth): require Origin allowlist for public clients Public OAuth clients (isConfidential=false) currently rely on PKCE alone for token-exchange identity. This adds an `allowedOrigins` field to OauthClient and enforces it on /api/auth/oauth/token and /revoke for public clients. CORS becomes per-origin instead of `*` when the request matches a registered origin. Confidential clients are unchanged — `client_secret` remains the auth boundary so they keep the existing wildcard CORS. The migration backfills existing OauthClient rows with allowedOrigins derived from the origin part of their redirectUris, so the rollout is non-breaking for any currently-registered application. Closes the residual code-interception window for browser-only PWA clients ahead of the civitai-app-starters static-PWA M7 work. * fix(oauth): always GET-redirect from consent approval Next.js's res.redirect() defaults to a 307 status, which preserves the HTTP method on the follow-up request. The consent page POSTs to /api/auth/oauth/authorize on Approve, so the 307 back to the registered redirect_uri caused the browser to re-POST the code/state to the third- party callback. Standard OAuth clients only define GET on their callback URL per RFC 6749 §4.1.2 and respond 405 to that POST — which broke first-time consent in e2e against next-app while second-time consent (skipped UI, original GET preserved) still worked. Switch all three redirects in the authorize handler to an explicit 303 so the browser always downgrades to GET, matching the spec regardless of how the request arrived. * refactor(oauth): single-lookup origin enforcement Move per-client origin enforcement into oauthModel.getClient so the token endpoint does one DB lookup instead of two. The model stashes the resolved client on the OAuth library's Request so the handler can drive CORS off it without re-querying. Bypass for the /authorize flow gated on body.grant_type (only set on /token + /revoke); native PKCE clients with no Origin header are allowed through (lets one OAuth client back both a browser SPA and a mobile app with a single consent). Other changes from PR review: - Migration: NOT NULL DEFAULT, normalized backfill (lowercase host, scheme-aware default-port stripping, [^/?#]+ to stop at query/fragment, ~* case-insensitive scheme match). - deriveAllowedOriginsFromRedirectUris filters to http/https. - Fail-closed fallback dbRead lookup if the library success path doesn't stash a client. - Rate-limit /token by IP instead of clientId (random-id rotation was bypassing the bucket and driving model-layer lookups). - Collapse /revoke into one findUnique and rate-limit before the origin check. - Drop Access-Control-Allow-Credentials from public-client CORS (Bearer tokens, no cookies). Adds OriginNotAllowedError sentinel + dedicated tests for oauthModel.getClient covering the new gating, plus a fail-closed fallback test for the /token handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(oauth): defense-in-depth origin check + diagnostic logging - /token success path: verify request Origin is in attached.allowedOrigins before echoing per-origin CORS. Redundant with oauthModel.getClient's check on the normal path but closes the fail-closed fallback path so an unverified Origin can never be echoed (addresses CodeQL CORS-misconfiguration alert + Copilot defense-in-depth comment). - /revoke: same allowlist-includes guard documented inline (logic was already there, comment now explicit). - /token catch: console.error the swallowed error so a 500 surfaces in dev/prod logs with a stack trace instead of an opaque response. - /authorize: separate res.redirect(...) calls from the return statement. res.redirect returns the res object, and returning it from a Next.js Pages API handler triggers the "API handler should not return a value, received object" warning in dev. - oauthModel.getClient: correct misleading comment that claimed /revoke uses grant_type — it uses token/token_type_hint per RFC 7009 and doesn't go through the OAuth model at all. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7b59f2c6a6 |
feat(reports): add Spam report reason (#2283)
* feat(reports): add Spam report reason Adds Spam as a first-class ReportReason alongside the existing ViolationType.Spam (which remains for the mod-side unpublish flow). New reports land in the mod queue at status Pending and appear in the default reason filter. ClickUp 868jne7my. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(reports): inherit base details default on Spam schema Drop the details override on reportSpamSchema so the discriminated union picks up baseSchema's `details: baseDetailSchema.default({})`, matching the CSAM precedent. Without this, callers had to send an explicit empty `details` object for Spam reports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
07725d8d78 | updated xguard labels | ||
|
|
231e579fa5 | Merge remote-tracking branch 'origin/main' into feature/prompt-wildcards | ||
|
|
da1f0d4b23 |
wildcards: set-level nsfwLevel rollup + audit-driven classification
- WildcardSet.nsfwLevel column (folded into the original add_wildcard_sets
migration). Bitwise OR of every non-Dirty category's nsfwLevel; maintained
by recomputeWildcardSetAuditStatus so visibility checks ("does this set
have content fitting .com SFW vs .red NSFW?") are a single-table bitmask
test, no category sub-query.
- Audit pipeline classifies severity via XGuard. Two label sets per workflow:
hard-fail labels (csam, urine, diaper, scat, menstruation, bestiality) ->
Dirty; level label (nsfw for v1) -> NsfwLevel.R, else NsfwLevel.PG. v1
restricts to the binary nsfw evaluator because pg/pg13/r/x/xxx aren't
well-tuned for text yet -- the schema stays bitwise so re-introducing
finer levels later is code-only.
- Callback recomputes Dirty from per-label triggered flags rather than
trusting output.blocked, which would otherwise flip on triggered level
labels (ordinary NSFW content would be marked Dirty).
- getResourceData stamps wildcardSetId + overrides canGenerate for
Wildcards-type ModelVersions, using the set-level rollup for visibility.
Single-table query, no JOIN.
- GenerationResource.wildcardSetId optional field -- downstream callers
(form hydration, model detail page "Generate" handoff) read it to route
the id into snippets.wildcardSetIds rather than appending to resources[].
- Docs (prompt-snippets-v1.md, prompt-snippets-schema.md) updated to
describe the rollup, audit label sets, and "wildcards aren't generation
resources" routing model.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
51d29d8e8c | check in | ||
|
|
44275e48d1 |
Switches the ClickHouse audit log to AggregatingMergeTree so duplicate
scans collapse into one decision-grain row with occurrences summed. Mod review tables collapse to a single `ScannerLabelReview` keyed by the same three columns — verdict once, covers all future identical scans. - policyHash -> version end-to-end (column + workflow metadata) - writer computes contentHash from scan input; standalone recordXGuardScan dropped in favor of the workflow helper - queue + detail queries use GROUP BY with explicit aggregates in HAVING/ORDER BY + SETTINGS prefer_column_name_to_alias = 1 for partition-pruning WHERE on lastSeenAt - /moderator/scanner-audit rebuilt around the new row unit with occurrence count + per-label verdict buttons - new docs/features/scanner-pending-migrations.md lists the Postgres migration, ClickHouse SQL, and Prisma regen step Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ef95bf8404 |
feat(image-scan): add AiRecognition + AnimeRecognition tag sources
Adds dedicated TagSource enum values for the new mediaRating classifier signals so downstream operational tagging (TagsOnImageDetails) can carry clean provenance for ai/anime tags rather than reusing Computed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9b27143ff1 |
feat(scanner-review): moderator review UI for scanner results
The consumer side of the scanner audit log. Mods FP-review triggered scans and FN-review near-miss scans; verdicts feed prompt tuning. - ScannerScanReview (scan-level completion marker) + ScannerReview (per-label TP/FP/TN/FN/Unsure) + ReviewVerdict enum, all joined to ClickHouse by workflowId (string, unenforced FK). - /moderator/scanner-audit page with Triggered / Near-miss tabs, scanner+label+policyVersion filters, paginated table, drawer detail with per-label verdict buttons + matched-terms display + Mark scan reviewed submit. CSV export pulls up to 50k rows for offline analysis. - scannerReview tRPC router: list / detail / upsertVerdict / deleteVerdict / submitReview / exportRows, all moderator-gated. Two-table design separates "this scan has been reviewed" from "this label had this verdict" so coverage tracking and disagreement spotting stay clean even across multiple mods on the same scan. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2f42cc972a | Merge remote-tracking branch 'origin/main' into feature/prompt-wildcards | ||
|
|
9dcc9e6cd4 |
feat(tips): add user/version flags to disable creator tips
Adds bitmask `flags Int` columns on User and ModelVersion. First bit (`DisableTips`) opts the user/version out of creator tips. The mini endpoint now returns a `creatorTips` boolean = neither flag set. Use cases: Civitai official account (user-level), licensed models (version-level). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
055e54f6e6 |
feat(oauth): scoped tokens, OAuth 2.0 server, per-subject buzz limits
Squashed merge of feature/scoped-tokens onto latest main.
OAuth 2.0 server (authorization code + PKCE, refresh, revoke, device
flow, OIDC discovery), bitwise TokenScope enum (25 flags) with a
fail-safe enforceTokenScope middleware (un-annotated procedures
default to requiring Full), 83 routers annotated, 15 buzz-spending
procedures gated with blockApiKeys: true.
Per-subject buzz limits: opaque (type, id) subject pair,
BuzzBudget[] shape supporting absolute/sliding/rollover variants
with optional currency filters, stored on ApiKey.buzzLimit
(User-type keys) or OauthConsent.buzzLimit (OAuth grants — stable
across access-token rotations). Civitai stores limits + busts
cache + cleans up subjects via /v1/manager/users/:userId/{auth,
limits/auth}/:type/:id; orchestrator owns enforcement and
rolling-window math.
Account UI: card-based ApiKeys, OAuthApps, ConnectedApps surfaces
with inline spend bars + a shared EditBuzzLimitModal. OAuth consent
screen collects an optional buzz limit when AIServicesWrite is
requested. OAuth Apps + Connected Apps gated behind the
`oauth-apps` Flipt flag (mod-only). Audit via the existing
ClickHouse `actions` table (BuzzLimit_Set ActionType).
DB: one new migration 20260507165710_add_buzz_limit_to_oauth_consent
adds OauthConsent.buzzLimit JSONB. Legacy KeyScope[] column drop is
deferred to a follow-up PR after this is stable in prod.
Demo client: civitai/civitai-oauth-demo (separate repo).
Conflict resolution during the rebase onto main: kept main's newer
multi-image candidate handling in comics.router.ts (it was a
substantive content divergence, not a metadata conflict; the
blockApiKeys: true annotation on purchaseChapterAccess was already
preserved through the non-conflicting merge regions). Prisma types
regenerated post-merge.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
af3f086b71 | added wildcard category auditing | ||
|
|
95d84c2526 | WildcardSet services | ||
|
|
8f5d58d139 | job to scan wildcard models to create new wildcard sets | ||
|
|
c61819f6cb | Merge branch 'main' of github.com:civitai/civitai | ||
|
|
ca915ed55b |
Gate mature comic-panel generations behind explicit unlock
* Stop persisting orchestrator blurred previews on the panel — mature
outputs (single-image OR per-candidate slots) now sit in a new
ComicPanelStatus.RequireUnlock state with markers in
metadata; the panel's imageUrl/imageId are never written for blocked
results, closing the leak where the CDN link could be discovered
through any surface that read panel.imageUrl.
* Derive blockedReason via formatGenerationResponse2 + WorkflowData
rather than reading raw orchestrator output, so domain/nsfwEnabled/
allowMatureContent gating produces the same /
reasons the rest of the app relies on.
* unlockPanelGeneration now lifts the workflow restriction at the
orchestrator AND inline-downloads the now-clean output(s), updating
metadata in one round-trip — no waiting for a poll tick.
* CTA is strictly domain-based: green redirects to civitai.red
(server-side mutation also rejects on green); red shows the in-place
yellow-Buzz unlock. The per-tile reveal in the candidate picker is
also gated this way so the Show button isn't a green-side backdoor.
* Capture per-candidate orchestrator nsfwLevel so the picker blurs
clean-but-mature outputs by default, with selection/zoom disabled
until the user explicitly reveals.
|
||
|
|
1b57f46003 |
feat(licensing-fees): per-image licensing fees on ModelVersion (#2240)
* feat(model-version): add licensing fee columns * feat(model-version): expose fee on mini endpoint * feat(model-version): licensing rate editor UI behind Flipt flag * feat(buzz): daily license fee payout job * feat(buzz-dashboard): license earnings chart segment * chore: prettier format * fix(model-version): include licensing fee fields in edit selector * refactor(buzz): fold license fee payout into creator-comp job * refactor(buzz): inline source filter in compensation query * fix(model-version): keep editor visible when existing fee set |
||
|
|
2978847888 | initial migration | ||
|
|
25696fb4f4 |
fix(models): clamp lastVersionAt to non-future Published versions (#2231)
* fix(models): clamp lastVersionAt to non-future Published versions updateModelLastVersionAt picked the latest "status=Published, publishedAt not null" version and wrote that publishedAt straight into Model.lastVersionAt with no upper bound. If a Published row ever held a future publishedAt (e.g. a Scheduled→Published edit that left publishedAt untouched, then an auto-unpublish demoted it back to Draft without the corrector re-running), the future date stuck on Model.lastVersionAt and the prod-side trg_sync_model_to_metric trigger mirrored it into ModelMetric.lastVersionAt, pinning the model to the top of the Newest feed (sorted by mm."lastVersionAt" DESC) with the "Updated" badge active until the future date arrived. Add `lte: new Date()` to the lookup so future publishedAt values can never seed lastVersionAt. The backfill migration repairs any currently-poisoned rows; trg_sync_model_to_metric handles the propagation into ModelMetric. ClickUp: 868jfgvnm Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(models): tighten sync_model_to_metric trigger Two changes to the prod-side Model→ModelMetric sync trigger: 1. Refuse to propagate a future Model.lastVersionAt into ModelMetric. Defense in depth around updateModelLastVersionAt's lte:NOW guard — even if some other path writes a future Model.lastVersionAt (manual SQL fix, admin script, future code regression), the feed table mm.lastVersionAt stays clean and the model can't pin to the top of the Newest feed. 2. Split AFTER INSERT OR UPDATE into two triggers so the UPDATE half carries a WHEN (...) clause and only fires when a synced column actually changed. Stops ModelMetric write churn from every unrelated Model UPDATE; matches the trg_sync_model_to_base_model_metric pattern. Idempotent: function uses CREATE OR REPLACE, triggers are dropped with IF EXISTS before being recreated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4b000523bc |
feat(model-version): add ExternalGeneration for fileless API-backed models (#2217)
* feat(model-version): add ExternalGeneration usage control for fileless API-backed models Lets moderators publish file-less model versions (e.g. NanoBanana, Seedream, ChatGPT-Images) that are routed via dedicated external-engine UIs. The new ModelUsageControl.ExternalGeneration value is mod-only to set, skips the wizard's file-upload step, is excluded from the reset-to-draft cron, and is auto-covered by the GenerationCoverage view so canGenerate and model-page badges work for all users without a manual EcosystemCheckpoints insert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(generation-coverage): guard ExternalGeneration branch with NOT m.poi Defense-in-depth from Copilot review: prevents a mod from silently making a PoI-flagged model generatable by flipping usageControl=ExternalGeneration. Mirrors the safeguard already in the catch-all branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(model-version-wizard): respect usageControl in edit-mode submit The previous editing branch returned early via goNext() before checking usageControl, so saving an existing version (or re-submitting after the wizard re-mounted in edit mode) always advanced to step 2 (Files) even when usageControl was ExternalGeneration. Unify the two paths to always navigate by URL with shallow routing when editing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(model-wizard): skip files step for ExternalGeneration in full-model wizard Mirror the ExternalGeneration carve-out from ModelVersionWizard: - Step 2 (version) submit jumps to step 4 (post) instead of step 3 (files) when usageControl is ExternalGeneration. - The auto-redirect useEffect respects the same skip when no version has files yet but the version is mod-published as ExternalGeneration. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(wizard): hide Files step from stepper for ExternalGeneration versions For mod-published file-less versions, the Files step is irrelevant — instead of just auto-skipping past it, omit it from the rendered Stepper entirely so the UI matches the actual flow (Edit version → Create post for the version wizard; Model info → Version info → Create post for the new-model wizard). URL step semantics stay stable (Files is still URL step 2 / 3), so direct links keep working. The Stepper's active index and onStepClick handler translate between URL step and rendered child position when the Files child is omitted. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f741b86db4 |
feat(home-blocks): add FeaturedCollections type + mod curation
New HomeBlockType that expands one system HomeBlock (userId=-1) into N randomly-picked collection sections on the homepage, each with a curator-attributed header. Pool of collection IDs lives in metadata and is mod-curated. Mod surface: - "Feature on homepage" toggle in collection context menu - /moderator/home-blocks/featured-collections admin page - "Featured Collections" entry in the moderator nav menu Safety rails: - Hourly refresh-featured-collections-eligibility cron writes a consolidated Redis state blob with per-pool recent-item counts and name-drift detection (threshold: >=3 accepted items/5d AND name matches approved snapshot) - Hydration reads eligibility from Redis, falls back to full pool on Redis miss, returns null when the job ran and nothing qualifies - Collection name snapshot captured on add; hydration drops the collection until a mod re-approves via the admin UI - sfwBrowsingLevelsFlag applied server-side so the block is always PG/PG-13 regardless of user preference - Picks whose post-filter items are empty are dropped so a pool entry of all-R/X items never renders an empty section - Ghost entries (deleted/missing collections) surface in admin UI with a remove action - Clone user blocks stay untouched on pool mutation; runtime reads pool state from the source block via sourceId - Zod + runtime clamps on limit/rows/renderCount/staleDays Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
aa1922b4ba |
feat(referral): spend-triggered referral program v2 (#2178)
* feat(referral): add spend-triggered referral program v2 Referrers earn Tokens (1/2/3 per Bronze/Silver/Gold membership month paid, capped at 3 months per referee) + 10% Blue Buzz kickback on referee yellow Buzz purchases. Referees get 25% of tier monthlyBuzz as Blue Buzz on first paid membership. 7-day settlement window with chargeback revoke. Tokens redeemable in shop for perk-only CustomerSubscription grants (no buzz stipend, no tier badge). Gated by Flipt flag referral-program-v2. - Prisma: ReferralReward, ReferralMilestone, ReferralRedemption models - UserReferral.firstPaidAt + paidMonthCount for cap tracking - referral.service.ts handles earning, settlement, clawback, milestones - Stripe webhook hooks manageInvoicePaid + completeStripeBuzzTransaction - charge.refunded + charge.dispute.created webhooks for clawback - Cookie TTL 5d -> 30d, single auto code per user - referral:* signals (pending, settled, milestone, tier-granted, etc.) - /user/referrals dashboard with shop modal + activity feed - Checkout banner with manual code entry + bonus preview - Terms at src/static-content/referrals/terms.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(referral): address review feedback from code + gap analysis - Add @@unique([kind, sourceEventId]) + @@index([status, settledAt]) on ReferralReward. Replaces fetch-then-insert dedupe with unique-violation catch; closes idempotency race on webhook retries. - Make sourceEventId NOT NULL (always set in practice) - Fix settleRewardRow: move buzz grant before status flip and revert claim on buzz transaction failure. Prior version left Settled status after a post-commit rollback attempt. - revokeForChargeback now handles Settled rewards via negative (ChargeBack) createBuzzTransaction, not just Pending. - Chargeback webhook looks up the linked invoice for the PI so membership rewards (sourceEventId = invoice.id) are also revoked. - Enforce minReferrerAccountAgeDays in resolveReferrerForReferee. - getReferrerBalance: single groupBy query replaces 4 aggregates. - redeemTokens: FOR UPDATE lock on settled token rows; grant actual CustomerSubscription via findReferralProductForTier. Requires admin setup of Civitai-provider products flagged referralGrantable:true. - awardMilestones: deterministic sourceEventId (userId:threshold) and P2002 catch prevent double-award under concurrent settlement. - Referee bonus consolidated into the membership payment transaction. - ReferralCheckoutBanner now self-fetches per-tier monthly buzz via trpc.referral.getTierBonuses and displays the computed bonus. - trackCheckoutView moved to publicProcedure (anonymous guests can hit it). - logFraudEvent moved after dedupe checks so only real events log. - Constants: REWARD_DESCRIPTIONS table, promoted REFERRAL_SYSTEM_ACCOUNT_ID. Known gaps deferred to follow-ups: Paddle buzz kickback hook, referral:click emitter + dashboard stat, share buttons (Twitter/Reddit/ Discord), expiring-soon UI card, redemption-vs-paid-tier UI check, Top Affiliate cosmetic on 1M milestone. See review compile for full list. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): address Justin's second-pass feedback Key changes following review: - Schema source of truth: moved all referral additions from the generated schema.prisma to schema.full.prisma so db:generate regenerates correctly. - Referral grants use CustomerSubscription.buzzType='referral' (new distinct value) so they stack with paid yellow/green/blue subs via session-user's highest-tier-across-all aggregation. @@unique([userId, buzzType]) no longer blocks a user from holding paid + referral at the same time. On re-redeem, existing referral sub is extended (same/lower tier) or upgraded (higher tier). - Attribution logging moved from Axiom to Postgres. New ReferralAttribution table links each attribution event to UserReferralCode + payment metadata (Stripe PI id, invoice id, charge id, card fingerprint, IP). Indexes support "all events for code/card/IP" queries for mod review. - Stripe invoice + PI webhooks forward payment identifiers + card fingerprint into the attribution record. - Paddle webhook gets a clarifying deprecation comment; no new referral paths touch Paddle. - Notifications (persistent + email) added alongside signals for: - referral-reward-settled - referral-milestone-hit - referral-token-expiring (daily cron, deduped per-user-per-expiry-date) - referral-welcome-bonus (referee gets in-app + email thank-you) - /user/referrals dashboard gains Share-on-X, Share-on-Reddit, and Share-on-Discord buttons in the hero. - 1M-milestone Top Affiliate badge grants the most-recent cosmetic as a placeholder until a bespoke one is authored. - Design doc (referral-program.md) extended with overlap stacking approach, attribution + fraud detection plan, and notification matrix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): tier queue redemptions, no exploit path Mirrors the prepaid-membership pattern: each redemption becomes a tier-time chunk stored on CustomerSubscription.metadata.referralQueue. Chunks sort by tier DESC so Gold always activates before Bronze. Same-tier chunks collapse into a single entry so the metadata stays small. On redemption: pool the currently-active chunk's remaining days + existing queue + new chunk, sort/collapse, promote the top chunk to active, persist the rest as queue. Cheap-Bronze-stacked-to-Gold exploit is impossible because chunks never change tier — a Bronze chunk grants Bronze for its own days, then the next chunk runs on its own terms. New hourly cron advance-referral-subs: for referral subs past currentPeriodEnd with a non-empty queue, promote the next chunk; otherwise cancel the sub. Static modal copy updated to reflect the new behavior. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(referral): unit tests for queue, earning, chargeback, milestones 24 vitest cases covering: - collapseTierQueue (pure): sort order, same-tier collapse, zero-duration drops, and the "cheap Bronze stacked with one Gold" exploit is impossible - recordMembershipPaymentReward: no-bound-referrer skip, 3-month cap, first-payment referee bonus creation, subsequent-month no-bonus path, P2002 idempotency, min-referrer-age rejection - recordBuzzPurchaseKickback: skip when referee never paid, 10% rate calculation - revokeForChargeback: pending (no buzz clawback) vs settled (negative buzz txn with referral-clawback:<id> externalTransactionId) - awardMilestones: no-op on zero lifetime, awards only qualifying thresholds, swallows P2002 on duplicate milestone - advanceReferralSubscriptions: empty-queue cancels, non-empty promotes highest tier, missing referralGrantable product skips (no partial progress) Collapse exports collapseTierQueue for direct testing. DB clients mocked with deep shape for each model used. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): timeline progress bar on dashboard Adds a segmented progress bar to /user/referrals showing the user's queued tier-time chunks. Mirrors the prepaid membership timeline pattern: each chunk is colored by tier (gold/silver/bronze), annotated with tooltips showing its date range and days-in-tier, and the active chunk is visually highlighted. getDashboard now returns referralGrant (activeTier, period bounds, queue). New ReferralTimelineProgress component renders the segmented bar only when referralGrant is present. Nothing shown if user has no active referral sub. Answers Justin's visibility concern — users can now see "Gold for 14 days, then Bronze for 42" rather than guessing at metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): add review walkthrough checklist Section 0 lists pre-review setup: migrations, Civitai referral product inserts, Flipt flag, terms doc placeholders. Sections 1-6 walk the design docs, code-by-area, manual smoke tests, known gaps, design questions, and sign-off checklist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): lock effective date + governing law Effective Date: April 21, 2026. Governing Law: Delaware (matches main TOS Section 19.2). Flipt flag referral-program-v2 added upstream in civitai/flipt-state and enabled by default (commit f09d584 there). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referral): generic ReferralRedemption + flatten migrations Collapses three migrations into one (safe — none applied yet): - 20260420153258_add_referral_program_v2 (original tables) - 20260420170000_add_referral_attribution - 20260421120000_referral_redemption_generic (redemption refactor) New consolidated migration 20260421120000_add_referral_program_v2 captures the final target schema in one file. ReferralRedemption drops tier/durationDays/subscriptionId in favor of: - rewardType: ReferralRedemptionType enum ('MembershipPerks' only today) - metadata: JSONB with shape { tier, durationDays, subscriptionId } Leaves room for future redemption types (BuzzGrant, cosmetic, etc.) without another migration. Service + dashboard UI updated to read metadata. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(prepaid): exclude referral buzzType from prepaid cron Both the daily tier-transition sweep and the expired-cleanup pass filter by product.provider='Civitai'. Referral-granted subs (buzzType='referral') point to Civitai-provider referral products, so without this guard the prepaid cron would try to transition them using its prepaid-token logic and cancel them for having no tokens. advanceReferralSubscriptions owns those subs instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): link dashboard from buzz + membership pages ReferralCallout component with two variants: - full: gradient hero card for the Buzz Dashboard top-of-page - compact: slim row for the Membership page header Both flag-gated on features.referralProgramV2 so the card vanishes when the program is off. Links to /user/referrals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): hidden debug endpoint for UI experimentation POST /api/testing/referrals guarded by the WEBHOOK_TOKEN header (same pattern as /api/testing/strikes). Exposes a controlled surface for driving the referrals dashboard without paying real money, instead of reaching into the DB directly. Actions: dump, bind-code, grant-tokens, grant-blue-buzz, enqueue-chunk, simulate-membership-payment, simulate-buzz-purchase, simulate-chargeback, settle-all, advance-subs, expire-tokens, reset. settleImmediately=true on grant-* skips the 7-day wait. settle-all / advance-subs / expire-tokens fast-forward the relevant cron for one user. reset wipes a user back to a clean slate. Typical flow + curl examples documented in docs/features/referral-program.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): document debug endpoint usage * docs(testing): establish debug-endpoint convention + header doc Referrals debug endpoint now leads with a block comment enumerating each action, required params, and a one-line description. That header is the documentation — agents read the file directly instead of consulting a wrapper skill. CLAUDE.md gets a new section describing the convention so the same pattern propagates to future features. Rule is: drop <feature>.ts under src/pages/api/testing/, guard with WebhookEndpoint, lead with a block comment, scope destructive actions to a single userId per call. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(referral): 8 staged experiment scenarios + auth fix Review doc section 7 adds a phase-by-phase experiment plan Justin can walk through as the feature matures in review. Phase 1 seeded directly via DB (6 settled Bronze tokens + 500 Blue Buzz display row on user 1) so the dashboard walk-through works before this branch deploys. Testing endpoint header updated: auth is ?token= query param (reading from TokenSecuredEndpoint), not Authorization: Bearer. Doc + curl examples corrected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): A/B/C/D dashboard variants with switcher Page now delegates rendering to one of four variants, picked via ?v= query param. Parent owns data fetch, redeem mutation, shop modal; variants are dumb components taking a shared ReferralDashboardVariantProps. Variants: - A · Current — baseline minimal (extracted from original referrals.tsx) - B · Explainer — Buzz-dashboard-style narrative, how-it-works callouts, 4-step earn/spend breakdown - C · Gamified — level/rank badge by conversion count, milestone ladder as centerpiece, achievement feed framing for recent rewards - D · Funnel — share → click → signup → first-paid → loyal stages, stages we can't track yet labeled "coming soon" instead of fabricated metrics SegmentedControl at the top lets Justin flip between them side-by-side on the real dashboard to pick a direction. Switcher removes itself once a final variant is chosen. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): refined gamified dashboard Picked variant C as the baseline per Justin's feedback and applied the rework in one pass. All four variants + switcher removed. Key changes: - Recruiter rank driven by a unified score (conversions + lifetimeBlueBuzz/1000), mapped to named ranks Rookie / Recruit / Advocate / Champion / Legend at 0 / 1 / 10 / 50 / 200 points. Separates cleanly from the Blue Buzz milestone ladder (milestone bonuses still trigger purely on buzz thresholds). - Rank card: big name, progress bar to next rank, three supporting stats (paid referrals, lifetime Blue Buzz, recruiter score). - Milestone ladder: full formatted numbers, named milestones, Blue Buzz bolt icons throughout, per-row Unlocked/Up next/Locked badge, progress bar on the next-up row. Dismissible explainer alert (persists in localStorage). - How-it-works card: 4-step explainer pulled from variant B (Share → Earn Tokens → Earn Blue Buzz → Spend Tokens). - Token Bank: inline 6-offer grid replaces the modal. Redeem happens in place. Shop modal removed from the parent page. - Recent referrals (renamed from Recent activity): kickbacks and recruits with amount on the right under status badge, cap 10 with Load more. - Palette pared back to neutral dark accents + tier colors on the shop tiles only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referral): dashboard polish pass Addresses feedback from the first C rework: - Bring color back: tier palette on shop headers (bronze/silver/gold gradients), accent-colored rank card (Rookie gray / Recruit teal / Advocate blue / Champion grape / Legend yellow), blue for Blue Buzz, yellow for milestone trophies, green for Settled badges. - "Recruiter rank" -> "Your rank". Removed the redundant top-right rank badge. Rank name is colored by accent and paired with a trophy icon in a filled ThemeIcon. - How it works is now dismissible (localStorage), uses FeatureCard-style layout with gradient icon headers (Share blue, Earn Tokens teal, Earn Blue Buzz yellow, Spend grape). - Milestone explainer wording clarified: "Once a friend pays for any Membership with your code, every Buzz purchase they make earns you 10% back as Blue Buzz" — reflects the actual gate (firstPaidAt, no time window). Lifetime Blue Buzz promoted to a standalone pill with a large bold number and bolt icon, visible alongside the section title. - Milestone rows: trophy icon + yellow filled ThemeIcon when unlocked, blue light ThemeIcon when Up next, muted when locked. Bonus amount now called out in yellow with a bolt icon. - Token Bank: Spendable / Pending promoted to prominent tiles with icons (IconCoin / IconClock) and hint text ("Ready to redeem" / "Settles in 7 days"). Shop regrouped by tier — 3 tier cards, each containing the 14d and 30d options as rows instead of 6 flat tiles. - Recent referrals: big reward number with tier-colored icon per row. - A few iconography upgrades across stats (IconSparkles for Recruiter Score, IconRocket for "Next rank" label). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(referral): blue-buzz bonus color, filled bolt, rank stats as cards - Milestone bonus amounts + unlocked trophy/badge now blue (Blue Buzz color), not yellow. Yellow was misleading since bonuses pay in Blue Buzz, not yellow Buzz. - Swapped IconBolt -> IconBoltFilled across the dashboard. Civitai never uses the outline variant elsewhere. - Rank stats converted from a divider-separated left-aligned Group to a 3-column Grid of RankStatCard tiles (icon block + label + value), matching the Token Bank / milestone pill styling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referrals): rescale recruiter score for granular progress - 1 point per Blue Buzz, 1,000 per paid referral month - Thresholds: Rookie 0, Recruit 1k, Advocate 10k, Champion 50k, Legend 200k - 500 BB now shows halfway to Recruit instead of stuck at 0 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): restructure dashboard per feedback - Move How It Works above Rank card - Move referral timeline under Token Bank - Token Bank: tier-colored accent header (no dark text on gradient), drop meaningless sparkles icon, add divider between duration rows, keep button filled when affordable / outlined default when not - Token Bank subtitle: "Spend referral tokens on Membership perks." - Add dismissible info alert describing token earning mechanics - Migrate kickback + how-it-works + token-bank dismissals from localStorage to user-level dismissedAlerts (trpc.user.dismissAlert) - Remove icon on Recent referrals header (only card with one) - Simplify HowStep #4 copy (drop tier-queue phrasing) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): round 2 UI polish per feedback - Rank card: progress bar uses next-rank color so it's visible against the subtle backdrop (previously gray on gray) - Token Bank alert: switch color from orange to blue to match Blue Buzz milestone alert - Tier cards: full-width dividers between duration rows - Spendable/Pending tiles: drop hint text, add info tooltip (IconInfoCircle) next to label - Recent referrals: - Add "Settles {date}" for Pending rows - Blue Buzz amounts in blue + bolt icon (drop "Blue Buzz" text) - Token amounts show coin icon (drop "tokens" text) - Icon backgrounds match tier color (bronze/silver/gold) - Milestone bonus: bolt icon before amount (+[bolt]{num} bonus) - How It Works cards: FeatureCards-style gradient header + circular icon to match Buzz dashboard style - Timeline: move tier-queue note to bottom, style as crypto-deposit info row (IconInfoCircle + dimmed text) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): round 3 polish — premium code block + stats + spotlight - Code block redesigned crypto-deposit style: gradient accent bar, spotlight hover, monospace code in a chip with ActionIcon copy, gradient "Copy share link" button, subtle background wash, socials under a divider with uppercase section label - Blue Buzz milestones: replace lifetime-earned chip with Earned + Pending stat blocks matching rank card / token bank pattern - Unify RankStatCard + TokenTile into single StatBlock (optional tooltip) - Add spotlight hover effect to How It Works cards - Rewrite How It Works copy: shorter, less dense, especially step 2 which was number-heavy - Move Program Terms link to footer of the page Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): scope spotlight, relocate copy-link, add skeletons - HowStep cards: spotlight now confined to the colored header (was spanning the whole card) - Referral code block: drop the out-of-place top-right "Copy share link" button; move it into the Share It row as the first action (still gradient-styled, compact-sm to match siblings) - Rank card: remove the radial glow accent — not used elsewhere in the dashboard, looked out of place above the other cards - Replace loading spinner on /user/referrals with a full-dashboard skeleton (matches section structure: code, how-it-works, rank, milestones, token bank, recent referrals) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): tier perks popover, yellow tokens, premium rank card - Shop tier cards: spotlight hover on the Paper, info bobble next to the tier title that opens a Popover with PlanBenefitList (monthly Buzz + default perks filtered by tier/buzzType). On green servers the popover also links out to /pricing for full details. - Subscriptions/PlanBenefitList: rename default benefit "Generate mature content with Blue Buzz" → "Unrestricted generation with Blue Buzz" (appears on red/yellow servers only). Affects the referrals popover and the pricing page. - Unify token color to yellow across the dashboard: - Token Bank Spendable stat: green → yellow - Recent referrals token icon: orange-5 → yellow-5 - Shop card cost icon: default → yellow-5 - Redemption history reward format: "−1 tok" → "−[coin] 1" - Rank card: redesigned premium style with spotlight, rank-colored gradient accent bar, subtle tinted background wash (matches the referral code block treatment) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): round 6 — inline value icons, bg fixes, popover portal - StatBlock gains optional valueIcon prop rendered inline before the number; outer icon is now decorative only - Blue Buzz milestones: - Earned: green accent, IconCircleCheck outer, IconBoltFilled inline - Pending: IconBoltFilled inline (outer IconClock unchanged) - Lifetime Blue Buzz (rank card): outer icon → IconHistory, inline IconBoltFilled - Token Bank: - Spendable: green accent, IconCircleCheck outer, IconCoin inline - Pending: IconCoin inline (outer IconClock unchanged) - TierPerksPopover: `withinPortal` so the Gold popover is no longer clipped by the card's `overflow: hidden` - Recent referrals: the leading "+" stays white regardless of reward color - ReferralCodeBlock + RankCard: apply light-dark background so they stop blending with the page body in dark mode Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(referrals): points-driven milestones + auto-backfill Replace blue-buzz-driven milestone progression with a unified Referral Points metric: 1 pt per Blue Buzz earned + 1k/2.5k/5k per paid Bronze/ Silver/Gold referral month (≈10% of each tier's monthly Buzz value). - constants.referrals.pointsPerTierMonth defines the tier weights - computeLifetimeReferralPoints sums settled buzz + tier-weighted months - awardMilestones now drives off lifetime points (same thresholds: 1k/10k/50k/200k/1M) - getReferrerBalance returns lifetimePoints + pendingPoints alongside the existing token/buzz fields (plus nextTokenExpiresAt and expiringSoonTokens for the use-them-or-lose-them UI) - getDashboard fires awardMilestones(userId) fire-and-forget so a user whose lifetime points crossed a threshold under the new scoring gets the bonus written + payed out on next page load (idempotent via the existing unique (userId, threshold) constraint) - getDashboard exposes activeMembership so the redeem confirm modal can warn before stacking on top of an existing paid plan - getTierBonuses now returns rewardsMultiplierByTier and purchasesMultiplierByTier (skipping referral-grantable placeholder products so we reflect what real subscribers get); coerces the loose JSON multiplier values to number to avoid string surprises - Testing endpoint grant-tokens defaults tokenAmount to the tier's canonical 1/2/3 when omitted so seeded data matches real flow Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(referrals): dashboard polish + flag gate Big consolidated UI pass after Justin's feedback rounds 7-10. Page + content - pages/user/referrals.tsx: gated server-side on features.referralProgramV2 -> notFound; meta title aligned with buzz-dashboard pattern (Civitai | Refer & earn); skeleton fallback while data loads - static-content/referrals/terms.md: frontmatter moved above the HTML placement comment so gray-matter parses the title correctly (terms page now shows the right title + description) Dashboard - "Refer & earn" hero with IconGift ThemeIcon; subtitle tightened - Premium ReferralCodeBlock: plain left side (gray-50/dark white-3%), spotlight + subtle gradient on the right Share It panel, gradient divider between, share buttons natural compact-sm - Friends-get blurb under the code: "Friends get 25% bonus Blue Buzz on their first Membership month" - HowStep cards spotlight scoped to the colored header; rewritten copy; "Token Bank" -> "Token Shop" everywhere (title, step 4, alert key) - Rank card: premium style w/ rank-colored gradient bar + spotlight, fallback default text color when rank is gray (rookie) so it reads in light mode - Milestones renamed -> "Milestones"; driven by lifetimePoints with green Earned (IconCircleCheck) + gray Pending (IconClock) stat blocks; inline blue bolt next to BB amounts, violet star next to point counts; unlocked thresholds show a fully filled progress bar - Token Shop: spendable (green) + pending (gray) stats with inline yellow coin icons; per-tier ShopTierCard with spotlight, tier- colored accent header, info popover that opens PlanBenefitList (tier-filtered), full-width dividers between duration rows; redeem triggers an openConfirmModal when the user has an active paid membership - Spendable stat shows an IconAlertTriangle to the right of the number when tokens expire within 30 days; popover explains how many and earliest expiry - Recent referrals: rows with bg-gray-50/dark white-3%, tier-colored icon backgrounds (bronze/silver/gold), settles-on date for pending, blue bolt + amount layout for BB, yellow coin + amount for tokens - Redemption history: "−1 tok" replaced with "− [coin] 1" - Tooltips switched to color="dark" so they stop reading white in light mode - Scoring details popover (with table) replaces the inline scoring text; same popover reused on the Points stat block via an UnstyledButton info bobble that matches the rest of the dashboard - Recruiter Score renamed -> Referral Score, IconAward outer + IconStarFilled inline - Skeleton mirrors all of the above so the loading state matches the new layout - ReferralCallout (Buzz dashboard entry): tileCard-style background matching neighboring cards; gift icon; copy without slashed tier list - ReferralTimelineProgress: dark-color tooltips; tier-queue note moved below the bars in crypto-deposit info-row style - dashboard.types.ts: computeRecruiterScore now passes through lifetimePoints (rank thresholds unchanged: 0/1k/10k/50k/200k) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(stripe): inline referral code + percent/multiplier perks copy Move the referral-code entry to Stripe's hosted checkout via custom_fields so it lives "in the same modal as checkout" (per feedback: the previous pre-checkout banner felt awkward). - createSubscribeSession adds a single optional text custom_field ("Referral code (optional)", key=ref_code). Cookie-driven refCode still flows through subscription metadata as before. - checkout.session.completed handler (subscription branch) reads session.custom_fields[].text.value for ref_code, uppercases/trims, and patches the new Subscription's metadata. The existing manageInvoicePaid -> bindReferralCodeForUser / recordMembershipPaymentReward flow then picks it up unchanged. - pages/pricing: drop the standalone ReferralCheckoutBanner since the field now lives on Stripe's checkout itself. Cross-cutting perks copy: - PlanBenefitList: "Generate mature content with Blue Buzz" -> "Unrestricted generation with Blue Buzz"; remove negative mx on the section divider so it stops blowing out of popovers. - getPlanDetails: shared formatBoostCopy helper renders perk multipliers as "{pct}% bonus..." below 2x and "{n}x ..." at or above 2x; applied to both rewards and purchases multipliers; drop the isGreen-only gate so red shows the purchase multiplier when the tier metadata defines one. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(referrals): correctness + abuse hardening from external review External Gemini review of the reward distribution paths surfaced a batch of correctness, race, and abuse issues. All addressed in this commit, with vitest coverage. The dashboard copy + token-earning popover from the same round are bundled in. Backend fixes - recordMembershipPaymentReward: open a $transaction and SELECT … FOR UPDATE the UserReferral row before reading paidMonthCount / firstPaidAt. Two concurrent invoice.paid webhooks for the same referee can no longer both create a RefereeBonus or both increment past the cap. - manageInvoicePaid (stripe webhook): if subscription_details.metadata is empty, fall back to fetching the parent Checkout Session and reading custom_fields[].text.value for ref_code. Stripe doesn't guarantee checkout.session.completed lands before invoice.paid; this catches the "first invoice arrives first" race. - ReferralReward.points column (new) snapshots the tier's point value at write time. computeLifetimeReferralPoints + getReferrerBalance now sum that column instead of dynamically multiplying historical membership token counts by the current pointsPerTierMonth. Re-tuning the constants no longer retroactively unlocks milestones. Migration backfills existing rows with their tier-canonical points. - payment_intent.succeeded: drop the swallowed .catch(() => null) on recordBuzzPurchaseKickback so a transient DB error propagates and Stripe retries the webhook. Idempotency is already guaranteed by the @@unique(kind, sourceEventId) constraint. - revokeForChargeback: per-referee tx that locks UserReferral via $queryRaw FOR UPDATE, decrements paidMonthCount by the count of revoked MembershipToken rows, and clears firstPaidAt to null when the count drops to 0. Closes the refund-and-keep-buzz-kickbacks abuse path. Lock prevents two concurrent chargebacks for the same referee from racing the read-modify-write. - grantReferralSubscription: lock the CustomerSubscription row at the start of the tx so a concurrent advance-cron run can't overwrite a newly-appended queue entry. - advanceReferralSubscriptions: run each sub through its own $transaction with FOR UPDATE on the row, re-check currentPeriodEnd inside the lock so a redemption that bumped the period out doesn't get clobbered. - grantReferralSubscription enforces constants.referrals.maxQueuedDays (365). A hot referrer can't queue years of perks (Stripe's date max is 2038). Returns a user-facing error so redeemTokens rolls back and the spent tokens are not consumed. - computeLifetimeReferralPoints + getReferrerBalance.lifetimePoints include Expired status — a token expiring shouldn't pull the user's lifetime points down (or boot them out of a milestone). Dashboard copy - Milestones info alert reworked to match how kickbacks actually work: earn points per paid Membership month + 10% of any Buzz the friend buys after joining. Cross a milestone for a bonus. - Token Shop info alert simplified: "Earn tokens per Bronze (1 token), Silver (2 tokens), or Gold (3 tokens) month, up to three months per friend." - New TokenEarningsPopover wired to the Spendable stat info bobble (mirrors the ScoringDetailsPopover pattern). Permanent reference for per-tier token amounts + the 3-month-per-friend cap + 90-day expiry, available even after the dismissable alert is closed. Tests - 35 vitest cases (was 24); all green. - New: race-protected first-payment via locked paidMonthCount, cap enforcement after lock, inline RefereeBonus settle (success + buzz-grant-failure revert), revokeForChargeback decrement + firstPaidAt clear, RefereeBonus revoke does not touch UserReferral, paid months remain → firstPaidAt stays, FOR UPDATE used in revokeForChargeback (no findUnique fallback), advance bails when currentPeriodEnd is bumped out by a parallel redeem, points snapshot is immune to config changes, lifetime aggregate filter includes Expired. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
bb7e9c7560 |
Model UI Overhaul - File Management & Download Experience (#1964)
* feat: [US-001] - Add quantType and componentType type definitions - Add ModelFileQuantType type: 'Q8_0' | 'Q6_K' | 'Q5_K_M' | 'Q4_K_M' | 'Q4_K_S' | 'Q3_K_M' | 'Q2_K' - Add ModelFileComponentType type: 'VAE' | 'TextEncoder' | 'UNet' | 'CLIPVision' | 'ControlNet' | 'Config' | 'Other' - Add quantType and componentType to BasicFileMetadata interface - Add quantType to UserFilePreferences interface - Update preferenceWeight to use Partial<Record<...>> for type compatibility Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Restore model UI overhaul planning documents Re-adds planning files from feature/model-ui-overhaul-plan branch: - Main plan document with implementation details - Proposal document with options analysis - HTML mockups for sidebar and file upload UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: US-002 - Add quantTypes and componentTypes constants Added modelFileQuantTypes and modelFileComponentTypes constant arrays to src/server/common/constants.ts for use in UI selectors. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-003 - Add quantType to file preference scoring - Added quantType: 0.5 to preferenceWeight object for scoring - Updated defaultFilePreferences to include quantType: 'Q4_K_M' as default - FileMetaKey type already includes quantType from BasicFileMetadata - Files with matching quantType now get 0.5 added to their score Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-004 - Add quant type preference to user settings UI - Added 'Preferred Quant Type' Select to SettingsCard.tsx - Shows conditionally when format preference is GGUF - Uses constants.modelFileQuantTypes for options - Wired to user.filePreferences.quantType - Added tooltip explaining quant types (Q8 = best quality, Q4 = balanced, Q2 = smallest) - Default value: Q4_K_M (balanced quality/size) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-005 - Update file service for quantType selection Updated getFileForModelVersion to accept and handle quantType parameter. Files with matching quantType will now be scored appropriately during file selection. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-006 - Update download API for quantType parameter - Added quantType to validation schema with Zod enum validation - quantType is automatically passed through to file selection via spread operator - Validated against constants.modelFileQuantTypes - Misalignment check automatically handles quantType Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-007 - Add quant type selector to file upload form - Added quantType field to FileFromContextProps and SchemaError types - Updated FilesProvider to initialize and handle quantType from metadata - Added quantType to metadata upload payload in handleUpload - Added quantType to modelFileMetadataSchema in server schema - Added validation refine to require quantType for GGUF files - Added conditional quantType Select in FileEditForm for .gguf files - Added tooltip explaining quant types (Q8 = best quality, Q4/Q2 = smaller) - Updated handleSave and handleReset to include quantType - quantType is marked as required (withAsterisk) for GGUF files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-008 - Add component type selector to file upload form Added componentType field to file upload form for non-Model file types. Component type selector appears for VAE, Text Encoder, Config, and Archive files. Auto-suggests componentType based on file type (VAE -> VAE, Text Encoder -> TextEncoder). Changes: - Added componentType to FileFromContextProps and SchemaError types - Added componentType to modelFileMetadataSchema - Added componentType Select to FileEditForm with conditional display - Updated handleSave, handleReset, and handleUpload to include componentType - Added auto-suggestion logic when file type changes Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-009 - Restructure file upload UI into sections - Restructured Files.tsx into three sections: Model Files, Required Components, Optional Files - Added section headers with appropriate icons (IconFile3d, IconPuzzle, IconFileSettings) - Model Files section: files with type 'Model' or 'Pruned Model' - Required Components section: files with type 'VAE', 'Text Encoder' - yellow warning styling - Optional Files section: files with type 'Config', 'Archive', 'Workflow', etc. - Files auto-categorize based on type into appropriate section - Yellow Card styling for Required Components section to indicate importance Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat: US-010 - Create Link Component Modal Implemented LinkComponentModal for linking to existing models on Civitai as required components. Users can now: - Select component type (VAE, TextEncoder, UNet, CLIPVision, ControlNet) - Search for models using QuickSearchDropdown - Select version from model - Select file from version - Link component is saved and displayed in Files.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [US-011] - Update FilesProvider validation for new fields - Add component-only model validation: models without Model type files must have at least 2 required components (VAE, Text Encoder, UNet, etc.) - Update conflict checking to include quantType in duplicate detection, preventing duplicate [size, type, fp, format, quantType] combinations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [US-012] - Create file variant grouping utility Add groupFilesByVariant() utility function that groups model files by: - Format (SafeTensor, GGUF, other) for model files - Component type (VAE, TextEncoder, etc.) for component files Within each group, files are sorted by quality (best first): - SafeTensor: fp32 > fp16 > bf16 > fp8 > nf4, full > pruned - GGUF: Q8_0 > Q6_K > Q5_K_M > Q4_K_M > Q4_K_S > Q3_K_M > Q2_K Also exports GroupedFileVariants type for consuming components. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [US-013] - Update download button with variant dropdown - Created DownloadVariantDropdown component for grouped file variant selection - Groups files by format (SafeTensor, GGUF, Other) - Shows 'Best match' badge on user's preferred file based on preferences - Dropdown shows file size, precision/quant type for each variant - Default selected file is user's preference match - Integrated into ModelVersionDetails sidebar when multiple model variants exist Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [US-014] - Add Required Components accordion section to sidebar - Create RequiredComponentsSection.tsx with yellow warning styling - Group component variants (e.g., Text Encoder fp16/fp8) with expandable lists - Single-variant components show without dropdown - Multi-variant components show expandable list with best match auto-selected - Add Download button for each component - Add "Download All Components" button that downloads preferred variants - Update detailAccordions default to include required-components Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [US-015] Handle component-only models in sidebar - Detect component-only models (no model files, only components) - Hide main download button for component-only models - Show informational message: 'This is a modular model - download components below' - Make 'Download All Components' the primary action (filled button, larger size) - Keep Generate button visible for component-only models Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [US-016] - Update FileInfo display for new metadata - Show quantType for GGUF files in file info popover - Show componentType for component files (VAE, TextEncoder, etc.) - Added friendly display names for component types - Handle missing metadata gracefully (only show if set) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Fix prettier formatting in FileInfo.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: Fix prettier formatting in feature files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: Remove unused imports from RequiredComponentsSection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [BUG-001] - Persist linkedComponents to FilesProvider context Move linkedComponents state from local useState in Files.tsx to FilesProvider context to ensure state persists across component remounts and is available for form submission. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [BUG-002] - Add componentType to duplicate file checking Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [BUG-003] - Fix race condition in LinkComponentModal auto-selection - Move auto-selection logic from render to useEffect hook - Add versionsLoading check to prevent accessing files before data loads - Add hasAutoSelectedFileRef to prevent multiple auto-selections - Reset ref in handleBack to allow re-selection when navigating back Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [BUG-005] - Agent review validation with null safety fix Ran agent-review to validate BUG-001 through BUG-004. Fixed issue identified in review: added missing toLowerCase() for .zip extension check in FileInfo.tsx for consistency with .gguf check. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [PAT-003] - Verify form validation patterns Audit of Zod validation patterns in FilesProvider.tsx - no code changes needed. Pattern compliance verified: Zod schema extension, refine usage, showErrorNotification. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [PAT-006] - Verify download button patterns Audit completed for DownloadVariantDropdown.tsx patterns. All patterns match established codebase conventions: - createModelFileDownloadUrl usage matches other components - formatKBytes usage consistent across codebase - DownloadButton polymorphic pattern properly implemented Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [PAT-008] - Run agent-review on pattern compliance External agent review completed for all new/modified components: - LinkComponentModal.tsx: PASS - modal, search, select patterns correct - FilesProvider.tsx: MOSTLY PASS - found silent error catch issue - Files.tsx: PASS - well-structured - RequiredComponentsSection.tsx: PASS - with code duplication note - DownloadVariantDropdown.tsx: PASS - with code duplication note - SettingsCard.tsx: PASS - optimistic updates correct Key findings documented: - getFileLabel/getFileDescription duplicated across 2 files (non-blocking) - Silent catch block in FilesProvider line 391 (non-blocking) - All core patterns (modal, search, select, accordion, tRPC) followed correctly Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [QUA-001] - Extract shared file helper functions - Create ~/utils/file-display-helpers.ts with getFileLabel and getFileDescription - Update DownloadVariantDropdown.tsx to import from shared utility - Update RequiredComponentsSection.tsx to import from shared utility - Remove duplicated function definitions from both files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [QUA-002] - Fix silent error catch block Add proper error handling to the empty catch block in FilesProvider.tsx. Previously, errors from createFileMutation.mutateAsync were silently caught. Now displays an error notification to the user matching established patterns. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [QUA-003] - Check for unused imports and dead code - Remove unused import MasonryScroller from masonic in Files.tsx - Remove unused variables offset and resizeObserver in Files.tsx - Fix unescaped apostrophe (We'll -> We'll) in Files.tsx - Remove unused index parameter in map callbacks in Files.tsx - Remove unused componentType prop destructuring in RequiredComponentsSection.tsx - Remove unused modelType prop destructuring in DownloadVariantDropdown.tsx Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [QUA-004] - Verify accessibility attributes Added comprehensive ARIA attributes and keyboard accessibility: - DownloadVariantDropdown: Added role="button", aria-expanded, aria-haspopup, aria-label, tabIndex, keyboard handler to dropdown trigger; role="listbox" to dropdown content; role="option", aria-selected, aria-label to VariantItem - RequiredComponentsSection: Added role="button", aria-expanded, aria-label, tabIndex, keyboard handler to expandable component headers; role="listbox" to variant list; role="option", tabIndex, aria-selected, aria-label, keyboard handler to file selection boxes - LinkComponentModal: Added aria-label to QuickSearchDropdown and Select components for search, version, and file selection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: [QUA-006] - Verify useEffect cleanup - Added useMemo to the `files` variable in LinkComponentModal.tsx to maintain reference equality for useEffect dependencies - Fixes ESLint react-hooks/exhaustive-deps warning about the files logical expression causing useEffect dependencies to change on every render - Reviewed all useEffect hooks in LinkComponentModal.tsx, FilesProvider.tsx, and DownloadVariantDropdown.tsx for proper cleanup - Verified no async operations/subscriptions/timers require cleanup in these files Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: Remove accidentally committed temp and ralph project files - Remove ralph phase2-pattern-reuse prd.json and progress.txt (should be gitignored) - Remove tmpclaude-* temp files - Add tmpclaude-* pattern to .gitignore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fixes typecheck issues * feat(model-ui): overhaul model sidebar with variant downloads, components, and actions - Restructure primary actions card with consolidated icon row (generate, share, like, vault, civitai link, notifications, collect, bid, report) - Add DownloadVariantDropdown with grouped file sections (SafeTensor/GGUF/Other) and best-match badge - Add RequiredComponentsSection with component variant selection and Download All - Persist linked components via RecommendedResource with isLinkedComponent flag - Overhaul PermissionIndicator to show all permission badges (commercial, generation, credit, merges, license, NSFW) - Handle early access/Buzz pricing in download and component sections - Expand upload UI component filter to include UNet, CLIPVision, ControlNet - Remove dead LinkComponentModal code (keep type export only) - Fix: side effect in setState updater causing duplicate API calls - Fix: memoize filesVisible to prevent groupFilesByVariant recomputing every render - Fix: stagger multi-download to avoid browser popup blocking - Fix: gate download hrefs on canDownload for early access models - Fix: add staleTime to getFollowingUsers query to reduce unnecessary refetches Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Includes design file * feat: model sidebar UI polish - verification status, button styling, reviews - Add VerifiedText scan status to all file variants (download dropdown, required components, optional files) - Remove redundant VAE alert banner from ModelFileAlert (now in RequiredComponentsSection) - Hide Bid button for unpublished/deleted model versions - Restyle Generate button as primary CTA with gradient background - Make download button styling consistent (light blue secondary CTA) for single and multi-variant - Update VerifiedText to match design (smaller text, colored icons, no ThemeIcon wrapper) - Fix VerifiedText click propagation preventing variant toggle on popover click - Fix light mode hover backgrounds on variant rows for readability - Replace ResourceReviewThumbActions with ModelVersionReview in Details card Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add isRequired to file metadata & merge upload UI sections - Add `isRequired` field to BasicFileMetadata, modelFileMetadataSchema, and linkedComponentSettingsSchema - Move LinkedComponent type from LinkComponentModal.tsx to model-file.schema.ts - Replace hardcoded 3-way file split (Model/Required/Optional) with dynamic 2-way split (Model Files + Additional Components) driven by acceptedModelFiles per model type - Add Switch toggle for required/optional on FileCard and LinkedComponentCard - Smart defaults: component types (VAE, Text Encoder, etc.) default to required - Update groupFilesByVariant() to use metadata.isRequired for sidebar grouping - RequiredComponentsSection now data-driven (no hardcoded component type list) - Update validation to use isRequired for component-only model checks Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Updates design * fix: restore buzz earned and generation popularity metrics to model version details Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: enrich linked components at read time & add backend link creation - Batch-fetch ModelFile data (name, sizeKB, type, metadata) in both controllers to enrich linked components at read time, fixing stale data for older records - Add addLinkedComponent mutation that resolves file details server-side, eliminating the extra modelVersion.getById fetch from the frontend - Remove sizeKB from linkedComponentSettingsSchema (no longer persisted) - Add fileType/fileMetadata to LinkedComponent for correct download URLs - Update RequiredComponentsSection and ModelVersionDetails to use file metadata for download URLs instead of user preferences - Display file size next to download button consistently across all component types - Add UNet, CLIPVision, ControlNet to modelFileTypes and modelFileOrder - Various UI polish: icon swap, border removal, no-wrap on file sizes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: guard download buttons behind purchase check for optional/linked files Optional files and linked components in both ModelVersionDetails and RequiredComponentsSection were missing canDownload guards, allowing direct downloads on early-access models that require purchase. Also removes unused downloadPrice prop from ComponentGroup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address PR review: type safety, null handling, and shared constants - Add componentFileTypes constant to DRY up hardcoded arrays in 3 locations - Tighten componentType schema validation from z.string() to z.enum() - Prevent null quantType via allowDeselect={false} on SettingsCard Select - Fix groupFilesByVariant to default isRequired to true (isRequired !== false) - Normalize component-only validation to match linked component convention - Remove unused RecommendedResourceSettings import Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Correctly filters out incompatible resources when linking components * Fix model UI feedback: share popover, download URLs, resource select, upload errors - Add withinPortal to ShareButton Popover to fix clipping in sidebar - Use primary download URL for linked components to avoid 404 misalignment - Default resource select to 'all' tab for modelVersion linking and skip empty recommendedModels filter to prevent spinner - Improve file upload rejection messages with supported types and max file limit notifications Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Port resource select fixes to new ResourceSelectModal component Apply the same fixes from the deleted ResourceSelectModal2.tsx: - Default to 'all' tab when selectSource is 'modelVersion' - Skip empty recommendedModels filter to prevent spinner Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Replace useEffect with conditional state for modelVersion tab default Use local useState instead of localStorage-backed state when selectSource is 'modelVersion', avoiding the stale tab problem without a useEffect override. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fixes bad download url for required and optional components * Fix Download All Components to download multiple files via hidden iframes The previous implementation used forEach + setTimeout which broke the browser's user-gesture trust context, causing only the first file to download. The model download endpoint uses res.redirect(), so programmatic <a> tag clicks get blocked after the first navigation. Hidden iframes each follow redirects independently in their own browsing context. Also adds loading state to the download button. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Migrate vaeId to linked components system Replace the legacy ModelVersion.vaeId FK with the linkedComponents system (RecommendedResource table). This unifies VAE linking under the same mechanism used for all other component types (TextEncoder, UNet, CLIPVision, etc.). - Add SQL migration to create RecommendedResource records for existing vaeId refs - Add getLinkedVaeIds helper for batch VAE resolution from linked components - Update model controller, file service, generation service, and public API - Replace raw SQL vaeId column refs with RecommendedResource subqueries in caches - Generalize file download to support all linked component types, not just VAE - Remove vaeId from selectors, schemas, and frontend form - Remove duplicate VAE display in model detail (linkedComponents already shows it) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Removes TextualInversion and Hypernetwork filters when selecting linkedComponents * Retrigger pr preview deployment * Address model upload wizard UI feedback - Add Workflow and Upscaler to file types, component types, and file order - Expand GGUF quant types from 7 to 25 (K-quants, legacy, I-quants) - Allow .gguf files in Checkpoint additional components (for quantized text encoders) - Fix .zip inference: no longer auto-assigns type, user must pick - Fix .zip format mapping from 'Diffusers' to 'Other' - Add .json default inference to 'Config' - Add ComfyUI-friendly display labels (CLIP / Text Encoder, UNet / Diffusion Model, etc.) - Add Upscaler and Workflow to linked component resource selector - Improve validation error surfacing with per-file toast notifications - Add red border highlight on file cards with validation errors - Make quant type select searchable for the expanded list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Update quantQualityRank with expanded GGUF quant types Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: add Consolidate Versions feature for merging multi-version models Allow creators to merge multiple model versions into a single version, moving all files into the target version with configurable file type mapping and aggregating stats (downloads, likes, etc.) from all source versions. Triggered from the model detail page context menu. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update import path for ReportEntity to use shared utils * feat: enhance ConsolidateVersions UI and support file metadata updates Overhaul the ConsolidateVersions modal with improved theme-aware design, file metadata editing (fp, size, format, quantType, isRequired), and updated schema/service to persist metadata changes during consolidation. Also pass quantType and isRequired through getModelsWithVersions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: GGUF validation, .bin file type options, and required toggle reset - Skip fp (precision) validation for GGUF checkpoint files since they use quantType instead — the UI shows Quant, not Precision, for GGUF files - Allow .bin files to be assigned component types (VAE, UNet, etc.) in the file type dropdown, not just Model/Negative - Stop resetting the Required toggle when changing file type in Additional Components — preserve the user's manual selection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove DownloadHistory reference from consolidateVersions The DownloadHistory table no longer exists in production — downloads are now tracked in ClickHouse. The raw SQL query was causing a 42P01 error when consolidating versions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: expand file upload types, selective version merging, rename consolidate to merge - Allow config-type files (.json, .yaml, .yml, .txt) as primary uploads for Workflows, Poses, Wildcards, and Other model types - Add .safetensors to Detection primary extensions - Ensure all model types accept archive + config in additional components - Extract shared filterFileTypeByExtension to file-display-helpers.ts - Make inferFileType model-type-aware for correct .zip default assignment - Fix additional dropzone defaultType bug (was always using first type) - Add selective source version picking to merge versions wizard - Rename ConsolidateVersions to MergeVersions across frontend and backend Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove debug console.log referencing ctx.isGreen in trpc middleware Stray debug log was accessing ctx.isGreen which doesn't exist on the context type — isGreen is a feature flag on ctx.features, not ctx directly. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: skip type inference for additional component drops, add Diffusion Model file type Files dropped in the Additional Components section no longer auto-infer as 'Model' type, letting users pick the correct component type (VAE, UNet, etc.). Split grouped labels "CLIP / Text Encoder" → "Text Encoder" and "UNet / Diffusion Model" → separate "UNet" and "Diffusion Model" options. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: reorganize model sidebar cards for better UX flow Move resource review card after the accordion so it no longer interrupts between download and model details. Group download-related alerts near the download card. Move donation goals below review as a secondary CTA. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat(model-upload): add UNet and Diffusion Model to Checkpoint primary file types Move UNet and Diffusion Model from the additional components section to the primary model files section for Checkpoint models, so users can select these types directly from the model file type dropdown. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: manuelurenah <manuel.ureh@hotmail.com> |
||
|
|
62f543cd3b |
Merge pull request #2186 from civitai/feature/generation-presets
add generation presets |
||
|
|
06cf4abd02 | add generation presets | ||
|
|
a081a771f7 |
feat(articles): add moderatorNsfwLevel override with edit-form picker (#2183)
* feat(articles): add moderatorNsfwLevel override and recompute-on-save Moderators previously had no way to force an article's rating to a specific level. Editing the maturity picker wrote to userNsfwLevel, but (a) upsert never triggered a nsfwLevel recompute - only the scan, text-moderation, and report webhooks did - and (b) even after a recompute the GREATEST-based derivation meant a mod could only raise the rating, never lower one that had been overshot by scans or a heavy-handed floor. Ally flagged an article stuck at R with no path to bring it back down. Add a nullable moderatorNsfwLevel column that takes precedence over the full derivation. updateArticleNsfwLevels now writes COALESCE(moderatorNsfwLevel, GREATEST(userNsfwLevel, cover, content, moderation_floor)), so non-null pins the rating bidirectionally and null falls back to the existing auto-derivation path. upsertArticle now: - Strips moderatorNsfwLevel from non-moderator payloads (silently; the picker never ships to owners). - Diffs the incoming moderatorNsfwLevel and userNsfwLevel against the persisted values to decide whether to trigger updateArticleNsfwLevels post-commit, closing the gap where direct edits to those inputs previously sat dormant until an unrelated webhook kicked recompute. - Auto-manages lockedProperties: setting an override locks the owner's userNsfwLevel picker, clearing it unlocks. Form gets a mod-only InputSelect with an "Auto (derived from content)" option for clearing the override, plus a blue alert that surfaces the override to the owner and tells them their own preference is preserved. The existing yellow "images raised it above your preference" alert is gated on moderatorNsfwLevel being null so the two don't stack. Migration adds the nullable column; search-index + card-layer cover lifts already read article.nsfwLevel and inherit override behavior without change. * refactor(articles): consolidate recompute + scope moderatorNsfwLevel Post-merge cleanup and Copilot review feedback on PR #2183: - upsertArticle: drop the conditional `shouldRecomputeNsfwLevel` / `userNsfwLevelChanged` recompute hook — it's subsumed by the `updateArticleImageScanStatus([id])` call that now runs unconditionally post-commit (from the main-branch leak fix) and does the same recompute under an advisory lock alongside ingestion recompute. Keeps `moderatorOverrideChanged` since it still drives the `lockedProperties` pin/unpin. - articleDetailSelect: remove `moderatorNsfwLevel` from the shared select. Copilot flagged that this shape is reused by the outbound webhook job (spreads `...article` into the third-party payload) and the Meilisearch indexer (spreads into the index document), so keeping the override in the shared select would leak an internal moderation signal to external consumers. Added a comment at the selector documenting the intentional omission. - getArticleById / getModeratorArticles: extend the select locally with `moderatorNsfwLevel: true` so the edit form and mod list still get the field they need to render the override UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: manuelurenah <manuel.ureh@hotmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cf37a19a20 |
feat(buzz): DB-backed Rewards Bonus Events with moderator UI (#2182)
Replaces the REWARDS_BONUS_MULTIPLIER Flipt flag with a first-class
RewardsBonusEvent model so mods can schedule, name, describe, and link
bonus events without a redeploy. Admin UI at /moderator/rewards-bonus-events.
Multiplier stored as int * 10 (15, 20, 30, 40); active event picked by
highest multiplier within the enabled start/end window. Site-wide banner
picks up the event's optional bannerLabel; info modal surfaces name,
description, and a Learn more link to an attached article (relative /articles/{id}
so it works on both civitai.com and civitai.red).
In-process 5 min cache over the enabled-events list, with time-boundary
filtering evaluated on every request so scheduled starts/ends take effect
immediately instead of waiting on TTL. Invalidated on upsert/delete, and
the banner query is invalidated client-side after moderator saves so the
editor sees changes without a refresh.
Dates picked in the mod UI are stored as UTC midnight / UTC end-of-day
regardless of the moderator's browser timezone, mirroring the challenges
event pattern (toDisplayUTC / fromDisplayUTC + dayjs.utc startOf/endOf).
Schema refine rejects inverted windows.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
e439ef6ac3 |
feat(collections): bucket-based nsfwLevel rollup
- Two-probe EXISTS scan (safe=1, nsfw=28; Blocked excluded)
- Precedence: forcedBrowsingLevel > item scan
- Collection.nsfw ignored — auto-flipped by user NSFW reports, unreliable
- Re-enable propagation from image/post/model/article updates to parent collections
- Trigger: re-scan on availability / read / metadata.forcedBrowsingLevel change
- Scope restricted to read IN ('Public','Unlisted')
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
45be69c4bb |
pick upscale model in generation panel
(cherry picked from commit
|
||
|
|
ed82ade5a9 |
feat: article ingestion status + manual rescan (#2158)
* feat: add article ingestion status and manual rescan Adds ArticleIngestionStatus enum (Pending/Scanned/Blocked/Error) to gate article visibility by scan state, separating scan state from publication state. Non-owner/non-mod article reads now filter on ingestion = Scanned. Also adds a user-triggered rescan feature: article owners and moderators can manually re-trigger image scanning and text moderation from the article detail page, with per-article rate limiting (3/day for owners, unlimited for mods). Key changes: - New Article.ingestion column with migration and serving-path gates - rescanArticle() service: resets ingestion, re-queues images, clears content hash and re-submits text moderation - article.rescan tRPC mutation with isOwnerOrModerator middleware - Rescan menu item in ArticleContextMenu + button in ArticleScanStatus - Search index sync on ingestion state changes via recomputeArticleIngestion - Text moderation webhook integration for ingestion recomputation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR feedback on article ingestion and rescan - Guard contentScannedAt to only set on transition to Scanned, preserving the original scan-completion timestamp for observability - Queue search index update immediately in rescanArticle so the article is not searchable while rescanning - Gate Rescan menu item behind articleImageScanning feature flag - Use ArticleIngestionStatus.Rescan (instead of Pending) for user-triggered rescans and content-edit rescans to distinguish from initial pending scans Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
1ef4313251 |
Scan article embedded images (#1879)
* Adds implementation docs
* Adds support for parsing and scanning embedded article images
* Fixes scanning and publishing after processing article images
* Shows processing modal for updated article
* Show problematic images on article scan with actions
* Improves article image scan migration, adds feature flag and contentScannedAt migration
* Adds feature flag check on image-scan-result
* Cleanup docs
* More cleanup to docs
* Fixes OOM issues with migration script
* Fixes after merging with main
* Updates article image scan to use new ingestion pipeline
* Fixes image ingestion step for article migration script
* refactor: replace JSDOM-based image extraction with tiptap-based getContentMedia
- Extend getContentMedia to handle both media nodes (edge-media) and image
nodes (img tags) with video support, replacing extractImagesFromArticle
- Remove manual ingestImage calls from migration script — DB trigger
(trg_image_scan_queue) handles queueing automatically
- Remove JSDOM dependency (jsdom, @types/jsdom) and article-image-helpers.ts
- Fix ESM build error caused by JSDOM leaking into Next.js page bundles
- Add unit tests for extractCloudflareUuid, isValidCivitaiImageUrl,
getContentMedia, and getContentImageUrls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: add xGuard text moderation for articles
Submit article title + content to xGuard for ML-based text moderation on
create/update. Webhook handler maps triggered labels to NsfwLevel, elevates
userNsfwLevel (never lowers), and auto-unpublishes blocked articles.
Migration script extended with mode param (images|text-moderation|both)
for backfilling existing articles. Docs renamed and updated to reflect
both image scanning and text moderation pipelines.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(text-moderation): pass rating labels to xGuard and use direct NsfwLevel mapping
- Add labels param to submitTextModeration and forward to orchestrator
- Pass rating labels (pg, pg13, r, x, xxx, nsfw) for article moderation calls
- Replace mapTriggeredLabelsToNsfwLevel with direct NsfwLevel enum mapping from triggered rating labels
- Remove old category-based labelNsfwMap (no longer needed)
- Respect dryRun in migration script text-moderation mode
- Temporarily disable article nsfwLevel updates in webhook to validate EntityModeration data
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(articles): update linkArticleContentImages to return orphaned IDs and support cleanupOnly mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(articles): update upsertArticle to handle orphaned image cleanup via deleteImageById
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(articles): delete content Image records on article deletion via deleteImageById
Replace deleteArticleContentImages (S3-only cleanup) with deleteImageById loop
so that Image DB records, S3 files, and caches are all cleaned up when an article
is deleted. Content image IDs are collected from ImageConnection before the
transaction removes those rows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(articles): remove deleteArticleContentImages and getContentImageUrls (replaced by deleteImageById)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(articles): add retry-failed-moderation mode and composable mode param
Change `mode` from single enum to comma-delimited array so modes can be
combined freely (e.g. `mode=text-moderation,retry-failed-moderation`).
Add `retry-failed-moderation` to re-submit articles with Failed/Expired
EntityModeration status. Also guard against undefined response in
orchestrator service.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(articles): rework migrate-article-images with separated mode processors
- Extract each mode (images, text-moderation) into its own function with
dedicated dataProcessor, eliminating interleaved if/else logic
- Fix stats race condition: each batch processor now collects into local
stats and merges after completion, preventing lost updates from
concurrent dataProcessor batches
- Fix extraction-failed articles being silently marked as scanned by
tracking failedArticleIds separately
- Merge retry-failed-moderation into text-moderation mode — now targets
all articles without a Succeeded EntityModeration status
- Limit text moderation concurrency via limitConcurrency to avoid
overloading the orchestrator
- Use Set for missing URL lookups (O(1) vs O(n) array scan)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Adds wait time when submitting text moderation requests
* refactor(text-moderation): simplify to nsfw-only scanning and split article backfill endpoint
- Reduce xGuard labels to ['nsfw'] across article create/update, retry job, and backfill
- Split article text-moderation backfill into dedicated endpoint with flat batch-by-batch concurrency using pgDbRead.cancellableQuery
- Strip text-moderation mode from migrate-article-images.ts (now image-scan only)
- Simplify text-moderation-result webhook Article handler to flip nsfw flag only (elevation-only)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(articles): tighten nsfwLevel filtering across serving paths for green rollout
- Fold Article.nsfw into nsfwLevel via CASE WHEN (matches the existing
updateModelNsfwLevels/updateBountyNsfwLevels pattern): when the text scan
flags an article, its level is written as nsfwBrowsingLevelsFlag so the
standard bitwise AND filter handles it without new read-side logic.
- Swap bit_or -> max for cover + content image aggregation in
updateArticleNsfwLevels. Since NsfwLevel values are powers of 2, integer max
is the correct "highest rating" semantic. Eliminates the multi-bit leak
class where articles mixing PG + NSFW images produced bitmasks retaining the
PG bit and slipped through (nsfwLevel & publicBrowsingLevelsFlag) != 0.
- Close serving-path leaks that previously bypassed any nsfwLevel filter:
getCivitaiNews now AND's publicBrowsingLevelsFlag into its raw SQL;
getCivitaiEvents passes it through to getArticles; getArticleById accepts
an optional browsingLevel and applies a post-fetch bitwise check for
non-owner/non-moderator callers.
- article.getById router derives browsingLevel from ctx.features.canViewNsfw
so civitai green force-hides NSFW articles on the detail endpoint without
relying on client-side SensitiveShield.
- Articles sitemap: replace hardcoded NsfwLevel.PG with publicBrowsingLevelsFlag
for semantic consistency with the other serving-path fixes.
- Remove createProfanityFilter + filterSensitiveProfanityData from the
article path (upsert write block, getArticleById read-side hygiene,
ProfanityEvaluation type imports, ArticleMetadata.profanityMatches +
profanityEvaluation fields). The orchestrator text-moderation scan fully
supersedes the lexical profanity check.
- /api/og.tsx fetchArticleData: keep title/content/stats visible when the
cover image is NSFW, letting getSafeImage + buildEntityImage(null) fall
through to the LogoPlaceholder template path instead of blanking the
whole OG card.
- Open articles feature flag to ['public'] (was ['blue','red','public']) so
civitai green can serve articles now that nsfwLevel filtering is correct
across every surface.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(migration): update default values for batchSize and concurrency in query schema
* fix(migration): drop articles entity from profanity backfill
ArticleMetadata no longer carries profanityMatches/profanityEvaluation
(removed in
|
||
|
|
c51d875d16 | fix migration | ||
|
|
c52b4fe5b9 |
Merge pull request #2146 from civitai/feature/green-auctions
Support Auctions on Civitai Green using Green Buzz |
||
|
|
cd5ae829d0 | fix to migrations | ||
|
|
d4242c4839 | added accountType to Bid and BidRecurring id signature | ||
|
|
809c0640da | Support Auctions on Civitai Green using Green Buzz | ||
|
|
2f2b91a602 | Merge branch 'main' of https://github.com/civitai/civitai into main | ||
|
|
12faf69b3c | job to retry text-moderation | ||
|
|
99a99bd910 |
fix: resolve follower notification timeout death spiral (#2140)
* fix: resolve follower notification timeout death spiral The new-model-from-following notification query has been timing out since April 3-4 due to a missing Model(status, publishedAt) index causing 620K+ row scans. When the query times out, the cursor never advances, widening the window on each retry and guaranteeing further timeouts. Three-part fix: - Add Model(status, publishedAt) index to eliminate full table scan - Cap notification query window to 10 minutes with incremental catch-up - Add 59-second lookback buffer to new-model-from-following query Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add comment explaining 59-second lookback buffer in notification query Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add context to safety valve log for easier operational debugging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
2fc9c543c8 | add text moderation | ||
|
|
84b07c5bd7 |
Redesign NowPayments reconciliation with sliding window and retry sweep
- Replace fixed 24h window with cursor-based sliding window using getJobDate (stored in KeyValue table, defaults to now-24h on first run) - Catch grantBuzzPurchase errors in processDeposit and upsert CryptoDeposit with status='buzz_failed' so deposits are never lost - Handle 409 duplicate transaction as success (webhook/reconciliation race) - Enforce status ordering: confirming can't overwrite finished/buzz_failed - Add retry sweep: each reconciliation run retries buzz_failed deposits (re-fetches from NP API for fresh data, max 72 retries = 12 hours) - Throw on null NP API response during pagination instead of silently stopping - Add retryCount column and status index to CryptoDeposit - Fix test assertions: PAGE_SIZE=500, page starts at 0 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
0b8975e3c7 |
feat: comic creator — project-scoped references, layouts, speech bubbles, and UX polish
- Project-scoped references: new ComicProjectReference junction table with
migration and backfill. References are now per-project with import modal
for bringing in references from other projects. Sidebar shows remove
(from project) vs delete permanently (with confirmation) actions.
- Pre-made panel layouts: 8 PNG layout images shown as horizontal scrollable
picker at top of Generate tab, included as reference images during generation.
Layout consumes an image slot. Zoom preview on each thumbnail.
- Speech bubbles: 4 PNG stamps added to DrawingEditor toolbar as a popover.
Clicking adds a resizable/draggable image element on the canvas.
- Multi-image polish: AwaitingSelection panel status (new DB enum + migration),
CandidateImageModal with pre-select/confirm and zoom, polling mutex to
prevent concurrent requests, panels no longer auto-open selection modal.
- Duplicate guards: only Ready panels with images can be duplicated, chapters
with incomplete panels are blocked. Confirmation dialogs and loading states
on both panel and chapter duplicate.
|
||
|
|
7157c8ce92 |
feat: comic creator — duplicate panel/chapter, multi-image generation, reference panel picker
- Duplicate panel/chapter: new tRPC mutations with position shifting,
confirmation dialogs, loading states, and guards (only Ready panels,
no incomplete chapters). Context menu items in PanelCard and chapter sidebar.
- Multi-image generation: quantity selector (1-4) in PanelModal, new
AwaitingSelection panel status with DB migration, CandidateImageModal
with pre-select/confirm flow and zoom preview, selectPanelImage mutation.
Panels show yellow pulsing outline when awaiting selection.
- Reference panel picker: replaced includePreviousImage boolean switch
with ReferencePanelPicker component — horizontal scrollable thumbnails
of all Ready panels in the chapter. Server validates referenced panel
belongs to same project and has an image.
- Polling hardened: added mutex to prevent concurrent polls, handle
zero-candidate edge case (marks panel Failed), removed console.logs.
|
||
|
|
dd07c79248 |
fix: prevent challenge winner cooldown bypass via image deletion (#2120)
* fix: prevent challenge winner cooldown bypass via image deletion ChallengeWinner.imageId FK used ON DELETE CASCADE, so deleting a winning image removed the winner record and allowed users to bypass cooldown. Changed to nullable imageId with ON DELETE SET NULL so winner records survive image deletion and cooldown enforcement remains intact. Fixes #2119 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: keep all winners on retry path, gate UI on both imageId and imageUrl Address review feedback: - Retry path no longer filters out winners with null imageId, preventing missed prize payouts/notifications when image deleted between runs - WinnerPodiumCard gates image block on both imageId and imageUrl for defensive safety Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: add placeholder for deleted winner images on podium card Shows an "Image removed" placeholder with IconPhotoOff when a winner's image has been deleted, instead of leaving a gap in the podium. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
8fc3c10536 | Limit requests based off of generator queue. Add jobs to handle enqueued jobs from the smart create | ||
|
|
068827d49a | Seadream support + Download support | ||
|
|
591f4a8570 |
feat: simplified multi-chain crypto deposits with permanent addresses (#2105)
* feat(crypto): simplified crypto deposit system with permanent addresses Replaces the old per-payment NowPayments flow with permanent deposit addresses per user. Adds deposit history with live signal updates, currency selector with fiat preference persistence, conversion rate display, and skeleton loading states. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): review pass — DRY, security, performance, accessibility DRY: Extract shared FIAT_OPTIONS, outerCardStyle, getFiatDisplay() into crypto-deposit.constants.ts. Create reusable FiatMenu component. Security: Remove webhook secret from error logs, add input bounds on perPage/page (max 25), cap concurrent API calls at 10. Performance: Replace spotlight setState with ref-based DOM manipulation, lazy-load QR code via next/dynamic, remove duplicate signal listener, stabilize callback refs, add staleTime to deposit history query. Backend: Fix broken reprocess-order endpoint, add division-by-zero guard in getBuzzConversionRate, replace `as never` Redis key casts with proper typing via paymentCacheKey helper. Accessibility: Add aria-labels to copy button, fiat menus, fee popover, and signal status refresh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(crypto): multi-chain deposit addresses and custody sweep Add per-chain deposit address generation, chain-config registry, payout webhook handler, and custody sweep job for consolidating funds across chains. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): review pass 2 — DRY, hardening, performance, a11y DRY: Centralize chain display names and Buzz conversion formula in chain-config.ts. Remove duplicate maps from UI components. Hardening: Validate chain input with z.enum, add custody sweep idempotency via Redis dedup, fix auth token race condition with promise deduplication, sanitize webhook error responses, guard against NaN balance values. Performance: Stabilize onRetry and handleFiatChange callbacks to prevent unnecessary re-renders and API calls. Use ref pattern for updateSettings dependency. Accessibility: Wrap currency badges in UnstyledButton with aria-label and aria-pressed for keyboard and screen reader access. Tests: Sync test file with multi-chain service API changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): rename variant, add network labels, fix EVM display name - Rename DepositCardVariantC → DepositCardContent, remove variants/ dir - Fix EVM chain display name: "Base" → "Ethereum" - Show network in chain badge for multi-network chains (e.g., "Ethereum — Base") - Add NETWORK_DISPLAY_NAMES map and getNetworkDisplayName() to chain-config - Change "Min" → "Minimum" deposit label for translation compatibility - Update FiatMenu comment to reference new component name Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(crypto): replace API-based deposit history with local CryptoDeposit table - Add CryptoDeposit model (replaces CryptoDepositFee) with full deposit lifecycle tracking: status, amounts, fees, chain, timestamps - Migration drops CryptoDepositFee, creates CryptoDeposit with userId index - processDeposit now upserts CryptoDeposit on every webhook status - getDepositHistory queries local DB instead of NowPayments API — correct chronological ordering across all chains, no more API rate concerns - Remove bustDepositCache endpoint and payment status Redis caching - Remove CRYPTO_PAYMENT_STATUS and CRYPTO_DEPOSIT_HISTORY Redis keys - Add NOWPAYMENTS_IPN_URL env var for configurable webhook URL (dev support) - Simplify DepositHistory component: chain comes from DB, no currency lookup - Update tests to match new DB-based architecture (23 tests passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): handle partially_paid, hide fees until finished, ceil rounding - Handle partially_paid webhook status (grants buzz like finished) — fixes $20 order amount causing partial payment instead of finished - Remove confirmed status handling (confusing UX, no action taken on it) - Only show fees in deposit history once status is finished - Ceil fee display to nearest cent instead of rounding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): show proper ticker + network in deposit history Deposit history was showing raw NowPayments currency codes (e.g., "USDCBASE") instead of split ticker + network. Re-add currencies lookup (React Query deduplicates, no extra API calls) to resolve codes to proper tickers. Shows network badge only when it differs from the chain name (e.g., "USDC [Base]" but just "BTC" for Bitcoin). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): show network badge for multi-network tickers, fix fee on partial - Show network badge only when the ticker exists on multiple networks (e.g., "USDC [Base]", "USDT [Tron]") — single-network coins like BTC, DOGE, LTC show no badge - Fix fees not showing for partially_paid deposits (status check was only matching 'finished') Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): use dotted underline abbr-style for fee popover trigger Replace the info icon button with a dotted-underline text trigger on the fee amount itself. Clicking the fee text opens the popover with fee details. Uses cursor-help and decoration-dotted for the standard abbreviation affordance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): centralize deposit completed status check Add isDepositComplete() to chain-config — single source of truth for statuses that mean buzz was credited (finished, partially_paid). Fixes toast notification not firing on partially_paid deposits. Replaces all scattered status === 'finished' checks across service, signal handler, and deposit history component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): clarify deposit address and history are personal Add "Your" to deposit address label and recent deposits heading so users understand the address is tied to their account and sharing it would credit Buzz to them, not the recipient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(crypto): add bonus buzz display, live timer, normalize deposit status - Add bonusBuzz (Int) and multiplier (Int, x100) columns to CryptoDeposit - Store membership multiplier and bonus amount at deposit time - Show bonus buzz via yellow + hover card with membership percentage - Normalize partially_paid to finished in DB and signals - Simplify downstream status checks to === 'finished' - Add live prop to DaysFromNow for auto-updating relative times - Use DaysFromNow in deposit history for live-ticking timestamps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(membership): add collapsible benefits list with buzz multiplier highlight Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ui): align card styles, fix modal width, polish buzz purchase components - Switch Coinbase packageSection from Card to Paper with outerCardStyle to match Crypto tab card rendering (Card vs Paper have different defaults) - Add consistent box-shadow to inner cards (customAmountCard, bulkBenefitsCard, paymentSection) using the shared light-dark shadow pattern - Constrain BuzzPurchaseLayout Grid to max-width 1200px so the modal doesn't expand unconstrained (size="xxl" isn't a real Mantine preset) - Simplify BuzzFeatures: remove hover animations, use light variant icons - Simplify MembershipUpsell: remove collapsible benefits, float image, add gradient multiplier text, show all benefits inline - Fix BonusBuzzContent light mode contrast with Tailwind dark: variants - Bump currency selector badges from size sm to md, fix multi-network badge alignment by wrapping in UnstyledButton - Update BUZZ_FEATURE_LIST copy (generation types, cosmetic shop) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dashboard): consolidate buzz dashboard visual consistency Normalize card borders, gaps, titles, loading states, and chart styles across the entire buzz dashboard for a cohesive look and feel. - Add border to .tileCard SCSS class, fix CreatorProgramV2 light-mode bg - Normalize all grid gutters to md (16px), section spacing with mt-xl - Standardize card titles to text-xl font-bold (~20px) - Replace loading spinners with skeleton rows (PurchasedCodesCard) and contextual empty state (GenerationBuzzEmptyState) - Create GenerationBuzzEmptyState with split-panel gradient/spotlight design - Redesign GeneratedImagesRewards card: info popover, sectioned filter, abbreviated Y-axis ticks, thinned date labels, shared tooltip style - Extract shared chart defaults (scales, tooltips, legends) to chart-defaults.ts - Use Next.js Link for feature card buttons (client-side navigation) - Float clear-selection button over scroll area in Top Earning Resources - Fix Bank Buzz button height, RedeemCodeCard title color, code block light mode - Fix MembershipUpsell type errors (stale SCSS .d.ts, ReactNode benefits) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dashboard): remove layout hacks and polish mobile view - Replace absolute positioning hack with proper CSS Grid for the Generation Buzz Earned card layout - Fix empty state collapsed to a thin line when no data - Remove arrow icon from transactions "View all" link to prevent title wrapping on mobile - Add whiteSpace: nowrap to View all link Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ui): remove extra padding from empty deposit state, fix p-in-p nesting - Remove stacked py="md" from EmptyDepositState inner Stack (outer Paper p="lg" is sufficient) - Revert h="100%" from deposit card Papers (not needed in Stack layout) - Fix validateDOMNesting warning in MembershipUpsell BenefitRow (Text renders as div) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |