Skip to content

feat(bonus): bonus points, shop, avatars and bounties - #55

Merged
florianjs merged 8 commits into
mainfrom
feat/bonus-shop
Oct 8, 2026
Merged

florianjs merged 8 commits into
mainfrom
feat/bonus-shop

Conversation

@florianjs

Copy link
Copy Markdown
Owner

⚠️ Merge #54 (i18n) first. This branch is built on top of it. Once #54 is in main, this PR's diff shrinks to the bonus commits only. It targets main directly to avoid the stacked-PR retargeting issue we hit with #52.

Summary

Bonus points, a shop and bounties (#48). All rates and prices are set by the admin.

Earning points

  • Seeding: pointsPerSeedDay per torrent per 24h, accrued on each seeder announce. It is limited to:
    • torrents the user uploaded or really downloaded (an HnR snatch entry exists), so a client-supplied left=0 cannot farm points on any torrent;
    • at most maxSeedingTorrents torrents earning at the same time.
  • Uploads: pointsPerUpload once, when an upload is approved (or immediately for staff and auto-approved roles).
  • Bounties: filling one pays its reward.
  • Admin grants: an admin can add or remove points on a user.

Shop

  • Admin CRUD of items:
    • upload_credit (GB added to the uploaded counter);
    • invite (adds invites);
    • gif_avatar (unlocks animated avatars).
  • Purchases: run in one transaction. The buyer row is locked, the debit is atomic so a balance can never go negative, then the effect is applied.
  • Ledger: every point movement (except seeding accrual) goes to bonus_transactions and appears in the user's history.

Avatars

  • Formats: PNG, JPEG and WebP for everyone (512 KB). Animated GIF, WebP and APNG once unlocked (2 MB).
  • Detection: from magic bytes, never from the declared type, so SVG and other scripts are rejected. Files are served under the existing CSP sandbox for /uploads.
  • Display: on the user profile and in the shop page.

Bounties

  1. Open: the requester opens a request, and the initial reward is moved from their balance into escrow. Other users can add points.
  2. Fill: anyone proposes an approved torrent. Its uploader will receive the reward. The requester cannot fill their own bounty.
  3. Review: the requester (or staff) accepts, which pays the uploader, or rejects, which reopens the bounty.
  4. Cancel: the requester or staff can cancel an open bounty. Every contribution is refunded.

Every state change is a conditional update (WHERE status = …), so concurrent actions cannot pay twice.

UI

  • Pages: /shop (balance, earning rules, items, avatar, history), /bounties (list, filters, search, create) and /bounties/:id.
  • Admin: /admin/bonus (rules, items editor, point grants).
  • Navigation: Shop and Bounties links in the main nav, and the balance in the user menu.
  • Translations: EN and FR strings live in i18n/locales/*/shop.json.

Schema

  • New columns on users: bonus_points, avatar_url, can_use_gif_avatar.
  • New tables: bonus_transactions, shop_items, bounties, bounty_contributions.
  • Applied by: the startup drizzle-kit push. 0012_bonus_shop.sql is provided for reference.
  • The shop starts empty: the admin creates items in /admin/bonus.

Test plan

  • npx vitest run: 139 passing, with new tests for seeding accrual, purchase rules, bounty transitions and image type detection
  • npm run build
  • Not tested against a real database yet. On staging:
    • create items, then buy each type (upload credit, invite, GIF avatar) and check the balance and history;
    • try to buy without enough points, and buy the GIF unlock twice;
    • upload a PNG avatar, try a GIF before and after unlocking, and try an SVG renamed to .png;
    • seed a torrent for a while and check that points grow (and don't grow on a torrent you never downloaded);
    • bounty flow: open, contribute, fill, reject, fill, accept (the uploader is paid), plus cancel (contributors are refunded);
    • disable the bonus system in admin and check the shop and bounties are closed.

Known

  • vue-tsc reports an "Excessive stack depth" error in one admin component. It comes from Nitro's typed $fetch routes once the route count grows, and moves between files. Typecheck is not part of CI.

Closes #48

🤖 Generated with Claude Code

florianjs and others added 8 commits October 8, 2026 12:51
- per-area locale files under i18n/locales/<code>/ to avoid merge conflicts
- browser language detection, choice stored in a cookie
- language switcher in the footer

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add common, nav, layout, auth, home and shared component keys in en and
fr, translate relative times and dates in format utils, and add the
language switcher to the login and register pages.

Refs #43

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add torrents, search, upload, edit and markdown keys in en and fr for
the torrent list and detail pages, search page, torrent table and the
upload/edit modals.

Refs #43

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add forum, users and invites keys in en and fr and format forum and
invite dates with the active locale.

Refs #43

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add mod and admin keys in en and fr for the admin/mod layouts and every
admin component, and format admin dates with the active locale.
Admin-configured content and server error messages stay untranslated.

Refs #43

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- earn points per torrent seeded (only for uploads or real snatches, capped
  number of torrents) and when an upload gets approved
- admin-defined shop: upload credit, invites, animated (GIF) avatar unlock
- atomic purchases and balance changes, every movement in a ledger
- avatars: png/jpeg/webp for everyone, animated formats once unlocked,
  detected from magic bytes
- bounties: points in escrow, contributions, fill with an approved torrent,
  accept pays the uploader, cancel refunds contributors
- admin endpoints for bonus settings, shop items and point grants

Refs #48

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /shop: balance, earning rules, items, avatar upload, history
- /bounties: list, filters, search, create; /bounties/:id: contribute, fill,
  accept, reject, cancel
- admin bonus page: earning rules, shop items editor, point grants
- english and french strings in i18n/locales/*/shop.json

Refs #48

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- shop and bounties in the main nav, balance in the user menu
- avatar on user profiles, bonus entry in the admin menu
- type the pending torrents fetch explicitly

Refs #48

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 8, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 27 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2e2f2bed-432c-4185-97de-88c1630e0e19
📥 Commits

Reviewing files that changed from the base of the PR and between bcfc48a and e621c0b.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (100)
  • app/components/CategorySidebar.vue
  • app/components/EditTorrentModal.vue
  • app/components/LanguageSwitcher.vue
  • app/components/MediaIdsFields.vue
  • app/components/NotificationToast.vue
  • app/components/ReportModal.vue
  • app/components/SearchBar.vue
  • app/components/TorrentTable.vue
  • app/components/UploadTorrentModal.vue
  • app/components/WysiwygEditor.vue
  • app/components/admin/Announcements.vue
  • app/components/admin/Bonus.vue
  • app/components/admin/Branding.vue
  • app/components/admin/Categories.vue
  • app/components/admin/Charts.vue
  • app/components/admin/Endpoints.vue
  • app/components/admin/HnR.vue
  • app/components/admin/HomepageContent.vue
  • app/components/admin/Invites.vue
  • app/components/admin/Panic.vue
  • app/components/admin/PendingTorrents.vue
  • app/components/admin/Protocols.vue
  • app/components/admin/Registration.vue
  • app/components/admin/Reports.vue
  • app/components/admin/Roles.vue
  • app/components/admin/Stats.vue
  • app/components/admin/System.vue
  • app/components/admin/Tags.vue
  • app/components/admin/TorznabBlacklist.vue
  • app/components/admin/TorznabConfig.vue
  • app/components/admin/TorznabLogTable.vue
  • app/components/admin/TorznabLogs.vue
  • app/components/admin/TorznabRateLimiting.vue
  • app/components/admin/TorznabStats.vue
  • app/components/admin/TorznabUsers.vue
  • app/components/admin/Users.vue
  • app/layouts/default.vue
  • app/pages/admin.vue
  • app/pages/admin/bonus.vue
  • app/pages/admin/index.vue
  • app/pages/auth/login.vue
  • app/pages/auth/register.vue
  • app/pages/bounties/[id].vue
  • app/pages/bounties/index.vue
  • app/pages/forum/category/[id].vue
  • app/pages/forum/index.vue
  • app/pages/forum/new-topic.vue
  • app/pages/forum/topic/[id].vue
  • app/pages/index.vue
  • app/pages/invites.vue
  • app/pages/mod.vue
  • app/pages/mod/index.vue
  • app/pages/search.vue
  • app/pages/shop.vue
  • app/pages/torrents/[hash].vue
  • app/pages/torrents/index.vue
  • app/pages/users/[id].vue
  • app/utils/format.ts
  • i18n/locales/en/app.json
  • i18n/locales/en/shop.json
  • i18n/locales/fr/app.json
  • i18n/locales/fr/shop.json
  • nuxt.config.ts
  • package.json
  • server/api/admin/bonus/settings.get.ts
  • server/api/admin/bonus/settings.put.ts
  • server/api/admin/shop/items/[id].delete.ts
  • server/api/admin/shop/items/[id].put.ts
  • server/api/admin/shop/items/index.get.ts
  • server/api/admin/shop/items/index.post.ts
  • server/api/admin/users/[id]/bonus.post.ts
  • server/api/bonus/me.get.ts
  • server/api/bounties/[id].get.ts
  • server/api/bounties/[id]/accept.post.ts
  • server/api/bounties/[id]/cancel.post.ts
  • server/api/bounties/[id]/contribute.post.ts
  • server/api/bounties/[id]/fill.post.ts
  • server/api/bounties/[id]/reject.post.ts
  • server/api/bounties/index.get.ts
  • server/api/bounties/index.post.ts
  • server/api/mod/torrents/[hash]/approve.post.ts
  • server/api/shop/items.get.ts
  • server/api/shop/purchase.post.ts
  • server/api/torrents/index.post.ts
  • server/api/users/[id].get.ts
  • server/api/users/me/avatar.delete.ts
  • server/api/users/me/avatar.post.ts
  • server/db/migrations/0012_bonus_shop.sql
  • server/db/schema.ts
  • server/routes/uploads/[name].get.ts
  • server/tracker/bonusAccrual.ts
  • server/tracker/handlers.ts
  • server/utils/bonus.ts
  • server/utils/bounties.ts
  • server/utils/imageType.ts
  • server/utils/settings.ts
  • server/utils/shopSchemas.ts
  • server/utils/uploadsDir.ts
  • tests/bonus.test.ts
  • tests/image-type.test.ts
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@florianjs
florianjs merged commit 4b2558b into main Oct 8, 2026
1 check passed
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.

[FEATURE] Seed Bonus (Shop), External Torrents

1 participant