diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-02-05 22:49:03 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-02-05 22:49:03 +0900 |
| commit | 504ff72fea72eb3d7c4cf45be1bd9620cb12a796 (patch) | |
| tree | ffc4761292a60c039e8388ac4b4a020bf1c8d401 /src/server | |
| parent | 792891c4bb1cce34a4d11bd7fd5388804bff4ca6 (diff) | |
| download | kioku-504ff72fea72eb3d7c4cf45be1bd9620cb12a796.tar.gz kioku-504ff72fea72eb3d7c4cf45be1bd9620cb12a796.tar.zst kioku-504ff72fea72eb3d7c4cf45be1bd9620cb12a796.zip | |
fix(decks): align due card count with study screen limits
The deck list was showing all due cards without applying the
newCardsPerDay limit or review card limit (100), causing a mismatch
with the actual number of cards available in the study screen.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/repositories/card.test.ts | 2 | ||||
| -rw-r--r-- | src/server/repositories/card.ts | 32 | ||||
| -rw-r--r-- | src/server/repositories/types.ts | 2 | ||||
| -rw-r--r-- | src/server/routes/cards.test.ts | 2 | ||||
| -rw-r--r-- | src/server/routes/decks.test.ts | 25 | ||||
| -rw-r--r-- | src/server/routes/decks.ts | 29 | ||||
| -rw-r--r-- | src/server/routes/study.test.ts | 2 |
7 files changed, 92 insertions, 2 deletions
diff --git a/src/server/repositories/card.test.ts b/src/server/repositories/card.test.ts index 1ed31c7..21e5485 100644 --- a/src/server/repositories/card.test.ts +++ b/src/server/repositories/card.test.ts @@ -112,6 +112,8 @@ function createMockCardRepo(): CardRepository { softDeleteByNoteId: vi.fn(), findDueCards: vi.fn(), countDueCards: vi.fn(), + countDueNewCards: vi.fn(), + countDueReviewCards: vi.fn(), findDueCardsWithNoteData: vi.fn(), findDueCardsForStudy: vi.fn(), findDueNewCardsForStudy: vi.fn(), diff --git a/src/server/repositories/card.ts b/src/server/repositories/card.ts index f546922..d382f4d 100644 --- a/src/server/repositories/card.ts +++ b/src/server/repositories/card.ts @@ -221,6 +221,38 @@ export const cardRepository: CardRepository = { return result[0]?.count ?? 0; }, + async countDueNewCards(deckId: string, now: Date): Promise<number> { + const boundary = getEndOfStudyDayBoundary(now); + const result = await db + .select({ count: sql<number>`count(*)::int` }) + .from(cards) + .where( + and( + eq(cards.deckId, deckId), + isNull(cards.deletedAt), + lt(cards.due, boundary), + eq(cards.state, CardState.New), + ), + ); + return result[0]?.count ?? 0; + }, + + async countDueReviewCards(deckId: string, now: Date): Promise<number> { + const boundary = getEndOfStudyDayBoundary(now); + const result = await db + .select({ count: sql<number>`count(*)::int` }) + .from(cards) + .where( + and( + eq(cards.deckId, deckId), + isNull(cards.deletedAt), + lt(cards.due, boundary), + ne(cards.state, CardState.New), + ), + ); + return result[0]?.count ?? 0; + }, + async findDueCardsWithNoteData( deckId: string, now: Date, diff --git a/src/server/repositories/types.ts b/src/server/repositories/types.ts index 47fb68f..4042daf 100644 --- a/src/server/repositories/types.ts +++ b/src/server/repositories/types.ts @@ -168,6 +168,8 @@ export interface CardRepository { now: Date, limit: number, ): Promise<CardForStudy[]>; + countDueNewCards(deckId: string, now: Date): Promise<number>; + countDueReviewCards(deckId: string, now: Date): Promise<number>; updateFSRSFields( id: string, deckId: string, diff --git a/src/server/routes/cards.test.ts b/src/server/routes/cards.test.ts index 2179720..4595e28 100644 --- a/src/server/routes/cards.test.ts +++ b/src/server/routes/cards.test.ts @@ -26,6 +26,8 @@ function createMockCardRepo(): CardRepository { softDeleteByNoteId: vi.fn(), findDueCards: vi.fn(), countDueCards: vi.fn(), + countDueNewCards: vi.fn(), + countDueReviewCards: vi.fn(), findDueCardsWithNoteData: vi.fn(), findDueCardsForStudy: vi.fn(), findDueNewCardsForStudy: vi.fn(), diff --git a/src/server/routes/decks.test.ts b/src/server/routes/decks.test.ts index d48e494..d0854c1 100644 --- a/src/server/routes/decks.test.ts +++ b/src/server/routes/decks.test.ts @@ -6,6 +6,7 @@ import type { CardRepository, Deck, DeckRepository, + ReviewLogRepository, } from "../repositories/index.js"; import { createDecksRouter } from "./decks.js"; @@ -31,6 +32,8 @@ function createMockCardRepo(): CardRepository { softDeleteByNoteId: vi.fn(), findDueCards: vi.fn(), countDueCards: vi.fn().mockResolvedValue(0), + countDueNewCards: vi.fn().mockResolvedValue(0), + countDueReviewCards: vi.fn().mockResolvedValue(0), findDueCardsWithNoteData: vi.fn(), findDueCardsForStudy: vi.fn(), findDueNewCardsForStudy: vi.fn(), @@ -39,6 +42,13 @@ function createMockCardRepo(): CardRepository { }; } +function createMockReviewLogRepo(): ReviewLogRepository { + return { + create: vi.fn(), + countTodayNewCardReviews: vi.fn().mockResolvedValue(0), + }; +} + const JWT_SECRET = process.env.JWT_SECRET || "test-secret"; async function createTestToken(userId: string): Promise<string> { @@ -82,15 +92,18 @@ describe("GET /api/decks", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; let mockCardRepo: ReturnType<typeof createMockCardRepo>; + let mockReviewLogRepo: ReturnType<typeof createMockReviewLogRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); mockCardRepo = createMockCardRepo(); + mockReviewLogRepo = createMockReviewLogRepo(); const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo, cardRepo: mockCardRepo, + reviewLogRepo: mockReviewLogRepo, }); app = new Hono(); app.onError(errorHandler); @@ -142,15 +155,18 @@ describe("POST /api/decks", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; let mockCardRepo: ReturnType<typeof createMockCardRepo>; + let mockReviewLogRepo: ReturnType<typeof createMockReviewLogRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); mockCardRepo = createMockCardRepo(); + mockReviewLogRepo = createMockReviewLogRepo(); const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo, cardRepo: mockCardRepo, + reviewLogRepo: mockReviewLogRepo, }); app = new Hono(); app.onError(errorHandler); @@ -255,15 +271,18 @@ describe("GET /api/decks/:id", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; let mockCardRepo: ReturnType<typeof createMockCardRepo>; + let mockReviewLogRepo: ReturnType<typeof createMockReviewLogRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); mockCardRepo = createMockCardRepo(); + mockReviewLogRepo = createMockReviewLogRepo(); const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo, cardRepo: mockCardRepo, + reviewLogRepo: mockReviewLogRepo, }); app = new Hono(); app.onError(errorHandler); @@ -325,15 +344,18 @@ describe("PUT /api/decks/:id", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; let mockCardRepo: ReturnType<typeof createMockCardRepo>; + let mockReviewLogRepo: ReturnType<typeof createMockReviewLogRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); mockCardRepo = createMockCardRepo(); + mockReviewLogRepo = createMockReviewLogRepo(); const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo, cardRepo: mockCardRepo, + reviewLogRepo: mockReviewLogRepo, }); app = new Hono(); app.onError(errorHandler); @@ -455,15 +477,18 @@ describe("DELETE /api/decks/:id", () => { let app: Hono; let mockDeckRepo: ReturnType<typeof createMockDeckRepo>; let mockCardRepo: ReturnType<typeof createMockCardRepo>; + let mockReviewLogRepo: ReturnType<typeof createMockReviewLogRepo>; let authToken: string; beforeEach(async () => { vi.clearAllMocks(); mockDeckRepo = createMockDeckRepo(); mockCardRepo = createMockCardRepo(); + mockReviewLogRepo = createMockReviewLogRepo(); const decksRouter = createDecksRouter({ deckRepo: mockDeckRepo, cardRepo: mockCardRepo, + reviewLogRepo: mockReviewLogRepo, }); app = new Hono(); app.onError(errorHandler); diff --git a/src/server/routes/decks.ts b/src/server/routes/decks.ts index 2e170db..6a24c66 100644 --- a/src/server/routes/decks.ts +++ b/src/server/routes/decks.ts @@ -7,20 +7,25 @@ import { cardRepository, type DeckRepository, deckRepository, + type ReviewLogRepository, + reviewLogRepository, } from "../repositories/index.js"; import { createDeckSchema, updateDeckSchema } from "../schemas/index.js"; export interface DeckDependencies { deckRepo: DeckRepository; cardRepo: CardRepository; + reviewLogRepo: ReviewLogRepository; } const deckIdParamSchema = z.object({ id: z.uuid(), }); +const REVIEW_CARDS_LIMIT = 100; + export function createDecksRouter(deps: DeckDependencies) { - const { deckRepo, cardRepo } = deps; + const { deckRepo, cardRepo, reviewLogRepo } = deps; return new Hono() .use("*", authMiddleware) @@ -30,7 +35,26 @@ export function createDecksRouter(deps: DeckDependencies) { const now = new Date(); const decksWithDueCount = await Promise.all( decks.map(async (deck) => { - const dueCardCount = await cardRepo.countDueCards(deck.id, now); + const [dueNewCards, dueReviewCards, reviewedNewCards] = + await Promise.all([ + cardRepo.countDueNewCards(deck.id, now), + cardRepo.countDueReviewCards(deck.id, now), + reviewLogRepo.countTodayNewCardReviews(deck.id, now), + ]); + + // Apply the same limits as the study screen + const newCardBudget = Math.max( + 0, + deck.newCardsPerDay - reviewedNewCards, + ); + const newCardsToStudy = Math.min(dueNewCards, newCardBudget); + const reviewCardsToStudy = Math.min( + dueReviewCards, + REVIEW_CARDS_LIMIT, + ); + + const dueCardCount = newCardsToStudy + reviewCardsToStudy; + return { ...deck, dueCardCount }; }), ); @@ -93,4 +117,5 @@ export function createDecksRouter(deps: DeckDependencies) { export const decks = createDecksRouter({ deckRepo: deckRepository, cardRepo: cardRepository, + reviewLogRepo: reviewLogRepository, }); diff --git a/src/server/routes/study.test.ts b/src/server/routes/study.test.ts index aeba46b..a58ea0d 100644 --- a/src/server/routes/study.test.ts +++ b/src/server/routes/study.test.ts @@ -26,6 +26,8 @@ function createMockCardRepo(): CardRepository { softDeleteByNoteId: vi.fn(), findDueCards: vi.fn(), countDueCards: vi.fn(), + countDueNewCards: vi.fn(), + countDueReviewCards: vi.fn(), findDueCardsWithNoteData: vi.fn(), findDueCardsForStudy: vi.fn(), findDueNewCardsForStudy: vi.fn(), |
