Make the portal the sole source of Community Points - #975
Conversation
Retires the MEE6 distribution loop: the daily XP sync cron and the steward Discord XP page are removed, and stewards now award event CP directly on the portal. Ten per-activity community event contribution types back a new Community Awards page (paste from Excel/Sheets, preview with click-to-pick ambiguity resolution, atomic idempotent confirm, recent-awards list with per-row delete). A new Discord bot answers /rank and /points and gives stewards /give-cp and /remove-cp, riding the existing backend as a signed Discord Interactions webhook with no gateway process. The effective-CP formula and the frozen MEE6 baseline are untouched; the state model and its signals remain as load-bearing joins. Hardening from review: awards carry a durable notes marker so historical contributions on reused slugs are never listable or revocable; the seed migration guarantees a 1:1 multiplier at cutover and drops future-dated periods; give-cp and remove-cp consume interaction ids durably (survives revocation, blocks replays alongside a 300s signature freshness window); commands are refused outside the configured guild; racing removals are serialized; hidden accounts never appear in match candidates; steward attribution uses user ids instead of emails; bot replies never resolve mentions; the retired MEE6 level/rank profile display is gone. ## Claude Implementation Notes - backend/contributions/migrations/0086_community_event_types.py: seed 10 event types; ensure active 1.0 multiplier, drop future periods; contribution-safe reverse - backend/contributions/community_awards.py: parsing, tiered Discord matching (visible-only candidates), award/revoke services, AWARD_NOTES_PREFIX scoping, StewardCommunityAwardViewSet (preview/confirm/list/delete, lock-serialized batch idempotency) - backend/contributions/views.py, serializers.py, api/urls.py: remove StewardDiscordXPViewSet/filterset/serializers; register steward-community-awards + discord_bot urls - backend/discord_bot/: interactions webhook (Ed25519 + 300s freshness + guild check), /rank + steward command handlers, ProcessedInteraction model (0001), commands catalog, register_discord_commands command - backend/community_xp/utils.py + leaderboard/views.py: extract get_cached_community_ranking() (floor 2500) shared by leaderboard and bot - backend/community_xp/admin.py + mee6syncrun/change_list.html: emergency-only warning on fetch/apply - backend/tally/settings.py, .env.example, deploy-apprunner*.sh: DISCORD_APPLICATION_ID + DISCORD_PUBLIC_KEY; discord_bot in INSTALLED_APPS - .github/workflows/sync-discord-xp.yml: deleted (code-side cutoff) - backend/contributions/tests/: test_community_awards.py new; test_discord_xp.py pruned to model/signal tests; test_is_submittable.py page_size=100 vs seeded types - backend/discord_bot/tests/: signature/PING/rank/guild tests, steward command idempotency + scoping, register command - frontend/src/routes/StewardCommunityAwards.svelte + lib/communityAwards.js: new awards page (live duplicate warnings) + shared awardable slugs - frontend/src/App.svelte, components/Sidebar.svelte, lib/api.js: route/nav/API swap to community awards - frontend/src/routes/StewardDiscordXP.svelte, lib/xpSearchToParams.js: deleted; StewardSearchBar.svelte XP variant stripped - frontend/src/components/SocialLink.svelte, profile/ProfileHeader.svelte: MEE6 level/rank tooltip blocks removed - frontend/src/routes/Dashboard.svelte + tests/communityDashboardRankings.test.js: "All-time Community Points" wording - backend/CLAUDE.md, frontend/CLAUDE.md: docs updated
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe change replaces steward Discord XP distribution with portal and Discord Community Points awards. It adds signed Discord interactions, awardable event types, cached ranking snapshots, a steward award interface, and removes retired XP workflows and presentation. ChangesCommunity awards cutover
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant Discord
participant Webhook
participant CommandHandler
participant ProcessedInteraction
participant CommunityAwardService
Discord->>Webhook: Send signed slash-command interaction
Webhook->>Webhook: Verify signature and guild
Webhook->>CommandHandler: Route command
CommandHandler->>ProcessedInteraction: Record interaction ID
CommandHandler->>CommunityAwardService: Create or revoke Community Points award
CommunityAwardService-->>CommandHandler: Return award result
CommandHandler-->>Discord: Send ephemeral response
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 11
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backend/community_xp/admin.py`:
- Around line 18-21: Require a server-side break-glass control in the MEE6 sync
admin flow before apply_sync_run() can execute, and record the acting user and
emergency reason for each approved re-baseline. In backend/community_xp/admin.py
lines 18-21, enforce this guard server-side rather than relying on the warning.
In
backend/community_xp/templates/admin/community_xp/mee6syncrun/change_list.html
lines 3-12, hide or disable the apply workflow whenever the break-glass control
is inactive.
In `@backend/contributions/migrations/0086_community_event_types.py`:
- Around line 83-86: Update the reverse logic in the migration’s reverse
function to avoid deleting reused seeded contribution types, specifically
excluding the existing community-event type from the queryset or otherwise
leaving seeded records intact. Preserve deletion only for migration-created
event types with no contributions, and prevent cascading removal of the reused
type’s multiplier history.
In `@backend/contributions/tests/test_community_awards.py`:
- Line 188: Replace the force_authenticate setup in the community awards tests
with a real wallet session created via
ethereum_auth.testing.login_wallet_session(client, user), and add at least one
test using that helper while preserving the existing authenticated endpoint
assertions.
- Around line 68-71: Update test_row_cap to assert
rest_framework.serializers.ValidationError instead of the broad Exception when
calling parse_award_lines, preserving the existing row-cap input and failure
assertion.
In `@backend/contributions/tests/test_is_submittable.py`:
- Around line 126-129: Update the request in test_filter_by_is_submittable_true
to include page_size=100, matching the pagination safeguard already used by the
nearby unfiltered test. Keep the test’s true-filter behavior and other request
parameters unchanged.
In `@backend/discord_bot/tests/test_steward_commands.py`:
- Around line 119-132: The `/give-cp` tests do not cover rejection of a linked
target whose Steward record has visible=False. Add a test alongside
test_give_to_unlinked_member_is_ephemeral that creates or configures the target
through _linked_user with visible=False, invokes handle_give_cp, and asserts an
ephemeral “not visible” response with no Contribution created.
In `@backend/discord_bot/views.py`:
- Around line 48-53: Update the guild validation around expected_guild so
application commands are rejected when DISCORD_GUILD_ID is unset or when it
differs from interaction["guild_id"]. Remove the conditional bypass for an empty
expected_guild while preserving the existing rejection response for both cases.
In `@frontend/src/App.svelte`:
- Line 316: Optionally add a legacy `/stewards/discord-xp` route alongside the
steward routes in the App route configuration, redirecting it to the replacement
steward destination with `replace()` so browser history does not loop. Follow
the existing legacy alias patterns used for `/community/referrals` and
`/foundations`.
In `@frontend/src/components/Sidebar.svelte`:
- Around line 25-30: Extract the duplicated award-eligibility predicate into
awardableTypesFor in communityAwards.js, using AWARDABLE_EVENT_SLUGS and safe
defaults for contributionTypes and permissionsMap. Replace the inline filtering
in Sidebar.svelte and StewardCommunityAwards.svelte with this shared helper so
both the sidebar link and page gate use identical rules.
In `@frontend/src/routes/StewardCommunityAwards.svelte`:
- Line 86: Replace the immediate subscribe/unsubscribe pattern in the
stewardPermissions read with Svelte’s get() helper, adding the required
svelte/store import and preserving the existing permissionsMap fallback to an
empty object.
- Around line 463-464: Update the card wrapper div around the horizontally
scrollable table to replace overflow-hidden with overflow-x-clip, preserving the
existing rounded, border, background, and shadow classes while leaving vertical
overflow visible for the inner overflow-x-auto container.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: ce1bc939-05b0-47af-aabe-a42ce69c53d6
📒 Files selected for processing (51)
.github/workflows/sync-discord-xp.ymlCHANGELOG.mdbackend/.env.examplebackend/CLAUDE.mdbackend/api/urls.pybackend/community_xp/admin.pybackend/community_xp/templates/admin/community_xp/mee6syncrun/change_list.htmlbackend/community_xp/utils.pybackend/contributions/community_awards.pybackend/contributions/migrations/0086_community_event_types.pybackend/contributions/serializers.pybackend/contributions/tests/test_community_awards.pybackend/contributions/tests/test_discord_xp.pybackend/contributions/tests/test_is_submittable.pybackend/contributions/views.pybackend/deploy-apprunner-dev.shbackend/deploy-apprunner.shbackend/discord_bot/__init__.pybackend/discord_bot/apps.pybackend/discord_bot/commands_catalog.pybackend/discord_bot/handlers.pybackend/discord_bot/management/__init__.pybackend/discord_bot/management/commands/__init__.pybackend/discord_bot/management/commands/register_discord_commands.pybackend/discord_bot/migrations/0001_initial.pybackend/discord_bot/migrations/__init__.pybackend/discord_bot/models.pybackend/discord_bot/responses.pybackend/discord_bot/steward_commands.pybackend/discord_bot/tests/__init__.pybackend/discord_bot/tests/test_interactions.pybackend/discord_bot/tests/test_register_commands.pybackend/discord_bot/tests/test_steward_commands.pybackend/discord_bot/urls.pybackend/discord_bot/verification.pybackend/discord_bot/views.pybackend/leaderboard/views.pybackend/tally/settings.pyfrontend/CLAUDE.mdfrontend/src/App.sveltefrontend/src/components/Sidebar.sveltefrontend/src/components/SocialLink.sveltefrontend/src/components/StewardSearchBar.sveltefrontend/src/components/profile/ProfileHeader.sveltefrontend/src/lib/api.jsfrontend/src/lib/communityAwards.jsfrontend/src/lib/xpSearchToParams.jsfrontend/src/routes/Dashboard.sveltefrontend/src/routes/StewardCommunityAwards.sveltefrontend/src/routes/StewardDiscordXP.sveltefrontend/src/tests/communityDashboardRankings.test.js
💤 Files with no reviewable changes (3)
- .github/workflows/sync-discord-xp.yml
- frontend/src/lib/xpSearchToParams.js
- frontend/src/routes/StewardDiscordXP.svelte
| # Post-cutover: the portal is the sole CP source and the daily MEE6 sync | ||
| # cron is gone. Fetch/apply are kept as EMERGENCY tools only — applying a | ||
| # new run re-baselines from a dead MEE6 board and can lose CP awarded since | ||
| # the final baseline. Do not run them without checking with the team. |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Enforce the emergency restriction before a MEE6 re-baseline.
The warning does not stop a user with Django change permission from applying a new snapshot. That action can discard Community Points awarded after the final MEE6 baseline.
backend/community_xp/admin.py#L18-L21: require a server-side break-glass control beforeapply_sync_run()can execute. Record the actor and emergency reason.backend/community_xp/templates/admin/community_xp/mee6syncrun/change_list.html#L3-L12: hide or disable the apply workflow unless the server-side break-glass control is active.
The PR objective retains MEE6 sync only for emergency use.
📍 Affects 2 files
backend/community_xp/admin.py#L18-L21(this comment)backend/community_xp/templates/admin/community_xp/mee6syncrun/change_list.html#L3-L12
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@backend/community_xp/admin.py` around lines 18 - 21, Require a server-side
break-glass control in the MEE6 sync admin flow before apply_sync_run() can
execute, and record the acting user and emergency reason for each approved
re-baseline. In backend/community_xp/admin.py lines 18-21, enforce this guard
server-side rather than relying on the warning. In
backend/community_xp/templates/admin/community_xp/mee6syncrun/change_list.html
lines 3-12, hide or disable the apply workflow whenever the break-glass control
is inactive.
| let canAccessCommunityAwards = $derived( | ||
| communityContributionTypes.some(type => | ||
| AWARDABLE_EVENT_SLUGS.includes(type.slug) && | ||
| (stewardPermissionMap[String(type.id)] || []).includes('accept') | ||
| ) | ||
| ); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win
Extract the award-permission predicate into the shared module.
This predicate is duplicated in frontend/src/routes/StewardCommunityAwards.svelte at lines 89-92. Both places filter community contribution types by AWARDABLE_EVENT_SLUGS and require the accept permission. If the eligibility rule changes, the sidebar link and the page gate can disagree, and a steward can see a link to a page that then denies access.
Move the rule into src/lib/communityAwards.js and call it from both files.
Based on learnings, the coding guidelines state: "Use shared helpers instead of duplicating domain logic" for frontend/src/lib/*.js.
♻️ Proposed refactor
Add the helper to frontend/src/lib/communityAwards.js:
// Community event types the steward may award CP on, given their permissions.
export function awardableTypesFor(contributionTypes, permissionsMap) {
return (contributionTypes || []).filter(type =>
AWARDABLE_EVENT_SLUGS.includes(type.slug) &&
((permissionsMap || {})[String(type.id)] || []).includes('accept')
);
}Then use it in Sidebar.svelte:
- import { AWARDABLE_EVENT_SLUGS } from '../lib/communityAwards.js';
+ import { awardableTypesFor } from '../lib/communityAwards.js'; let canAccessCommunityAwards = $derived(
- communityContributionTypes.some(type =>
- AWARDABLE_EVENT_SLUGS.includes(type.slug) &&
- (stewardPermissionMap[String(type.id)] || []).includes('accept')
- )
+ awardableTypesFor(communityContributionTypes, stewardPermissionMap).length > 0
);And in StewardCommunityAwards.svelte:
- eventTypes = allTypes.filter(type =>
- AWARDABLE_EVENT_SLUGS.includes(type.slug) &&
- (permissionsMap[String(type.id)] || []).includes('accept')
- );
+ eventTypes = awardableTypesFor(allTypes, permissionsMap);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/src/components/Sidebar.svelte` around lines 25 - 30, Extract the
duplicated award-eligibility predicate into awardableTypesFor in
communityAwards.js, using AWARDABLE_EVENT_SLUGS and safe defaults for
contributionTypes and permissionsMap. Replace the inline filtering in
Sidebar.svelte and StewardCommunityAwards.svelte with this shared helper so both
the sidebar link and page gate use identical rules.
Source: Coding guidelines
The Discord webhook now fails closed when no guild is configured, so a misconfigured deployment cannot accept commands from anywhere (PING still answers for endpoint verification). The seed migration's reverse is a conservative no-op so a rollback can never cascade away the reused community-event type's configuration. The old steward Discord XP URL aliases to the Community Awards page for stale bookmarks, the award-permission rule is shared between the sidebar and the page gate, and the recent-awards card clips horizontal bleed without trapping mobile scroll. ## Claude Implementation Notes - backend/discord_bot/views.py: reject application commands when DISCORD_GUILD_ID is unset (fail closed), not just on mismatch - backend/contributions/migrations/0086_community_event_types.py: reverse is RunPython.noop; delete-by-slug risked cascading the reused community-event type - backend/discord_bot/tests/test_interactions.py: unset-guild fail-closed test - backend/discord_bot/tests/test_steward_commands.py: hidden-target /give-cp refusal test - backend/contributions/tests/test_community_awards.py: assert serializers.ValidationError on the row cap; wallet-session auth test via login_wallet_session - backend/contributions/tests/test_is_submittable.py: page_size=100 on the true-filter test (same seeded-types hazard as the others) - frontend/src/lib/communityAwards.js: awardableTypesFor() shared predicate - frontend/src/components/Sidebar.svelte + routes/StewardCommunityAwards.svelte: use the shared predicate; one-shot store read via get(); overflow-x-clip on the awards card - frontend/src/App.svelte: /stewards/discord-xp legacy alias to the Community Awards page
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/routes/StewardCommunityAwards.svelte (1)
148-151: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winUse the committed batch summary for idempotent retries.
If the first confirmation commits but its response is lost, the steward can change
rowsbefore retrying the samebatchId. This branch then reports the currentincludedRowsandincludedPoints, not the committed batch. Use the server’s stored summary, or freeze the submitted summary bybatchId.Based on the idempotent
confirmCommunityAwardscontract, the repeated response must describe the committed batch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/routes/StewardCommunityAwards.svelte` around lines 148 - 151, Update the already_processed branch in the confirmation response handling to report the committed batch summary returned by confirmCommunityAwards, rather than the current includedRows and includedPoints. Preserve the existing non-repeated response mapping and ensure repeated responses describe the original batch identified by batchId.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@frontend/src/routes/StewardCommunityAwards.svelte`:
- Around line 148-151: Update the already_processed branch in the confirmation
response handling to report the committed batch summary returned by
confirmCommunityAwards, rather than the current includedRows and includedPoints.
Preserve the existing non-repeated response mapping and ensure repeated
responses describe the original batch identified by batchId.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: f56ccc9d-beca-4558-b09a-42bb2fd7a3cf
📒 Files selected for processing (10)
backend/contributions/migrations/0086_community_event_types.pybackend/contributions/tests/test_community_awards.pybackend/contributions/tests/test_is_submittable.pybackend/discord_bot/tests/test_interactions.pybackend/discord_bot/tests/test_steward_commands.pybackend/discord_bot/views.pyfrontend/src/App.sveltefrontend/src/components/Sidebar.sveltefrontend/src/lib/communityAwards.jsfrontend/src/routes/StewardCommunityAwards.svelte
CI builds the test database with migrations disabled, so contribution types
created by a data migration do not exist there. The community award tests now
create the awardable event types through a shared, idempotent fixture helper,
which keeps them passing both locally and in CI.
## Claude Implementation Notes
- backend/contributions/testing.py: new ensure_community_event_types() helper (repo {app}/testing.py convention); creates the community category, the 10 awardable types and their 1.0 multipliers idempotently
- backend/contributions/tests/test_community_awards.py: build types via the helper instead of ContributionType.objects.get(slug=...)
- backend/discord_bot/tests/test_interactions.py, test_steward_commands.py: same; award() uses the helper's types
- backend/CLAUDE.md: document that CI runs tally.test_settings with migrations disabled, plus the command to reproduce it locally
/points was an exact alias of /rank, so it only added a second identical entry to the command picker; it is gone. The remaining commands answer with embeds instead of a single run-on line: /rank shows Community Points, rank and Builder standing as separate fields, brand-coloured, with the member's Discord avatar as thumbnail and a title linking to their portal profile, and tells unranked members what the threshold is. Steward award and removal confirmations get the same treatment, while every denial or error stays ephemeral plain text. ## Claude Implementation Notes - backend/discord_bot/responses.py: channel_message() takes optional content/embeds; BRAND_COLOR (#19A663) - backend/discord_bot/handlers.py: /rank builds an embed (fields, thumbnail from DiscordConnection.avatar_url, profile URL, unranked footer naming COMMUNITY_RANKING_MIN_POINTS); router no longer accepts 'points' - backend/discord_bot/commands_catalog.py: 'points' definition removed - backend/discord_bot/steward_commands.py: give-cp/remove-cp confirmations as embeds - backend/discord_bot/tests/: assert embed fields instead of content; register test expects 3 commands - backend/CLAUDE.md: document embed replies and the removed alias
Awarding rights are configured in the portal admin through steward records and per-type permissions, so gating the Discord commands on Manage Server was wrong in both directions: it hid them from stewards who are not Discord admins and offered them to Discord admins who are not stewards. The award commands are now hidden by default and a guild administrator grants specific roles access from the Discord integrations screen, while the portal check still decides every actual award. Ranking stays open to every member, and a rejected user is now told whether they need to link their account or request steward access. ## Claude Implementation Notes - backend/discord_bot/commands_catalog.py: default_member_permissions '0' (admin-only until roles are granted via Integrations) replacing MANAGE_GUILD '32'; /rank keeps no gate - backend/discord_bot/steward_commands.py: _steward_invoker distinguishes unlinked account from missing steward access; notes that portal admin is the source of truth - backend/discord_bot/tests/test_register_commands.py: assert '0' on give-cp and that /rank has no default_member_permissions - backend/CLAUDE.md: document the two layers (portal authorization vs Discord visibility) and that a bot token cannot set command permissions
The card repeated the word Community in adjacent labels and rendered every value at the same weight, so the labels competed with the numbers they were describing. The points total is now the largest element with a small muted caption beneath it carrying the category and rank, builder standing follows in a smaller block, and the top three placements earn a medal. Members who are not yet ranked are told exactly how many points they still need instead of just being called unranked. ## Claude Implementation Notes - backend/discord_bot/handlers.py: /rank builds a markdown description (## heading + -# subtext) instead of inline fields; medal for ranks 1-3; unranked shows the remaining CP to the floor; footer folded into the caption - backend/discord_bot/tests/test_interactions.py: assert on description structure rather than field names
Ranking previously only ever reported the caller's own standing, so comparing with someone else meant asking them to run it. It now takes an optional member argument and reports that person instead. Community standing is already public on the portal leaderboard, so this exposes nothing new, and accounts hidden from those leaderboards answer exactly as unlinked accounts do so a lookup cannot confirm they exist or reveal their points. ## Claude Implementation Notes - backend/discord_bot/handlers.py: handle_rank resolves the optional user option and falls back to the invoker; card building extracted to _rank_embed(user); hidden/unlinked targets share one ephemeral reply; resolved_user_label() helper added - backend/discord_bot/commands_catalog.py: /rank gains the optional user option (requires re-running register_discord_commands) - backend/discord_bot/steward_commands.py: reuse resolved_user_label instead of its own inline copy - backend/discord_bot/tests/test_interactions.py: lookup of another member, hidden-target non-disclosure, unlinked-target cases - backend/CLAUDE.md: document the lookup and its privacy rule
The card had grown tall and sparse: a large square avatar sat beside five stacked lines of text, and the builder block repeated the same two-line shape as the community block. The avatar now sits as a small round icon inline with the name, and builder standing collapses into a single compact row, so the card reads as one tight block with the points total still leading. ## Claude Implementation Notes - backend/discord_bot/handlers.py: _rank_embed uses author (name + icon_url + profile url) instead of title + thumbnail; builder moves from three description lines to one inline field; description is now just the headline and its caption - backend/discord_bot/tests/test_interactions.py: assert on author name rather than title
Builder points sat in a labelled row while community points led with the
number, so the two halves of the card read as different kinds of information.
Builder now follows the same shape — the points total first, its rank in a
muted caption underneath — one size down so it still reads as secondary.
## Claude Implementation Notes
- backend/discord_bot/handlers.py: builder block returns to the description as '### {n} BP' plus a '-# Builder · Rank #n' caption instead of an inline field; BP matches the portal's own label for Builder Points
The award preview returned per-row duplicate flags, a summary block and the selected type, none of which the page reads: it recomputes counts and duplicate warnings itself after the steward resolves ambiguous rows, so a server-side snapshot was both unused and liable to go stale. Preview also accepted an event name it never used. The consumed-interaction record kept a command label nothing queried, and the awards page guarded date formatting with a catch that could never fire, since an unparseable date returns a string rather than throwing. ## Claude Implementation Notes - backend/contributions/community_awards.py: preview returns batch_id + rows only; duplicate_user/summary/contribution_type_id and the matched-count pass removed; single-caller batch_already_processed inlined into confirm - backend/discord_bot/models.py + migrations/0001_initial.py: drop the command field (only read by an unused __str__); column never shipped so the initial migration is edited rather than superseded - backend/discord_bot/steward_commands.py: drop the command kwarg from both get_or_create calls - frontend/src/routes/StewardCommunityAwards.svelte: formatDate guards the parsed date instead of wrapping a non-throwing call in try/catch; preview no longer sends the unused event_name - backend/contributions/tests/test_community_awards.py: assert the rows rather than the removed summary/duplicate flags - backend/CLAUDE.md, frontend/CLAUDE.md: trim the awards section to the non-obvious rules instead of repeating the endpoint table
Brings in the mobile MetaMask sign-in change (#976). The only conflict was CHANGELOG.md, where dev and this branch each added an entry at the top of Unreleased; both are unrelated user-facing changes, so both are kept with the newer one first. frontend/CLAUDE.md merged cleanly — dev documented MetaMask routing while this branch documented the community awards API, in different sections.
Retires MEE6 XP distribution: the daily sync cron and the steward Discord XP page are removed, and stewards now award community event CP directly on the portal. Ten per-activity community event contribution types back a new steward Community Awards page with bulk paste from Excel/Sheets, match preview with click-to-pick ambiguity resolution, atomic idempotent confirm, and a recent-awards list with per-row delete. A Discord bot rides the existing backend as a signed interactions webhook: /rank and /points for everyone, /give-cp and /remove-cp for stewards, registered via a management command. The effective CP formula and the frozen MEE6 baseline are untouched, the MEE6 sync endpoint is kept as an emergency-only tool, and the retired MEE6 level/rank profile display is gone. Hardening includes marker-scoped awards so historical contributions on reused slugs are never revocable, a guaranteed 1:1 multiplier at cutover, durable consumption of Discord interaction ids with a signature replay window, guild-restricted commands, and steward attribution by user id instead of email.
Summary by CodeRabbit
New Features
Changes