Files
civitai__civitai/.vscode
Manuel Emilio Urena df3d68a9d8 feat(challenges): go-live and follow notifications (#3372)
* 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>
2026-07-25 14:39:55 -04:00
..
2025-06-18 15:20:59 -04:00
2025-08-25 12:23:45 -06:00
2022-12-19 16:31:44 -07:00
2022-11-10 15:02:59 +00:00
2022-12-19 16:31:44 -07:00