mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
df3d68a9d8
* feat(challenges): add ChallengeEngagement table for challenge tracking Adds a per-user "notify me" engagement table for challenges, plus the manual-apply migration SQL. Nothing reads or writes it yet. * refactor(challenges): move creator-block helpers into challenge-visibility Avoids an import cycle: challenge-engagement.service.ts (Task 3) needs the viewer block-exclusion set, and challenge.service.ts needs to call the engagement service. * fix(challenges): move creator-block helpers into challenge-block.service instead challenge-visibility.ts is a pure-logic leaf with no test-time deps; pulling user-preferences.service into it drags in the Redis cache graph (user-preferences.service -> redis/caches.ts -> orchestrator/models.ts -> redis/resource-data.redis.ts) and hits a circular-import TDZ crash. That silently zeroed out three daily-challenge suites (37 tests) instead of failing loudly, since vitest reports an empty suite as passing. Move getChallengeExcludedUserIds/challengeCreatorBlockSql into a new challenge-block.service.ts instead, alongside the other *.service.ts modules that already depend on user-preferences.service. Revert challenge-visibility.ts to its pre-move state. * feat(challenges): add challenge engagement service for tracking Tracks per-user "notify me" state on challenges (challengeEngagement Notify rows) and resolves recipients for go-live/reminder notifications. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(challenges): scope creator-block to User source in toggleChallengeNotify System challenges share a bot/judge createdById across every instance, so an unscoped block-check locked out any user who'd ever blocked that one account from tracking any daily challenge (same bug class as #3294). Scope the exclusion to source === 'User', and only apply it (plus the status gate) on the tracking path so untracking a stale subscription always works, even with the creator blocked. Strengthen the create/delete test assertions to check call args, not just call counts, and add regression tests for the System-source bypass and the untrack-while-blocked path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(challenges): expose toggleNotify and getTrackedIds procedures Surfaces the Task 3 challenge-engagement service over tRPC so the UI can toggle "notify me" tracking and read the viewer's tracked ids. * feat(challenges): auto-track a user challenge for its creator Write goes through the same transaction as the challenge create (inline tx.challengeEngagement.create) rather than the engagement service, to avoid a cycle with challenge.service.ts. Updates the collection-owner wiring test's tx mock, which didn't stub challengeEngagement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(challenges): pin the creator auto-track engagement payload The prior mockTx.challengeEngagement.create stub only prevented a crash; nothing asserted the write's shape, so a wrong type/id/userId would pass silently. Extends the existing create-path test with a toHaveBeenCalledWith assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(challenges): add go-live, ending-soon, results and follow notifications Registers four discovery notification types (challenge-starting, new-challenge-from-following, challenge-ending-soon, challenge-results) as toggleable Update-category notifications, distinct from the existing outcome notifications which stay non-toggleable System-category. The two SQL-driven types gate on source/ingestion/status to avoid announcing hidden, unscanned, blocked or cancelled challenges, and use a wall-clock floor (not a row-cap cursor) to bound the scan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(challenges): notify trackers when a challenge goes live Fires challenge-starting for everyone tracking a challenge, right after the activation job wins the setChallengeActive claim. Wrapped in its own try/catch so a notification failure can never undo or block activation itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(challenges): give the lost-claim notify test real teeth It never overrode getChallengeNotifyRecipients, so the [] default made createNotification.not.toHaveBeenCalled() pass regardless of whether the won-claim guard actually ran — the assertion proved nothing about the one behavior this task exists to protect (no double-notify on a lost claim). Force non-empty recipients so the guard is what makes it pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(challenges): notify trackers and entrants when results are announced Winners already get their own challenge-winner notification; this adds a best-effort companion send (trackers ∪ entrants, minus winners) from both the manual and system completion paths so nobody is double-notified. * fix(challenges): exclude participation-prize earners from results notification Both endChallengeAndPickWinners and pickWinnersForChallenge were only excluding winners from the challenge-results fan-out, not participation- prize earners — so a non-winning participant who earned the entry prize got both challenge-participation and challenge-results for the same outcome. Merge participation-prize user ids into excludeUserIds at both call sites; also await the axiom log in the helper's catch, matching sibling call sites. * feat(challenges): add challenge notify toggle component and hooks Client-side pieces for challenge go-live notifications: useTrackedChallengeIds and useToggleChallengeNotify (optimistic cancel/snapshot/rollback against challenge.getTrackedIds, idiom copied from FollowUserButton.tsx) plus the shared ChallengeNotifyToggle bell/button component three surfaces will mount next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(challenges): annotate TRACKABLE array to keep ChallengeStatus assignable TypeScript inferred a ("Scheduled" | "Active")[] literal-union array type, so TRACKABLE.includes(challenge.status) rejected the wider ChallengeStatus argument (TS2345). Vitest doesn't type-check so this only surfaced in tsc/next build. Explicit ChallengeStatus[] annotation matches the proven pattern in challenge-engagement.service.ts:14. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(challenges): cover ChallengeNotifyToggle render-gating and click isolation Zero coverage on the component three surfaces (card link, dropdown, detail page) are about to mount next: logged-out/non-trackable-status gating, tracked-state affordance text, toggleNotify called with the inverted setTo, and — the one this file exists to catch — a dropped stopPropagation() letting a bell click bubble into the wrapping card's navigation handler. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(challenges): surface the notify toggle on cards, menu and detail page Mounts ChallengeNotifyToggle on the challenge card header, the context menu (as a Notify me/Stop notifying me item), and the detail page's status-badge row. The menu's early-return guard is loosened from `!canDelete` to `!canDelete && !canTrack` so non-owners get a menu at all; the existing Delete item is now explicitly gated on `canDelete` so non-owners never see it, with a regression test covering that split. * feat(challenges): add Tracking filter to Your Challenges * fix(challenges): close visibility gaps in the go-live notification wave new-challenge-from-following gated only on the cover image level, but Challenge.nsfwLevel is the MAX level a challenge admits — an X-rated challenge with a PG cover was pushed to PG-only followers who can never open it. Gate on both levels, matching the feed, and drop POI covers (hidden from feed and detail alike, so the notification led to a 404). challenge-ending-soon crossed its 24h mark at go-live for any challenge <= 24h long (every daily, and CHALLENGE_MIN_DURATION_HOURS user challenges), racing the hourly activation cron for a status=Active it usually lost — and the per-challenge key gives no second chance. Skip those; challenge-starting already covers them. toggleChallengeNotify enforced only the block gate, so any authed user could track a challenge above their browsing level, still unscanned, or not yet visible — sequential ids make that enumerable. Apply the same gates the feed and getChallengeDetail apply, with the creator exempt. Also: entrant derivation now honours CollectionItem.status so a rejected-only entrant isn't reminded; the context menu no longer renders an empty dropdown for logged-out visitors; untracking invalidates the feed so the card leaves the Tracking tab; and toggleNotify no longer rejects unhandled at its `void` call sites. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(challenges): relocate the notify toggle to the menu and header A bell in the challenge card header overlapped the status/countdown badge row, and on the detail page it sat mid-badge-row away from the other per-challenge actions. Cards now reach tracking through the overflow menu item they already had, and the detail page renders the bell in the title row's action group beside share and the overflow menu, sized to match them. The `button` variant of ChallengeNotifyToggle loses its only caller with that move, so it and the `variant`/`size` props go with it; the state-dependent label survives as the tooltip and accessible name. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * feat(challenges): remind trackers 8 hours before a challenge closes challenge-ending-soon fired 24 hours out, which is also the minimum challenge duration — so every daily challenge and every minimum-length user challenge crossed its own mark at go-live and was skipped entirely. At 8 hours the reminder lands inside the challenge's life instead of at its start, leaving a full working day to enter, and those challenges now get it. The short-challenge guard stays for the degenerate case where a challenge is no longer than the window itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(lint): let ESLint actually lint test files tsconfig.json excludes src/**/__tests__/**, but .eslintrc.js points the type-aware parser at that tsconfig for every .ts/.tsx. Linting a test file therefore died with "TSConfig does not include this file" — a fatal parse error, which means NO rule runs on the file at all. That silently disabled prettier/prettier there, and since the editor formats via source.fixAll.eslint (editor.formatOnSave is off), test files never got formatted on save and only failed in the CI prettier gate. Drop the project reference for test globs; nothing in the overrides block needs type info. restrict-template-expressions is the one type-aware rule in the shared set and errors at load time without a project, so it's off for tests too. Also format the three test files that slipped through. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(vscode): point the workspace at the repo's TypeScript on any platform typescript.tsdk carried Windows separators, so the workspace TypeScript never resolved on macOS or Linux. Forward slashes work everywhere — VS Code resolves the path against the workspace folder and normalizes it. Moved to the current js/ts.* setting ids, and added the workspace-version prompt: without it VS Code keeps its bundled TypeScript until someone runs "Select TypeScript Version" by hand, so the path alone changes nothing for anyone who hasn't already done that. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>