Skip to content

Make the portal the sole source of Community Points - #975

Open
JoaquinBN wants to merge 12 commits into
devfrom
JoaquinBN/xp-to-cp-migration-plan
Open

Make the portal the sole source of Community Points#975
JoaquinBN wants to merge 12 commits into
devfrom
JoaquinBN/xp-to-cp-migration-plan

Conversation

@JoaquinBN

@JoaquinBN JoaquinBN commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

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

    • Added a Community Awards page for stewards to preview, confirm, search, and remove event-based Community Point awards.
    • Added Discord commands for viewing rank and points, awarding Community Points, and removing awards.
    • Added community-event contribution types and leaderboard eligibility thresholds.
  • Changes

    • Retired daily MEE6 synchronization and related Discord XP management screens.
    • Updated Discord profile and dashboard displays to emphasize roles and Community Points.
    • Added safeguards against duplicate, ambiguous, or invalid awards.

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
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The 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.

Changes

Community awards cutover

Layer / File(s) Summary
Award data, services, APIs, and rankings
backend/contributions/..., backend/community_xp/utils.py, backend/leaderboard/views.py, backend/api/urls.py, backend/contributions/tests/...
Adds awardable event types, parsing, Discord-user matching, atomic confirmation, scoped deletion, ranking snapshots, and validation tests.
Signed Discord commands and idempotency
backend/discord_bot/..., backend/tally/settings.py, backend/deploy-apprunner*.sh
Adds signed webhook handling, rank and point commands, steward award commands, command registration, processed-interaction storage, and runtime configuration.
Community Awards steward interface
frontend/src/routes/StewardCommunityAwards.svelte, frontend/src/lib/api.js, frontend/src/components/Sidebar.svelte, frontend/src/App.svelte
Replaces the Discord XP route with preview, confirmation, search, pagination, and deletion flows.
Legacy XP retirement and presentation cleanup
backend/contributions/views.py, backend/contributions/serializers.py, frontend/src/components/*, frontend/src/components/StewardSearchBar.svelte, backend/community_xp/*
Removes retired XP APIs and UI behavior, updates MEE6 synchronization warnings, and removes XP-specific Discord display and search logic.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the primary change: retiring MEE6 distribution and making the portal the sole source of Community Points.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch JoaquinBN/xp-to-cp-migration-plan

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 5c66d56 and 15213bc.

📒 Files selected for processing (51)
  • .github/workflows/sync-discord-xp.yml
  • CHANGELOG.md
  • backend/.env.example
  • backend/CLAUDE.md
  • backend/api/urls.py
  • backend/community_xp/admin.py
  • backend/community_xp/templates/admin/community_xp/mee6syncrun/change_list.html
  • backend/community_xp/utils.py
  • backend/contributions/community_awards.py
  • backend/contributions/migrations/0086_community_event_types.py
  • backend/contributions/serializers.py
  • backend/contributions/tests/test_community_awards.py
  • backend/contributions/tests/test_discord_xp.py
  • backend/contributions/tests/test_is_submittable.py
  • backend/contributions/views.py
  • backend/deploy-apprunner-dev.sh
  • backend/deploy-apprunner.sh
  • backend/discord_bot/__init__.py
  • backend/discord_bot/apps.py
  • backend/discord_bot/commands_catalog.py
  • backend/discord_bot/handlers.py
  • backend/discord_bot/management/__init__.py
  • backend/discord_bot/management/commands/__init__.py
  • backend/discord_bot/management/commands/register_discord_commands.py
  • backend/discord_bot/migrations/0001_initial.py
  • backend/discord_bot/migrations/__init__.py
  • backend/discord_bot/models.py
  • backend/discord_bot/responses.py
  • backend/discord_bot/steward_commands.py
  • backend/discord_bot/tests/__init__.py
  • backend/discord_bot/tests/test_interactions.py
  • backend/discord_bot/tests/test_register_commands.py
  • backend/discord_bot/tests/test_steward_commands.py
  • backend/discord_bot/urls.py
  • backend/discord_bot/verification.py
  • backend/discord_bot/views.py
  • backend/leaderboard/views.py
  • backend/tally/settings.py
  • frontend/CLAUDE.md
  • frontend/src/App.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/components/SocialLink.svelte
  • frontend/src/components/StewardSearchBar.svelte
  • frontend/src/components/profile/ProfileHeader.svelte
  • frontend/src/lib/api.js
  • frontend/src/lib/communityAwards.js
  • frontend/src/lib/xpSearchToParams.js
  • frontend/src/routes/Dashboard.svelte
  • frontend/src/routes/StewardCommunityAwards.svelte
  • frontend/src/routes/StewardDiscordXP.svelte
  • frontend/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

Comment on lines +18 to +21
# 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 before apply_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.

Comment thread backend/contributions/migrations/0086_community_event_types.py Outdated
Comment thread backend/contributions/tests/test_community_awards.py
Comment thread backend/contributions/tests/test_community_awards.py
Comment thread backend/contributions/tests/test_is_submittable.py
Comment thread backend/discord_bot/views.py
Comment thread frontend/src/App.svelte
Comment on lines +25 to 30
let canAccessCommunityAwards = $derived(
communityContributionTypes.some(type =>
AWARDABLE_EVENT_SLUGS.includes(type.slug) &&
(stewardPermissionMap[String(type.id)] || []).includes('accept')
)
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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

Comment thread frontend/src/routes/StewardCommunityAwards.svelte Outdated
Comment thread frontend/src/routes/StewardCommunityAwards.svelte Outdated
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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Use the committed batch summary for idempotent retries.

If the first confirmation commits but its response is lost, the steward can change rows before retrying the same batchId. This branch then reports the current includedRows and includedPoints, not the committed batch. Use the server’s stored summary, or freeze the submitted summary by batchId.

Based on the idempotent confirmCommunityAwards contract, 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

📥 Commits

Reviewing files that changed from the base of the PR and between 15213bc and c6aae55.

📒 Files selected for processing (10)
  • backend/contributions/migrations/0086_community_event_types.py
  • backend/contributions/tests/test_community_awards.py
  • backend/contributions/tests/test_is_submittable.py
  • backend/discord_bot/tests/test_interactions.py
  • backend/discord_bot/tests/test_steward_commands.py
  • backend/discord_bot/views.py
  • frontend/src/App.svelte
  • frontend/src/components/Sidebar.svelte
  • frontend/src/lib/communityAwards.js
  • frontend/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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant