-
Notifications
You must be signed in to change notification settings - Fork 8
add hour goals #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add hour goals #91
Conversation
📝 WalkthroughWalkthroughAdds a Buckshot Roulette game and persistent hour-goal feature, including frontend UI, new API endpoints, a GameModule/service/controller, and a large Prisma schema migration introducing shop, transactions, gift codes, and related indexes and constraints. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Browser/Frontend
participant Controller as GameController
participant Service as GameService
participant Shop as ShopService
participant Prisma as Database
Client->>Controller: POST /api/game/buckshot-roulette
activate Controller
Controller->>Service: playBuckshotRoulette(userId)
activate Service
Service->>Prisma: Query user balance & game item
Prisma-->>Service: balance & item
alt Insufficient balance
Service-->>Controller: throw BadRequest
else Sufficient balance
Service->>Service: Determine win/loss, compute wager & payout
Service->>Prisma: Create transaction record
Prisma-->>Service: Transaction saved
Service->>Shop: Request updated balance
Shop->>Prisma: Query balance
Prisma-->>Shop: New balance
Shop-->>Service: New balance
Service-->>Controller: return GameResult
end
deactivate Service
Controller-->>Client: 200 OK + GameResult
deactivate Controller
Client->>Controller: GET /api/game/buckshot-roulette/stats
activate Controller
Controller->>Service: getGameStats(userId)
activate Service
Service->>Prisma: Aggregate transactions
Prisma-->>Service: GameStats
Service-->>Controller: GameStats
deactivate Service
Controller-->>Client: 200 OK + GameStats
deactivate Controller
-->>Controller: GameStats
deactivate Service
Controller-->>Client: 200 OK + GameStats
deactivate Controller
GameStats
deactivate Controller
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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 |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 9
🤖 Fix all issues with AI agents
In @lark-ui/src/lib/ProgressBar.svelte:
- Around line 15-18: The code reads savedGoal from localStorage and sets
goalHours = parseInt(savedGoal, 10) without validating, which can produce NaN,
0, or negative values and break percentage math; update the logic in
ProgressBar.svelte to parse and validate savedGoal (use parseInt(savedGoal,
10)), then check Number.isFinite(parsed) and parsed > 0 before assigning to
goalHours, otherwise keep the existing default goalHours; additionally ensure
any percentage calculations using goalHours guard against division by zero/NaN
by using a safe denominator (e.g., fallback to default goalHours) so progress
rendering never divides by zero.
In @lark-ui/src/routes/app/roulette/+page.svelte:
- Around line 87-103: The UI text is inconsistent: the says "50/50 gamble" but the rules block's rule span
currently reads "40/60 odds"; update the rule span showing odds to "50/50 odds"
so it matches the backend WIN_PROBABILITY = 0.5 and the
text.
In @owl-api/prisma/migrations/20260113032356_migration/migration.sql:
- Around line 60-73: Add a unique constraint/index for the shop_items.name
column so names cannot be duplicated and upserts in
GameService.findOrCreateGameItem() are safe; update the migration that creates
the "shop_items" table to include a unique index or constraint on "name" (e.g.,
create unique index "shop_items_name_key" on "shop_items"("name") or alter the
table to add UNIQUE(name)), then re-run or add a follow-up migration so the
database enforces uniqueness.
- Around line 131-136: Remove the redundant non-unique indexes: delete the
CREATE INDEX statements for "slack_link_tokens_token_idx" on table
slack_link_tokens and for "gift_codes_code_idx" on table gift_codes because the
UNIQUE indexes "slack_link_tokens_token_key" and "gift_codes_code_key" already
provide indexing on those columns; ensure only the UNIQUE index statements
remain and update the migration SQL to remove the duplicate CREATE INDEX lines.
- Around line 28-30: The migration sets "role" and "onboard_complete" to NOT
NULL without backfilling existing NULLs; add a backfill step before changing the
constraints by executing UPDATE "users" SET "role" = 'user' WHERE "role" IS NULL
and UPDATE "users" SET "onboard_complete" = false WHERE "onboard_complete" IS
NULL, or alternately add ALTER COLUMN ... SET DEFAULT ('user' / false) then
ALTER COLUMN ... SET NOT NULL for the "role" and "onboard_complete" columns to
ensure no NULL values exist when the NOT NULL constraint is applied.
- Around line 41-45: The migration drops the "users_airtable" primary key
constraint without adding a replacement, which will leave the table without a
primary key; modify the migration so you either preserve the existing primary
key or explicitly set "email" as the new primary key: either remove the DROP
CONSTRAINT for "users_airtable_pkey" (keeping the existing PK) or follow the
DROP with an ADD PRIMARY KEY ("email") statement (and ensure "email" is NOT NULL
and unique beforehand); update the ALTER TABLE block for "users_airtable"
accordingly so the table always has a primary key after the migration.
In @owl-api/src/game/game.service.ts:
- Around line 59-60: Update the incorrect "50/50" text to reflect the actual
40/60 odds wherever it appears: change the inline comment above the win
calculation that reads "Determine win/loss with 50/50 odds" to indicate 40/60
(or reference GAME_CONFIG.WIN_PROBABILITY) near the line with const won =
Math.random() < GAME_CONFIG.WIN_PROBABILITY, and update the shop item
description string in findOrCreateGameItem that currently says "50/50 odds" to
state the correct "40/60 odds" (or mention the WIN_PROBABILITY value) so
comments and user-facing text match the implemented probability.
- Around line 45-78: The playBuckshotRoulette flow currently reads the user
balance then later creates a transaction, allowing a race where concurrent calls
pass the balance check; fix by performing the balance check, item lookup
(findOrCreateGameItem) and creation of the transaction atomically inside a
database transaction with row-level locking so no concurrent request can reuse
the same balance: use Prisma's transactional API (prisma.$transaction or
prisma.$executeRaw / interactiveTransaction) to lock the user's row (SELECT ...
FOR UPDATE / equivalent) before re-reading balance, compute
wager/transactionCost/payout, then create the prisma.transaction record within
the same transaction and commit; ensure all DB reads/writes in
playBuckshotRoulette (including the findOrCreateGameItem logic if it touches DB)
happen inside that transaction to prevent races.
- Around line 137-157: findOrCreateGameItem has a race condition where
concurrent calls can both not find the item and create duplicates; change the
implementation to use a database upsert (or create with unique constraint
handling) on shopItem by name (BUCKSHOT_ROULETTE_ITEM_NAME) so the operation is
atomic, and ensure the shop_items.name column has a unique constraint to prevent
duplicates.
event
duplicates.
he shop_items.name column has a unique constraint to prevent
duplicates.
text. In @owl-api/prisma/migrations/20260113032356_migration/migration.sql: - Around line 60-73: Add a unique constraint/index for the shop_items.name column so names cannot be duplicated and upserts in GameService.findOrCreateGameItem() are safe; update the migration that creates the "shop_items" table to include a unique index or constraint on "name" (e.g., create unique index "shop_items_name_key" on "shop_items"("name") or alter the table to add UNIQUE(name)), then re-run or add a follow-up migration so the database enforces uniqueness. - Around line 131-136: Remove the redundant non-unique indexes: delete the CREATE INDEX statements for "slack_link_tokens_token_idx" on table slack_link_tokens and for "gift_codes_code_idx" on table gift_codes because the UNIQUE indexes "slack_link_tokens_token_key" and "gift_codes_code_key" already provide indexing on those columns; ensure only the UNIQUE index statements remain and update the migration SQL to remove the duplicate CREATE INDEX lines. - Around line 28-30: The migration sets "role" and "onboard_complete" to NOT NULL without backfilling existing NULLs; add a backfill step before changing the constraints by executing UPDATE "users" SET "role" = 'user' WHERE "role" IS NULL and UPDATE "users" SET "onboard_complete" = false WHERE "onboard_complete" IS NULL, or alternately add ALTER COLUMN ... SET DEFAULT ('user' / false) then ALTER COLUMN ... SET NOT NULL for the "role" and "onboard_complete" columns to ensure no NULL values exist when the NOT NULL constraint is applied. - Around line 41-45: The migration drops the "users_airtable" primary key constraint without adding a replacement, which will leave the table without a primary key; modify the migration so you either preserve the existing primary key or explicitly set "email" as the new primary key: either remove the DROP CONSTRAINT for "users_airtable_pkey" (keeping the existing PK) or follow the DROP with an ADD PRIMARY KEY ("email") statement (and ensure "email" is NOT NULL and unique beforehand); update the ALTER TABLE block for "users_airtable" accordingly so the table always has a primary key after the migration. In @owl-api/src/game/game.service.ts: - Around line 59-60: Update the incorrect "50/50" text to reflect the actual 40/60 odds wherever it appears: change the inline comment above the win calculation that reads "Determine win/loss with 50/50 odds" to indicate 40/60 (or reference GAME_CONFIG.WIN_PROBABILITY) near the line with const won = Math.random() < GAME_CONFIG.WIN_PROBABILITY, and update the shop item description string in findOrCreateGameItem that currently says "50/50 odds" to state the correct "40/60 odds" (or mention the WIN_PROBABILITY value) so comments and user-facing text match the implemented probability. - Around line 45-78: The playBuckshotRoulette flow currently reads the user balance then later creates a transaction, allowing a race where concurrent calls pass the balance check; fix by performing the balance check, item lookup (findOrCreateGameItem) and creation of the transaction atomically inside a database transaction with row-level locking so no concurrent request can reuse the same balance: use Prisma's transactional API (prisma.$transaction or prisma.$executeRaw / interactiveTransaction) to lock the user's row (SELECT ... FOR UPDATE / equivalent) before re-reading balance, compute wager/transactionCost/payout, then create the prisma.transaction record within the same transaction and commit; ensure all DB reads/writes in playBuckshotRoulette (including the findOrCreateGameItem logic if it touches DB) happen inside that transaction to prevent races. - Around line 137-157: findOrCreateGameItem has a race condition where concurrent calls can both not find the item and create duplicates; change the implementation to use a database upsert (or create with unique constraint handling) on shopItem by name (BUCKSHOT_ROULETTE_ITEM_NAME) so the operation is atomic, and ensure the shop_items.name column has a unique constraint to prevent duplicates.condition where concurrent calls can both not find the item and create duplicates; change the implementation to use a database upsert (or create with unique constraint handling) on shopItem by name (BUCKSHOT_ROULETTE_ITEM_NAME) so the operation is atomic, and ensure the shop_items.name column has a unique constraint to prevent duplicates.the shop_items.name column has a unique constraint to prevent duplicates." tabindex="0" role="button">
🧹 Nitpick comments (8)
lark-ui/src/routes/app/settings/+page.svelte (3)
18-20: Minor: Missing semicolon on line 19.For consistency with the rest of the file, add a semicolon after
$state(false)on line 19.- let goalUpdating = $state(false) + let goalUpdating = $state(false);
32-35: Consider validating the parsed goal value.If localStorage contains a corrupted or out-of-range value,
parseIntcould returnNaNor a value outside the slider bounds (50-150). Consider adding validation:const savedGoal = localStorage.getItem('hourGoal'); if (savedGoal) { - hourGoal = parseInt(savedGoal, 10); + const parsed = parseInt(savedGoal, 10); + if (!isNaN(parsed) && parsed >= 50 && parsed <= 150) {p; parsed <= 150) { + hourGoal = parsed; + } }}
63-71: The "Saving..." state will never be visible.
localStorage.setItemis synchronous, sogoalUpdatingis set totrueand immediately back tofalsewithin the same event loop tick. The button will never show "Saving..." to the user.If the loading state is intentional for UX consistency, consider keeping
goalUpdatingbut removing its use in the button, or simply remove thegoalUpdatingstate entirely since it serves no practical purpose here.lark-ui/src/routes/app/roulette/+page.svelte (1)
46-46: Consider: hardcoded balance threshold.The
0.1threshold is hardcoded here and on line 118/123. The backend usesGAME_CONFIG.MIN_BALANCE_TO_PLAYwhich could differ. If the backend minimum changes, the frontend check would be out of sync, potentially allowing users to attempt plays that will fail server-side.This is low-risk but worth noting for future maintainability.
owl-api/src/game/game.controller.ts (1)
17-20: Add rate limiting to prevent rapid gambling exploitation.This endpoint allows wagering ALL unspent hours with no cooldown. A user could rapidly hit this endpoint to either exploit favorable outcomes or accidentally lose everything. Consider adding a stricter rate limit via
@Throttle()decorator or implementing a cooldown period in the service.Example rate limit decorator
+import { Throttle } from '@nestjs/throttler'; + @Post('buckshot-roulette') +@Throttle({ default: { limit: 1, ttl: 60000 } }) // 1 request per minute async playBuckshotRoulette(@Req() req: Request) {{owl-api/src/game/game.service.ts (2)
60-60: Consider using cryptographically secure randomness for gambling outcomes.
Math.random()is not cryptographically secure. While this is server-side code (reducing external exploitation risk), for a gambling feature, usingcrypto.randomInt()or similar would be more appropriate.Example using Node.js crypto
import { randomInt } from 'crypto'; // Replace Math.random() < 0.4 with:th: const won = randomInt(100)40; // 40% win probability
114-130: Consider database-level aggregation for better performance.Fetching all transactions and filtering in memory (lines 121-123) won't scale well for users with many games. Database aggregation would be more efficient.
Example using Prisma aggregate
const [stats] = await this.prisma.$queryRaw[{wins: bigint, losses: bigint, net: number}]>gt;` SELECT COUNT(*) FILTER (WHERE cost < 0) as wins,ns, COUNT(*) FILTER (WHERE cost > 0) as losses,es, -SUM(cost) as net FROM transactions WHERE user_id = ${userId} AND item_id = ${gameItem.itemId} `; owl-api/src/game/game.module.ts (1)
8-13: ImportShopModuleinstead of providingShopServicedirectly.
ShopModulealready providesShopService, so providing it again here creates unnecessary duplication. ImportShopModuleand removeShopServicefrom the providers array to reuse the existing service instance.Regarding
PrismaService: While providing it directly in each module is not ideal for singleton behavior, this pattern is used consistently throughout the codebase. Consider addressing this separately across all modules if singleton database connections are required.
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (10)
lark-ui/src/lib/ProgressBar.sveltelark-ui/src/lib/auth.tslark-ui/src/routes/app/roulette/+page.sveltelark-ui/src/routes/app/settings/+page.svelteowl-api/prisma/migrations/20260113032356_migration/migration.sqlowl-api/src/app.module.tsowl-api/src/datadog.config.tsowl-api/src/game/game.controller.tsowl-api/src/game/game.module.tsowl-api/src/game/game.service.ts
🧰 Additional context used
🧬 Code graph analysis (3)
owl-api/src/game/game.module.ts (1)
owl-api/src/app.module.ts (1)
Module(18-45)
lark-ui/src/lib/auth.ts (2)
owl-api/src/game/game.service.ts (3)
GameResult(23-29)playBuckshotRoulette(45-92)getGameStats(100-131)owl-api/src/game/game.controller.ts (2)
playBuckshotRoulette(18-20)getGameStats(29-31)
owl-api/src/game/game.service.ts (1)
lark-ui/src/lib/auth.ts (1)
GameResult(742-748)
🔇 Additional comments (15)
lark-ui/src/lib/ProgressBar.svelte (3)
25-32: LGTM!The clamping logic correctly caps hours at the goal, and the percentage calculations are straightforward. This will work correctly once
goalHoursvalidation is in place.
35-38: LGTM!Derived values are correctly computed from the reactive state.
74-83: LGTM!The conditional rendering logic correctly uses
goalHoursto determine when to show the pending and remaining hour indicators.lark-ui/src/routes/app/settings/+page.svelte (2)
129-152: LGTM!The Hour Goal UI section is well-structured with clear labels, appropriate slider range, and consistent styling with the rest of the settings page.
236-271: LGTM!Good cross-browser slider styling with both WebKit and Firefox thumb styles. The gradient-based track fill tied to
--valueis a clean approach.lark-ui/src/lib/auth.ts (3)
742-755: LGTM!The
GameResultandGameStatstypes correctly mirror the backend interfaces defined inowl-api/src/game/game.service.ts.
761-775: LGTM!The function follows the established patterns in this file with proper credentials, error handling, and return structure.
780-791: LGTM!Clean implementation following the same pattern as other authenticated GET functions in this module.
lark-ui/src/routes/app/roulette/+page.svelte (4)
24-34: LGTM!Authentication check and initial data loading are properly handled with appropriate redirect for unauthenticated users.
45-63: LGTM!The play handler properly manages state, prevents concurrent plays, and handles both success and error cases appropriately.
159-191: LGTM!The result modal is well-designed with clear win/loss differentiation, appropriate feedback messages, and good UX with overlay click-to-close functionality.
193-270: LGTM!Clean, well-organized CSS with appropriate responsive units and consistent theming. The fixed balance display and pseudo-element title effect add nice visual polish.
owl-api/src/datadog.config.ts (1)
1-6: LGTM!The dotenv configuration is correctly placed before the tracer import, ensuring environment variables are loaded before Datadog initialization. The conditional check for non-production environments is appropriate.
owl-api/src/app.module.ts (1)
16-16: LGTM!The
GameModuleis correctly imported and registered in the rootAppModule.Also applies to: 35-36
owl-api/src/game/game.controller.ts (1)
18-19: Type augmentation forreq.useris already properly configured.The Express Request interface is correctly augmented in
owl-api/src/types/express.d.tswithuser?: User. No action needed—the type safety concern is already addressed.Likely an incorrect or invalid review comment.
owl-api/prisma/migrations/20260113032356_migration/migration.sql
Outdated
Show resolved
Hide resolved
owl-api/prisma/migrations/20260113032356_migration/migration.sql
Outdated
Show resolved
Hide resolved
owl-api/prisma/migrations/20260113032356_migration/migration.sql
Outdated
Show resolved
Hide resolved
owl-api/prisma/migrations/20260113032356_migration/migration.sql
Outdated
Show resolved
Hide resolved
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>om>
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (1)
lark-ui/src/lib/ProgressBar.svelte (1)
12-21: Consider usingparseFloatand adding an upper bound check.The current validation is solid (checking for NaN and positive values), but two potential improvements:
parseInttruncates decimals (e.g., "50.5" becomes 50), which is inconsistent with the decimal hour values used elsewhere in this component.- No upper bound—a maliciously large value in localStorage could cause UI issues.
♻️ Suggested improvement
const savedGoal = localStorage.getItem('hourGoal'); if (savedGoal) { - const parsed = parseInt(savedGoal, 10); - if (!Number.isNaN(parsed) && parsed > 0) {d > 0) { + const parsed = parseFloat(savedGoal); + if (!Number.isNaN(parsed) && parsed > 0 && parsed <= 1000) {; parsed <= 1000) { goalHours = parsed; } } }
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
lark-ui/src/lib/ProgressBar.svelte
🔇 Additional comments (3)
lark-ui/src/lib/ProgressBar.svelte (3)
28-35: LGTM!The clamping logic and percentage calculations correctly use the dynamic
goalHoursvalue. The approach of capping bothapprovedHoursandtotalHoursat the goal ensures percentages stay within valid bounds.
38-41: LGTM!Good use of Svelte 5's
$derivedrune for reactive computed values. These will correctly update ifgoalHoursortotalHourschange.
77-93: LGTM!The conditional rendering logic correctly uses
goalHoursto determine visibility of the pending and remaining hour segments. The conditions properly handle edge cases where approved hours meet or exceed the goal.
|
this includes commits that were removed earlier (commits from #84 were removed) |
techpixel
left a comment
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove commits from #84
This reverts commit b8b864a.
owl-api/prisma/migrations/20260113032356_migration/migration.sql
Outdated
Show resolved
Hide resolved
techpixel
left a comment
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lgtm
|
fyi this will take time to update on the frontend |
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.