From 409a3964d2c35270c5f26ad04697e10c20312de4 Mon Sep 17 00:00:00 2001 From: sudacode Date: Thu, 9 Apr 2026 00:33:23 -0700 Subject: [PATCH] feat(stats): support 365d range in trends query --- .../immersion-tracker/__tests__/query.test.ts | 59 +++++++++++++++++++ .../immersion-tracker/query-trends.ts | 3 +- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/core/services/immersion-tracker/__tests__/query.test.ts b/src/core/services/immersion-tracker/__tests__/query.test.ts index f131048e..3f9c8ef0 100644 --- a/src/core/services/immersion-tracker/__tests__/query.test.ts +++ b/src/core/services/immersion-tracker/__tests__/query.test.ts @@ -835,6 +835,65 @@ test('getTrendsDashboard keeps local-midnight session buckets separate', () => { } }); +test('getTrendsDashboard supports 365d range and caps day buckets at 365', () => { + const dbPath = makeDbPath(); + const db = new Database(dbPath); + withMockNowMs('1772395200000', () => { + try { + ensureSchema(db); + + const videoId = getOrCreateVideoRecord(db, 'local:/tmp/365d-trends.mkv', { + canonicalTitle: '365d Trends', + sourcePath: '/tmp/365d-trends.mkv', + sourceUrl: null, + sourceType: SOURCE_TYPE_LOCAL, + }); + const animeId = getOrCreateAnimeRecord(db, { + parsedTitle: '365d Trends', + canonicalTitle: '365d Trends', + anilistId: null, + titleRomaji: null, + titleEnglish: null, + titleNative: null, + metadataJson: null, + }); + linkVideoToAnimeRecord(db, videoId, { + animeId, + parsedBasename: '365d-trends.mkv', + parsedTitle: '365d Trends', + parsedSeason: 1, + parsedEpisode: 1, + parserSource: 'test', + parserConfidence: 1, + parseMetadataJson: null, + }); + + const insertDailyRollup = db.prepare( + ` + INSERT INTO imm_daily_rollups ( + rollup_day, video_id, total_sessions, total_active_min, total_lines_seen, + total_tokens_seen, total_cards, CREATED_DATE, LAST_UPDATE_DATE + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, + ); + // Seed 400 distinct rollup days so we can prove the 365d range caps at 365. + const latestRollupDay = 20513; + const createdAtMs = '1772395200000'; + for (let offset = 0; offset < 400; offset += 1) { + const rollupDay = latestRollupDay - offset; + insertDailyRollup.run(rollupDay, videoId, 1, 30, 4, 100, 2, createdAtMs, createdAtMs); + } + + const dashboard = getTrendsDashboard(db, '365d', 'day'); + + assert.equal(dashboard.activity.watchTime.length, 365); + } finally { + db.close(); + cleanupDbPath(dbPath); + } + }); +}); + test('getTrendsDashboard month grouping spans every touched calendar month and keeps progress monthly', () => { const dbPath = makeDbPath(); const db = new Database(dbPath); diff --git a/src/core/services/immersion-tracker/query-trends.ts b/src/core/services/immersion-tracker/query-trends.ts index 2a0f3eb2..5f95e90d 100644 --- a/src/core/services/immersion-tracker/query-trends.ts +++ b/src/core/services/immersion-tracker/query-trends.ts @@ -13,7 +13,7 @@ import { } from './query-shared'; import { getDailyRollups, getMonthlyRollups } from './query-sessions'; -type TrendRange = '7d' | '30d' | '90d' | 'all'; +type TrendRange = '7d' | '30d' | '90d' | '365d' | 'all'; type TrendGroupBy = 'day' | 'month'; interface TrendChartPoint { @@ -85,6 +85,7 @@ const TREND_DAY_LIMITS: Record, number> = { '7d': 7, '30d': 30, '90d': 90, + '365d': 365, }; const MONTH_NAMES = [