Skip to content

Comments

Issue #664: Sage Mode#1004

Draft
theeneon wants to merge 1 commit intomainfrom
claude/issue-664-20260128-1415
Draft

Issue #664: Sage Mode#1004
theeneon wants to merge 1 commit intomainfrom
claude/issue-664-20260128-1415

Conversation

@theeneon
Copy link
Collaborator

@theeneon theeneon commented Jan 28, 2026

This PR addresses issue #664

Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced Sage Mode system with mastery progression and activation mechanics in combat
    • Added Sage Mode rolling system with pity mechanics and free swap allowances based on player status
    • Integrated Sage Mode mastery experience rewards into quest completions
    • Created management tools for Sage Mode content administration and filtering

✏️ Tip: You can customize this high-level summary in your review settings.

Add Sage Mode - a new combat feature similar to bloodlines that requires
manual activation during battle and provides temporary power boosts.

Database:
- Add sageMode and sageModeRolls tables
- Add sageModeId and sageMasteryExperience to userData
- Add sage mode constants (roll types, costs, swaps, mastery ranks)

Combat Integration:
- Add "sageMode" to fromType union for effect tracking
- Protect sage mode effects from clear/copy/mirror in tags.ts
- Add battle state tracking fields for sage mode activation
- Add sageModes to ExtraState

API:
- Create full sageMode tRPC router with CRUD, roll, purchase, swap endpoints
- Add SageModeValidator for content editing
- Register router in root.ts

Admin UI:
- Create sage mode list page at /manual/sageMode
- Create sage mode edit page with effects and afterEffects editors
- Add SageModeFiltering component
- Update ItemWithEffects and EditContent to support sageMode type

Quest Integration:
- Add reward_sage_mastery_experience to reward fields
- Process sage mastery rewards on quest completion

Resolves #664

Co-authored-by: theeneon <theeneon@users.noreply.github.com>
@vercel
Copy link

vercel bot commented Jan 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
tnr Ready Ready Preview, Comment Jan 28, 2026 2:49pm

Request Review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

Walkthrough

Introduces a complete Sage Mode system with database schema, configuration constants, API endpoints, frontend UI, validators, and integration into combat and quest mechanics. Enables users to unlock, swap, roll, and master sage modes with cooldown mechanics and reputation costs.

Changes

Cohort / File(s) Summary
Database Constants
app/drizzle/constants.ts
Added 13 new constants for Sage Mode configuration: roll types, costs, swap mechanics, mastery ranks/EXP, disabled battles, and image asset reference.
Database Schema
app/drizzle/schema.ts
Created two new tables: SageMode (mode data with effects, costs, mastery requirements) and SageModeRolls (user roll tracking). Extended UserData with sageModeId and sageMasteryExperience fields plus index. Added relation wiring.
Frontend Pages & Navigation
app/src/app/manual/page.tsx
app/src/app/manual/sageMode/page.tsx
app/src/app/manual/sageMode/edit/[sagemodeid]/page.tsx
Added Sage Mode to manual grid; created list page with infinite pagination, filtering, and CRUD; created edit page with form management for effects and after-effects.
Frontend Hooks & Utilities
app/src/hooks/sageMode.ts
app/src/layout/SageModeFiltering.tsx
Implemented useSageModeEditForm hook for form state/validation; created SageModeFiltering component and utilities for filtering sage modes by name, rank, level, village, visibility.
Frontend Type Extensions
app/src/layout/EditContent.tsx
app/src/layout/ItemWithEffects.tsx
Extended EffectFormWrapperProps and ItemWithEffectsProps to support "sageMode" type; updated field ignore lists for sage mode editing.
Combat System Integration
app/src/libs/combat/types.ts
app/src/libs/combat/tags.ts
Added SageMode to CombatQueryUser, BattleUserState (activation tracking), ExtraState (sage mode registry); added "sageMode" to exclusion lists in copy/mirror/removal handlers.
Quest System Integration
app/src/libs/quest.ts
app/src/validators/rewards.ts
Added reward_sage_mastery_experience field to quest reward processing with shrine/daily-boost scaling.
Validators
app/src/validators/sageMode.ts
app/src/validators/combat.ts
Created sageModeFilteringSchema for UI filtering; created SageModeValidator for sage mode CRUD with fields for effects, costs, rounds, ranks, mastery requirements.
API Routing
app/src/server/api/root.ts
Wired new sageModeRouter into main appRouter.
API Router Implementation
app/src/server/api/routers/sageMode.ts
Implemented comprehensive Sage Mode router (692 lines): 11+ endpoints for listing, fetching, creating, updating, deleting, swapping (with cooldown/free allowances), rolling (natural/item), pity-rolling, removing, and purchasing sage modes. Includes database helpers, validation, Discord logging, and error handling.
Router Integration
app/src/server/api/routers/combat.ts
app/src/server/api/routers/quests.ts
Added sageModes field to combat info and extraState; added reward_sage_mastery_experience accumulation to quest rewards.

Sequence Diagram

sequenceDiagram
    participant User as Client
    participant API as API Router
    participant DB as Database
    participant Combat as Combat Engine
    participant Discord as Discord Webhook

    User->>API: Roll Sage Mode (natural/item)
    activate API
    API->>DB: Fetch user & rollable sage modes
    DB-->>API: User data + filtered modes
    API->>API: Check cooldowns, rank filters
    API->>API: Random selection or pity logic
    API->>DB: Create SageModeRolls entry
    DB-->>API: Roll recorded
    API->>DB: Update UserData.sageModeId
    DB-->>API: User updated
    API-->>User: Sage Mode granted + toast
    deactivate API

    User->>API: Swap Sage Mode
    activate API
    API->>DB: Check monthly swap count & cooldown
    DB-->>API: Swap history
    API->>API: Validate free swaps or charge cost
    API->>DB: Update UserData.sageModeId + log
    DB-->>API: Swap recorded
    API->>Discord: Log swap (non-dev)
    Discord-->>API: Logged
    API-->>User: Swap confirmed
    deactivate API

    User->>Combat: Enter Battle
    activate Combat
    Combat->>DB: Fetch user with sage mode
    DB-->>Combat: User + SageMode data
    Combat->>Combat: Initialize BattleUserState.sageModeActivated
    Note over Combat: Track activation round & expiration
    Combat->>Combat: Apply sage mode effects each round
    Combat-->>User: Battle with sage mode active
    deactivate Combat
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

The PR introduces substantial new functionality across the codebase with dense logic (particularly the 692-line sageMode.ts router), multiple interconnected database tables, frontend pages with form management, combat integration, and numerous validators. While many changes are relatively straightforward individual additions, the breadth of system impact and the complexity of the router logic (with rolling mechanics, cooldown validation, pity calculations, and database transactions) requires careful review.

Possibly related PRs

  • PR #684: Modifies app/src/libs/combat/tags.ts to adjust effect exclusion lists (the current PR adds "sageMode" to these lists, providing additional context for similar exclusion logic patterns).
  • PR #971: Modifies app/src/libs/quest.ts for reward processing (related code region where sage mastery experience rewards are now handled).
  • PR #692: Modifies app/src/libs/combat/tags.ts to expand effect type exclusions (similar pattern of managing which effect sources are affected by copy/mirror/removal handlers).

Suggested labels

Completed

Suggested reviewers

  • MathiasGruber

Poem

🐰 A sage mode hops into the fray,
With mastery ranks that grow each day,
Rolling for fortune, swapping with grace,
Sage wisdom lights up every battle space! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is minimal and incomplete. While it mentions issue #664 and notes Claude Code generation, it lacks substantive detail about the implementation, changes, motivations, potential breaking changes, or any structured information as requested by the repository template. Expand the description to address the template sections: explain what was implemented and why, describe key changes and their impact, note any breaking changes, and provide relevant context or screenshots if applicable.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title references Issue #664 and Sage Mode, which is the primary feature being implemented in this comprehensive changeset. The title is clearly related to and accurately summarizes the main objective of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch claude/issue-664-20260128-1415

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 and usage tips.

@greptile-apps
Copy link

greptile-apps bot commented Jan 28, 2026

Greptile Overview

Confidence Score: 2/5

  • Critical combat integration bug will prevent sage mode from working
  • The implementation is comprehensive and well-structured, but the missing combat data loading is a blocking issue that will prevent the feature from functioning. All the groundwork is solid, but the core combat integration has a gap.
  • app/src/server/api/routers/combat.ts requires immediate fixes to load sage mode data

Important Files Changed

Filename Overview
app/src/server/api/routers/combat.ts Missing sageMode relation loading and extraState population - sage mode will not work in combat
app/drizzle/schema.ts Added SageMode and SageModeRolls tables with proper indexes and relations
app/drizzle/constants.ts Added sage mode constants, costs, mastery ranks, and image URL
app/src/server/api/routers/sageMode.ts Comprehensive sage mode router with CRUD, rolling, swapping, and purchasing - minor free swap logic concern

Sequence Diagram

sequenceDiagram
    participant User
    participant UI
    participant SageModeRouter
    participant Database
    participant QuestSystem
    participant CombatSystem

    Note over User,Database: Sage Mode Acquisition Flow
    User->>UI: Roll for sage mode
    UI->>SageModeRouter: roll()
    SageModeRouter->>Database: Check existing rolls
    SageModeRouter->>SageModeRouter: Calculate random rank (S/A/B/C)
    alt Got sage mode
        SageModeRouter->>Database: Update userData.sageModeId
        SageModeRouter->>Database: Insert sageModeRolls record
        SageModeRouter-->>User: Success with sage mode name
    else No sage mode
        SageModeRouter->>Database: Insert empty roll (pity tracking)
        SageModeRouter-->>User: Unable to connect with natural energy
    end

    Note over User,CombatSystem: Sage Mode Usage Flow
    User->>UI: Purchase sage mode
    UI->>SageModeRouter: purchaseSageMode()
    SageModeRouter->>Database: Verify cost & village
    SageModeRouter->>Database: Deduct reputation points
    SageModeRouter->>Database: Update userData.sageModeId

    Note over User,CombatSystem: Combat Integration (MISSING)
    User->>CombatSystem: Enter battle
    CombatSystem->>Database: Load user data
    Note right of CombatSystem: ❌ Missing: sageMode relation
    Note right of CombatSystem: ❌ Missing: extraState.sageModes population
    CombatSystem-->>User: Sage mode not available in combat

    Note over User,QuestSystem: Experience & Mastery Flow
    User->>QuestSystem: Complete quest
    QuestSystem->>Database: Award sage_mastery_experience
    Database->>Database: Increment userData.sageMasteryExperience
    Note right of Database: Mastery ranks: INITIATE → ADEPT → MASTER → LEGENDARY
Loading

@github-actions
Copy link
Contributor

📦 Next.js Bundle Analysis for tnr

This analysis was generated by the Next.js Bundle Analysis action. 🤖

This PR introduced no changes to the JavaScript bundle! 🙌

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +658 to +660
case "SILVER":
return 0;
case "NORMAL":
Copy link

Choose a reason for hiding this comment

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

SILVER supporters get 0 free swaps but logic shows they receive free swaps. Either update constant to 1 or fix the logic check.

The function returns 0 for SILVER but isFreeSwap on line 171 will be true if freeSwapsUsed < freeSwaps (0 < 0 = false), so this is correct. However, the code on line 204 suggests SILVER supporters should get free swaps based on the log message.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/server/api/routers/sageMode.ts
Line: 658:660

Comment:
`SILVER` supporters get 0 free swaps but logic shows they receive free swaps. Either update constant to 1 or fix the logic check.

The function returns 0 for SILVER but `isFreeSwap` on line 171 will be true if `freeSwapsUsed < freeSwaps` (0 < 0 = false), so this is correct. However, the code on line 204 suggests SILVER supporters should get free swaps based on the log message.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +884 to +885
export const SAGE_MODE_SWAP_FREE_SILVER = 0;
export const SAGE_MODE_SWAP_FREE_GOLD = 1;
Copy link

Choose a reason for hiding this comment

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

Verify intended values match the bloodline swap constants pattern (BLOODLINE_SWAP_FREE_SILVER = 0, BLOODLINE_SWAP_FREE_GOLD = 1)

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/drizzle/constants.ts
Line: 884:885

Comment:
Verify intended values match the bloodline swap constants pattern (BLOODLINE_SWAP_FREE_SILVER = 0, BLOODLINE_SWAP_FREE_GOLD = 1)

How can I resolve this? If you propose a fix, please make it concise.

@greptile-apps
Copy link

greptile-apps bot commented Jan 28, 2026

Additional Comments (2)

app/src/server/api/routers/combat.ts
Missing sageMode relation in the combat user query. The sageMode field needs to be loaded here (like bloodline) for it to be available during combat initialization.

        bloodline: true,
        sageMode: true,
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/server/api/routers/combat.ts
Line: 1422:1422

Comment:
Missing `sageMode` relation in the combat user query. The `sageMode` field needs to be loaded here (like `bloodline`) for it to be available during combat initialization.

```suggestion
        bloodline: true,
        sageMode: true,
```

How can I resolve this? If you propose a fix, please make it concise.

app/src/server/api/routers/combat.ts
Need to add sage mode to extraState here, similar to how bloodline is added.

    }

    // Add sage mode
    if (
      user.sageMode &&
      user.sageModeId &&
      !extraState.sageModes[user.sageModeId]
    ) {
      extraState.sageModes[user.sageModeId] = user.sageMode;
    }
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/server/api/routers/combat.ts
Line: 2930:2930

Comment:
Need to add sage mode to `extraState` here, similar to how bloodline is added.

```suggestion
    }

    // Add sage mode
    if (
      user.sageMode &&
      user.sageModeId &&
      !extraState.sageModes[user.sageModeId]
    ) {
      extraState.sageModes[user.sageModeId] = user.sageMode;
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/src/libs/combat/types.ts (1)

296-303: Sage Mode data is not loaded into battle state.
The extraState.sageModes map is initialized as empty in initiateBattle() (lines 1494, 2857) but never populated. Unlike other static data (jutsus, items, bloodlines, villages), there is no database query to fetch SageMode records. If combat logic attempts to access extraState.sageModes[sageModeId], it will encounter undefined data.

Add a sageMode database query to the Promise.all() block and populate extraState.sageModes during user processing, similar to how other static lookups are handled.

🤖 Fix all issues with AI agents
In `@app/drizzle/constants.ts`:
- Around line 1756-1757: The constant IMG_MANUAL_SAGE_MODE is pointing to the
same URL as the bloodline asset, causing the Sage Mode manual tile to reuse the
bloodline icon; update IMG_MANUAL_SAGE_MODE to the correct Sage Mode image URL
(or the intended asset identifier) so it references the proper image, ensuring
you change the value assigned to the IMG_MANUAL_SAGE_MODE constant in this file.

In `@app/drizzle/schema.ts`:
- Around line 642-654: The pityRolls column uses tinyint which cannot hold
PITY_SAGE_MODE_ROLLS (200); change the column definition in schema.ts
(pityRolls) from tinyint("pityRolls").default(0).notNull() to
smallint("pityRolls").unsigned().default(0).notNull() so it can store values up
to/above 200 while preserving default and notNull constraints.
- Around line 2017-2019: The battle initialization is missing sage mode data:
update the client.query.userData.findMany() call inside initiateBattle() to
include sageMode: true in its with clause, then map the returned userData
entries to populate extraState.sageModes using each user's sageModeId and
sageMasteryExperience (ensure keys match existing lookups so combat logic can
access sageModes by user id without undefined references).

In `@app/src/app/manual/sageMode/edit/`[sagemodeid]/page.tsx:
- Line 31: The call to setNullsToEmptyStrings(data) mutates the tRPC cache
object during render; instead create a sanitized, non-mutating copy (e.g. using
structuredClone or a deep clone) and memoize it with useMemo based on the
original data, then pass that sanitizedData to the edit form (the component
rendering the form in this page, locate where data is passed into the form).
Replace the in-place call to setNullsToEmptyStrings(data) with a pure sanitizer
function that returns a cloned object, or call setNullsToEmptyStrings on the
cloned object inside useMemo, ensuring the original data from the query is never
mutated.

In `@app/src/layout/SageModeFiltering.tsx`:
- Around line 1-10: This file uses React hooks (useUserData,
useContentFiltering) but is missing the client directive; add "use client"; as
the very first line of SageModeFiltering.tsx so the component is treated as a
client component and hooks like useUserData, useContentFiltering, buildFilter,
and defineFilteringSchema can run without crashing.

In `@app/src/server/api/routers/quests.ts`:
- Around line 1519-1520: Add a cap constant SAGE_MASTERY_EXP_CAP to the
constants module (same place as MEDNIN_EXP_CAP and MAX_SKILL_POINTS in
`@/drizzle/constants.ts`) and apply it when computing sageMasteryExperience in the
quests update logic: replace the current accumulation
(user.sageMasteryExperience + (rewards.reward_sage_mastery_experience ?? 0))
with Math.min(user.sageMasteryExperience +
(rewards.reward_sage_mastery_experience ?? 0), SAGE_MASTERY_EXP_CAP) so the
pattern matches existing capped fields like medicalExperience and skillPoints.

In `@app/src/server/api/routers/sageMode.ts`:
- Around line 682-690: The public filter sageModeDatabaseFilter currently honors
input.hidden allowing clients to fetch hidden entries; modify the filter to
prevent that by adding an authentication flag (e.g., add a boolean parameter
like isAuthenticated or isAdmin to sageModeDatabaseFilter) and change the hidden
branch so that when the caller is unauthenticated (flag false or omitted) you
always apply eq(sageMode.hidden, false); only allow honoring input.hidden when
isAuthenticated/isAdmin is true (or move the admin-only filtering to a protected
endpoint that calls a separate admin-only filter). Ensure you update callers
(e.g., the public getAll route) to pass the proper flag.
- Around line 58-63: The findMany call to ctx.drizzle.query.sageMode in the
pagination path lacks an orderBy, which makes offset paging non-deterministic;
update the call (the ctx.drizzle.query.sageMode.findMany invocation that uses
baseFilters, skip, and input.limit) to include a stable orderBy (for example by
id asc or createdAt desc or name asc) so pages are returned in a consistent
order across requests.
- Around line 40-44: The publicProcedure getAllNames currently returns all
sageMode records via ctx.drizzle.query.sageMode.findMany and may leak hidden
entries; update the query to only return non-hidden records by adding a filter
for hidden: false (or alternatively enforce an authorization check/role guard
before executing getAllNames) so that only allowed users can see hidden
modes—locate getAllNames and modify the call to findMany (or wrap it with the
existing auth/role utilities) to apply the hidden=false constraint or require
proper role scope.
- Around line 526-545: The update-and-log are run in parallel causing a log to
be inserted even when the guarded update affects 0 rows; change the Promise.all
to a sequential flow: first await the result of
client.update(userData).set(...).where(...) (the update call in this diff),
check its returned affected/updated-rows value (or boolean) and only if it's >0
proceed to client.insert(actionLog).values(...); if the update affected 0 rows,
do not insert the actionLog and return or throw an appropriate error/response to
indicate the operation failed. Ensure you use the actual result field returned
by your DB client (e.g., affectedRows/rowCount/numUpdated) to decide success.
🧹 Nitpick comments (2)
app/src/libs/quest.ts (1)

290-325: Consider adding tracker task for sage mastery experience.

The experience tracker tasks section (lines 290-314) creates tasks for medical_experience_gained, crafting_experience_gained, hunting_experience_gained, and gathering_experience_gained, but there's no equivalent sage_mastery_experience_gained task.

If there are or will be quest objectives that track when a user gains sage mastery experience (similar to how quests can track "gain X medical experience"), a task entry would be needed here:

if (rewards.reward_sage_mastery_experience > 0) {
  experienceTrackerTasks.push({
    task: "sage_mastery_experience_gained" as const,
    increment: rewards.reward_sage_mastery_experience,
  });
}

If no such quest objectives are planned, this can be safely deferred.

app/src/hooks/sageMode.ts (1)

26-64: Rename loading flags for clarity.

l1/l2 obscure intent; using descriptive boolean names improves readability.

♻️ Proposed rename
-  const { data: villages, isPending: l1 } = api.village.getAllNames.useQuery(undefined);
+  const { data: villages, isPending: isVillagesLoading } =
+    api.village.getAllNames.useQuery(undefined);

-  const { mutate: updateSageMode, isPending: l2 } = api.sageMode.update.useMutation({
+  const { mutate: updateSageMode, isPending: isUpdatePending } =
+    api.sageMode.update.useMutation({
     onSuccess: (data) => {
       showMutationToast(data);
       refetch();
     },
   });

-  const loading = l1 || l2;
+  const loading = isVillagesLoading || isUpdatePending;

As per coding guidelines: Use descriptive variable names with auxiliary verbs (e.g., isLoading, hasError).

Comment on lines +1756 to +1757
export const IMG_MANUAL_SAGE_MODE =
"https://uploadthing.b-cdn.net/f/Hzww9EQvYURJaCMo8gYYfKMcJ2B5EmWt6VsNgqxpG8OSXAQk";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Verify the Sage Mode manual image asset.

IMG_MANUAL_SAGE_MODE currently matches the bloodline image URL, so the manual tile will reuse the bloodline icon. If that’s not intentional, swap in the correct asset.

🤖 Prompt for AI Agents
In `@app/drizzle/constants.ts` around lines 1756 - 1757, The constant
IMG_MANUAL_SAGE_MODE is pointing to the same URL as the bloodline asset, causing
the Sage Mode manual tile to reuse the bloodline icon; update
IMG_MANUAL_SAGE_MODE to the correct Sage Mode image URL (or the intended asset
identifier) so it references the proper image, ensuring you change the value
assigned to the IMG_MANUAL_SAGE_MODE constant in this file.

Comment on lines +642 to +654
id: varchar("id", { length: 191 }).primaryKey().notNull(),
createdAt: datetime("createdAt", { mode: "date", fsp: 3 })
.default(sql`(CURRENT_TIMESTAMP(3))`)
.notNull(),
updatedAt: datetime("updatedAt", { mode: "date", fsp: 3 })
.default(sql`(CURRENT_TIMESTAMP(3))`)
.notNull(),
userId: varchar("userId", { length: 191 }).notNull(),
sageModeId: varchar("sageModeId", { length: 191 }),
used: smallint("used").default(0).notNull(),
pityRolls: tinyint("pityRolls").default(0).notNull(),
type: mysqlEnum("type", consts.SAGE_MODE_ROLL_TYPES).default("NATURAL").notNull(),
goal: mysqlEnum("rank", consts.LetterRanks),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: studie-tech/TheNinjaRPG

Length of output: 647


🏁 Script executed:

fd -t f "constants" | grep -E "\.(ts|tsx)$"

Repository: studie-tech/TheNinjaRPG

Length of output: 124


🏁 Script executed:

rg "PITY_SAGE_MODE_ROLLS" -t ts

Repository: studie-tech/TheNinjaRPG

Length of output: 280


🏁 Script executed:

cat -n app/drizzle/schema.ts | sed -n '635,660p'

Repository: studie-tech/TheNinjaRPG

Length of output: 1205


🏁 Script executed:

rg "smallint\|tinyint" app/drizzle/schema.ts -A 1 -B 1

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

cat -n app/drizzle/constants.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 50380


🏁 Script executed:

rg "unsigned" app/drizzle/schema.ts -A 2 -B 2

Repository: studie-tech/TheNinjaRPG

Length of output: 5214


🏁 Script executed:

rg "smallint\|tinyint" app/drizzle/schema.ts | head -20

Repository: studie-tech/TheNinjaRPG

Length of output: 49


Increase pityRolls column range.

PITY_SAGE_MODE_ROLLS is 200, which exceeds a signed tinyint range (-128 to 127). This risks overflow or failed writes. Change to smallint with unsigned to safely store the value.

Proposed fix
-    pityRolls: tinyint("pityRolls").default(0).notNull(),
+    pityRolls: smallint("pityRolls", { unsigned: true }).default(0).notNull(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
id: varchar("id", { length: 191 }).primaryKey().notNull(),
createdAt: datetime("createdAt", { mode: "date", fsp: 3 })
.default(sql`(CURRENT_TIMESTAMP(3))`)
.notNull(),
updatedAt: datetime("updatedAt", { mode: "date", fsp: 3 })
.default(sql`(CURRENT_TIMESTAMP(3))`)
.notNull(),
userId: varchar("userId", { length: 191 }).notNull(),
sageModeId: varchar("sageModeId", { length: 191 }),
used: smallint("used").default(0).notNull(),
pityRolls: tinyint("pityRolls").default(0).notNull(),
type: mysqlEnum("type", consts.SAGE_MODE_ROLL_TYPES).default("NATURAL").notNull(),
goal: mysqlEnum("rank", consts.LetterRanks),
id: varchar("id", { length: 191 }).primaryKey().notNull(),
createdAt: datetime("createdAt", { mode: "date", fsp: 3 })
.default(sql`(CURRENT_TIMESTAMP(3))`)
.notNull(),
updatedAt: datetime("updatedAt", { mode: "date", fsp: 3 })
.default(sql`(CURRENT_TIMESTAMP(3))`)
.notNull(),
userId: varchar("userId", { length: 191 }).notNull(),
sageModeId: varchar("sageModeId", { length: 191 }),
used: smallint("used").default(0).notNull(),
pityRolls: smallint("pityRolls", { unsigned: true }).default(0).notNull(),
type: mysqlEnum("type", consts.SAGE_MODE_ROLL_TYPES).default("NATURAL").notNull(),
goal: mysqlEnum("rank", consts.LetterRanks),
🤖 Prompt for AI Agents
In `@app/drizzle/schema.ts` around lines 642 - 654, The pityRolls column uses
tinyint which cannot hold PITY_SAGE_MODE_ROLLS (200); change the column
definition in schema.ts (pityRolls) from
tinyint("pityRolls").default(0).notNull() to
smallint("pityRolls").unsigned().default(0).notNull() so it can store values up
to/above 200 while preserving default and notNull constraints.

Comment on lines +2017 to 2019
sageModeId: varchar("sageModeId", { length: 191 }),
sageMasteryExperience: int("sageMasteryExperience").default(0).notNull(),
status: mysqlEnum("status", consts.UserStatuses).default("AWAKE").notNull(),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find initiateBattle implementation in combat router
rg -n "initiateBattle" app/src/server/api/routers/combat.ts -A 50

Repository: studie-tech/TheNinjaRPG

Length of output: 8886


🏁 Script executed:

#!/bin/bash
# Search for sageModeId and sageMasteryExperience usage in combat files
rg -n "sageModeId|sageMasteryExperience" app/src/server/api/routers/combat.ts app/src/libs/combat/

Repository: studie-tech/TheNinjaRPG

Length of output: 164


🏁 Script executed:

#!/bin/bash
# Get the rest of initiateBattle function, specifically the usersState construction
rg -n "usersState" app/src/server/api/routers/combat.ts -B 5 -A 10 | head -100

Repository: studie-tech/TheNinjaRPG

Length of output: 4070


🏁 Script executed:

#!/bin/bash
# Find processUsersForBattle function definition
rg -n "processUsersForBattle" app/src/libs/combat/ -A 50 | head -150

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Search for processUsersForBattle across entire app
rg -n "processUsersForBattle" app/src/ | head -20

Repository: studie-tech/TheNinjaRPG

Length of output: 452


🏁 Script executed:

#!/bin/bash
# Get the processUsersForBattle function starting at line 2131
sed -n '2131,2250p' app/src/server/api/routers/combat.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 4100


🏁 Script executed:

#!/bin/bash
# Search for CombatQueryUser type definition
rg -n "type CombatQueryUser|interface CombatQueryUser" app/src/ -A 40

Repository: studie-tech/TheNinjaRPG

Length of output: 5891


🏁 Script executed:

#!/bin/bash
# Find UserData type/interface definition
rg -n "type UserData|interface UserData" app/src/ -A 100 | head -150

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check the User table definition around the specified lines
sed -n '2010,2030p' app/drizzle/schema.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 1436


🏁 Script executed:

#!/bin/bash
# Search for the query that fetches users for combat battles
rg -n "fetchedUsers|query.userData|userData.findMany" app/src/server/api/routers/combat.ts -B 5 -A 15 | head -200

Repository: studie-tech/TheNinjaRPG

Length of output: 5880


🏁 Script executed:

#!/bin/bash
# Get more of the userData.findMany query around line 1420
sed -n '1420,1530p' app/src/server/api/routers/combat.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 4023


🏁 Script executed:

#!/bin/bash
# Check schema for sageMode relation definition
rg -n "sageMode" app/drizzle/schema.ts -B 2 -A 2

Repository: studie-tech/TheNinjaRPG

Length of output: 2262


🏁 Script executed:

#!/bin/bash
# Search for sageMode usage in combat logic
rg -n "\.sageMode\.|sageMode\." app/src/server/api/routers/combat.ts app/src/libs/combat/ | head -30

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Search more broadly for sage in combat context
rg -n "sage" app/src/server/api/routers/combat.ts app/src/libs/combat/ -i

Repository: studie-tech/TheNinjaRPG

Length of output: 7903


🏁 Script executed:

#!/bin/bash
# Get context around line 2857 where sageModes is initialized
sed -n '2840,2880p' app/src/server/api/routers/combat.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 1049


🏁 Script executed:

#!/bin/bash
# Search for where sageModes entries are added to extraState
rg -n "extraState.sageModes|sageModes\[" app/src/server/api/routers/combat.ts -A 3

Repository: studie-tech/TheNinjaRPG

Length of output: 49


Load sageMode data into battle state during initiateBattle().

The sageModeId and sageMasteryExperience fields are not being included in the battle initialization. Add sageMode: true to the with clause of the client.query.userData.findMany() query (around line 1420) and populate extraState.sageModes with sage mode data so combat logic can access these values without encountering undefined references.

🤖 Prompt for AI Agents
In `@app/drizzle/schema.ts` around lines 2017 - 2019, The battle initialization is
missing sage mode data: update the client.query.userData.findMany() call inside
initiateBattle() to include sageMode: true in its with clause, then map the
returned userData entries to populate extraState.sageModes using each user's
sageModeId and sageMasteryExperience (ensure keys match existing lookups so
combat logic can access sageModes by user id without undefined references).

{ retry: false, enabled: !!sageModeId && !!userData },
);

setNullsToEmptyStrings(data);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don’t mutate query data during render.

setNullsToEmptyStrings(data) mutates the tRPC cache object in render, which can cause subtle cache/state bugs. Create a sanitized copy (memoized) and pass that to the edit form instead.

♻️ Suggested change (memoized clone)
-import { useEffect, use } from "react";
+import { useEffect, useMemo } from "react";
 ...
-  setNullsToEmptyStrings(data);
+  const normalizedData = useMemo(() => {
+    if (!data) return null;
+    const copy = {
+      ...data,
+      effects: [...data.effects],
+      afterEffects: [...data.afterEffects],
+    };
+    setNullsToEmptyStrings(copy as unknown as Record<string, unknown>);
+    return copy;
+  }, [data]);
 ...
-  if (isPending || !userData || !canChangeContent(userData.role) || !data) {
+  if (isPending || !userData || !canChangeContent(userData.role) || !normalizedData) {
     return <Loader explanation="Loading data" />;
   }
 
-  return <SingleEditSageMode sageMode={data} refetch={refetch} />;
+  return <SingleEditSageMode sageMode={normalizedData} refetch={refetch} />;
🤖 Prompt for AI Agents
In `@app/src/app/manual/sageMode/edit/`[sagemodeid]/page.tsx at line 31, The call
to setNullsToEmptyStrings(data) mutates the tRPC cache object during render;
instead create a sanitized, non-mutating copy (e.g. using structuredClone or a
deep clone) and memoize it with useMemo based on the original data, then pass
that sanitizedData to the edit form (the component rendering the form in this
page, locate where data is passed into the form). Replace the in-place call to
setNullsToEmptyStrings(data) with a pure sanitizer function that returns a
cloned object, or call setNullsToEmptyStrings on the cloned object inside
useMemo, ensuring the original data from the query is never mutated.

Comment on lines +1 to +10
import { LetterRanks } from "@/drizzle/constants";
import { useUserData } from "@/utils/UserContext";
import { canChangeContent } from "@/utils/permissions";
import {
ContentFiltering,
useContentFiltering,
buildFilter,
defineFilteringSchema,
} from "@/layout/ContentFiltering";
import type { LetterRank } from "@/drizzle/constants";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Add "use client"; — hooks in a server component will crash.

This file uses React hooks, so it must be marked as a client component.

✅ Required fix
+"use client";
+
 import { LetterRanks } from "@/drizzle/constants";
 import { useUserData } from "@/utils/UserContext";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { LetterRanks } from "@/drizzle/constants";
import { useUserData } from "@/utils/UserContext";
import { canChangeContent } from "@/utils/permissions";
import {
ContentFiltering,
useContentFiltering,
buildFilter,
defineFilteringSchema,
} from "@/layout/ContentFiltering";
import type { LetterRank } from "@/drizzle/constants";
"use client";
import { LetterRanks } from "@/drizzle/constants";
import { useUserData } from "@/utils/UserContext";
import { canChangeContent } from "@/utils/permissions";
import {
ContentFiltering,
useContentFiltering,
buildFilter,
defineFilteringSchema,
} from "@/layout/ContentFiltering";
import type { LetterRank } from "@/drizzle/constants";
🤖 Prompt for AI Agents
In `@app/src/layout/SageModeFiltering.tsx` around lines 1 - 10, This file uses
React hooks (useUserData, useContentFiltering) but is missing the client
directive; add "use client"; as the very first line of SageModeFiltering.tsx so
the component is treated as a client component and hooks like useUserData,
useContentFiltering, buildFilter, and defineFilteringSchema can run without
crashing.

Comment on lines +1519 to +1520
sageMasteryExperience:
user.sageMasteryExperience + (rewards.reward_sage_mastery_experience ?? 0),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "SAGE.*CAP|SAGE.*MAX|sageMastery.*Max" --type=ts

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

cat -n app/drizzle/constants.ts | head -100

Repository: studie-tech/TheNinjaRPG

Length of output: 3475


🏁 Script executed:

sed -n '1485,1525p' app/src/server/api/routers/quests.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 1845


🏁 Script executed:

rg -n "MEDNIN_EXP_CAP|MAX_SKILL_POINTS" --type=ts

Repository: studie-tech/TheNinjaRPG

Length of output: 1598


🏁 Script executed:

rg -n "sageMastery|sage_mastery" --type=ts -A 2 | head -80

Repository: studie-tech/TheNinjaRPG

Length of output: 2662


🏁 Script executed:

fd "schema" -e ts --type f | head -10

Repository: studie-tech/TheNinjaRPG

Length of output: 88


🏁 Script executed:

rg -n "sageMastery|sage.*rank|mastery.*rank" --type=ts -B 2 -A 2 | head -100

Repository: studie-tech/TheNinjaRPG

Length of output: 6349


🏁 Script executed:

rg -n "sageMode|sage" app/drizzle/schema.ts -B 2 -A 2

Repository: studie-tech/TheNinjaRPG

Length of output: 4772


🏁 Script executed:

rg -n "sage" app/drizzle/constants.ts -i

Repository: studie-tech/TheNinjaRPG

Length of output: 1089


🏁 Script executed:

rg -n "sageMasteryExperience" --type=ts -B 3 -A 3 | grep -E "(CAP|MAX|THRESHOLD|>|>=|<|<=)" -B 2 -A 2

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

fd "sage" -e ts --type f | xargs rg -l "sageMastery" | head -10

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

rg -n "sageMastery" app/src/server/api/routers/sageMode.ts -B 2 -A 2

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

sed -n '888,920p' app/drizzle/constants.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 1022


🏁 Script executed:

rg -n "SAGE_MASTERY" app/drizzle/constants.ts -A 10

Repository: studie-tech/TheNinjaRPG

Length of output: 628


🏁 Script executed:

rg -n "sageMasteryExperience" app/src --type=ts -B 2 -A 2 | head -80

Repository: studie-tech/TheNinjaRPG

Length of output: 664


🏁 Script executed:

rg -n "SAGE_MASTERY_REQUIRED_EXP" --type=ts -B 2 -A 2

Repository: studie-tech/TheNinjaRPG

Length of output: 385


🏁 Script executed:

rg -n "sageMasteryExperience" app/src --type=ts -C 3

Repository: studie-tech/TheNinjaRPG

Length of output: 828


🏁 Script executed:

rg -n "sageMastery" app/src --type=tsx -l | head -10

Repository: studie-tech/TheNinjaRPG

Length of output: 94


🏁 Script executed:

rg -n "sageMastery" app/src/app --type=tsx -B 2 -A 2 | head -60

Repository: studie-tech/TheNinjaRPG

Length of output: 94


🏁 Script executed:

fd "sageMastery" app/src -e tsx -e ts 2>/dev/null | head -10

Repository: studie-tech/TheNinjaRPG

Length of output: 49


🏁 Script executed:

rg -n "sageMastery" app/src -t ts -B 2 -A 2 | head -100

Repository: studie-tech/TheNinjaRPG

Length of output: 664


🏁 Script executed:

sed -n '100,130p' app/src/server/api/routers/hospital.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 1413


🏁 Script executed:

sed -n '110,130p' app/src/server/api/routers/hospital.ts

Repository: studie-tech/TheNinjaRPG

Length of output: 1102


Add a cap constant for sage mastery experience.

The code accumulates sageMasteryExperience without a cap, unlike medicalExperience (capped at MEDNIN_EXP_CAP = 4,000,000) and skillPoints (capped at MAX_SKILL_POINTS = 100). However, the sage mastery feature defines rank thresholds with a maximum at LEGENDARY: 400,000. Add SAGE_MASTERY_EXP_CAP to @/drizzle/constants.ts and apply it using Math.min() to match the established pattern, or confirm that uncapped experience is intentional.

🤖 Prompt for AI Agents
In `@app/src/server/api/routers/quests.ts` around lines 1519 - 1520, Add a cap
constant SAGE_MASTERY_EXP_CAP to the constants module (same place as
MEDNIN_EXP_CAP and MAX_SKILL_POINTS in `@/drizzle/constants.ts`) and apply it when
computing sageMasteryExperience in the quests update logic: replace the current
accumulation (user.sageMasteryExperience +
(rewards.reward_sage_mastery_experience ?? 0)) with
Math.min(user.sageMasteryExperience + (rewards.reward_sage_mastery_experience ??
0), SAGE_MASTERY_EXP_CAP) so the pattern matches existing capped fields like
medicalExperience and skillPoints.

Comment on lines +40 to +44
getAllNames: publicProcedure.query(async ({ ctx }) => {
return await ctx.drizzle.query.sageMode.findMany({
columns: { id: true, name: true, image: true },
orderBy: (table, { asc }) => [asc(table.name)],
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Public getAllNames can expose hidden content.

If hidden modes are meant to stay private, this endpoint should filter hidden = false or be protected/role-checked.

🔒 Example guard
 return await ctx.drizzle.query.sageMode.findMany({
-  columns: { id: true, name: true, image: true },
+  columns: { id: true, name: true, image: true },
+  where: eq(sageMode.hidden, false),
   orderBy: (table, { asc }) => [asc(table.name)],
 });
🤖 Prompt for AI Agents
In `@app/src/server/api/routers/sageMode.ts` around lines 40 - 44, The
publicProcedure getAllNames currently returns all sageMode records via
ctx.drizzle.query.sageMode.findMany and may leak hidden entries; update the
query to only return non-hidden records by adding a filter for hidden: false (or
alternatively enforce an authorization check/role guard before executing
getAllNames) so that only allowed users can see hidden modes—locate getAllNames
and modify the call to findMany (or wrap it with the existing auth/role
utilities) to apply the hidden=false constraint or require proper role scope.

Comment on lines +58 to +63
const results = await ctx.drizzle.query.sageMode.findMany({
with: { village: { columns: { name: true } } },
where: and(...baseFilters),
offset: skip,
limit: input.limit,
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a deterministic orderBy for pagination.

Offset pagination without orderBy can return inconsistent pages as the dataset changes. Add a stable order (e.g., name/id/createdAt) to keep paging deterministic.

♻️ Suggested change
 const results = await ctx.drizzle.query.sageMode.findMany({
   with: { village: { columns: { name: true } } },
   where: and(...baseFilters),
+  orderBy: (table, { asc }) => [asc(table.name)],
   offset: skip,
   limit: input.limit,
 });
🤖 Prompt for AI Agents
In `@app/src/server/api/routers/sageMode.ts` around lines 58 - 63, The findMany
call to ctx.drizzle.query.sageMode in the pagination path lacks an orderBy,
which makes offset paging non-deterministic; update the call (the
ctx.drizzle.query.sageMode.findMany invocation that uses baseFilters, skip, and
input.limit) to include a stable orderBy (for example by id asc or createdAt
desc or name asc) so pages are returned in a consistent order across requests.

Comment on lines +526 to +545
await Promise.all([
client
.update(userData)
.set({
sageModeId: mode?.id || null,
reputationPoints: user.reputationPoints - repCost,
})
.where(
and(eq(userData.userId, user.userId), gte(userData.reputationPoints, repCost)),
),
client.insert(actionLog).values({
id: nanoid(),
userId: user.userId,
tableName: "user",
changes: [logMsg],
relatedId: user.userId,
relatedMsg: "SageMode Changed",
relatedImage: user.avatarLight || user.avatar || IMG_AVATAR_DEFAULT,
}),
]);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Ensure update success before logging.

updateSageMode logs in Promise.all even if the guarded update updates 0 rows (e.g., concurrent rep spend). That can produce incorrect logs and “success” responses. Check the update result and only log when it succeeds.

✅ Safer sequencing
-  await Promise.all([
-    client
-      .update(userData)
-      .set({
-        sageModeId: mode?.id || null,
-        reputationPoints: user.reputationPoints - repCost,
-      })
-      .where(
-        and(eq(userData.userId, user.userId), gte(userData.reputationPoints, repCost)),
-      ),
-    client.insert(actionLog).values({
-      id: nanoid(),
-      userId: user.userId,
-      tableName: "user",
-      changes: [logMsg],
-      relatedId: user.userId,
-      relatedMsg: "SageMode Changed",
-      relatedImage: user.avatarLight || user.avatar || IMG_AVATAR_DEFAULT,
-    }),
-  ]);
+  const updateResult = await client
+    .update(userData)
+    .set({
+      sageModeId: mode?.id || null,
+      reputationPoints: user.reputationPoints - repCost,
+    })
+    .where(and(eq(userData.userId, user.userId), gte(userData.reputationPoints, repCost)));
+
+  if (!updateResult.rowsAffected) {
+    throw serverError("BAD_REQUEST", "Unable to update sage mode");
+  }
+
+  await client.insert(actionLog).values({
+    id: nanoid(),
+    userId: user.userId,
+    tableName: "user",
+    changes: [logMsg],
+    relatedId: user.userId,
+    relatedMsg: "SageMode Changed",
+    relatedImage: user.avatarLight || user.avatar || IMG_AVATAR_DEFAULT,
+  });
🤖 Prompt for AI Agents
In `@app/src/server/api/routers/sageMode.ts` around lines 526 - 545, The
update-and-log are run in parallel causing a log to be inserted even when the
guarded update affects 0 rows; change the Promise.all to a sequential flow:
first await the result of client.update(userData).set(...).where(...) (the
update call in this diff), check its returned affected/updated-rows value (or
boolean) and only if it's >0 proceed to client.insert(actionLog).values(...); if
the update affected 0 rows, do not insert the actionLog and return or throw an
appropriate error/response to indicate the operation failed. Ensure you use the
actual result field returned by your DB client (e.g.,
affectedRows/rowCount/numUpdated) to decide success.

Comment on lines +682 to +690
export const sageModeDatabaseFilter = (input?: SageModeFilteringSchema) => {
return [
...(input?.name ? [like(sageMode.name, `%${input.name}%`)] : []),
...(input?.village ? [eq(sageMode.villageId, input.village)] : []),
...(input?.rank ? [eq(sageMode.rank, input.rank)] : [isNotNull(sageMode.rank)]),
...(input?.level ? [eq(sageMode.level, input.level)] : []),
...(input?.hidden !== undefined
? [eq(sageMode.hidden, input.hidden)]
: [eq(sageMode.hidden, false)]),
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Public filtering allows fetching hidden modes by passing hidden=true.

Since getAll is public, a client can override the default and read hidden content. Consider forcing hidden=false for unauthenticated users or moving admin filtering to a protected endpoint.

🤖 Prompt for AI Agents
In `@app/src/server/api/routers/sageMode.ts` around lines 682 - 690, The public
filter sageModeDatabaseFilter currently honors input.hidden allowing clients to
fetch hidden entries; modify the filter to prevent that by adding an
authentication flag (e.g., add a boolean parameter like isAuthenticated or
isAdmin to sageModeDatabaseFilter) and change the hidden branch so that when the
caller is unauthenticated (flag false or omitted) you always apply
eq(sageMode.hidden, false); only allow honoring input.hidden when
isAuthenticated/isAdmin is true (or move the admin-only filtering to a protected
endpoint that calls a separate admin-only filter). Ensure you update callers
(e.g., the public getAll route) to pass the proper flag.

@theeneon
Copy link
Collaborator Author

@MathiasGruber @Phrosfire will be working on this one.

@MathiasGruber MathiasGruber marked this pull request as draft January 29, 2026 11:17
@MathiasGruber
Copy link
Collaborator

MathiasGruber commented Jan 29, 2026

Let's keep it as draft until done then :) then I know I don't need to look

@MathiasGruber MathiasGruber linked an issue Jan 29, 2026 that may be closed by this pull request
8 tasks
@theeneon
Copy link
Collaborator Author

theeneon commented Feb 9, 2026

Let's keep it as draft until done then :) then I know I don't need to look

Phros is out of commission this week and I know this PR has been opened for awhile you can complete it if you want to.

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.

Sage Mode

3 participants