Răsfoiți Sursa

refactor: migrate from CacheKeys to tsCacheKeys across the codebase

- Updated imports and usages of CacheKeys to tsCacheKeys in various services, builders, and modules.
- Removed legacy CacheKeys definitions and related comments.
- Commented out or removed development-only modules and configurations in app modules.
- Added a script to enforce restrictions on CacheKeys usage outside designated files.
- Adjusted timestamp handling in ad-related services to use seconds instead of milliseconds.
- Cleaned up unused code and improved type safety in video payload handling.
Dave 3 luni în urmă
părinte
comite
d925f8cc2e
41 a modificat fișierele cu 258 adăugiri și 496 ștergeri
  1. 2 2
      .env.app
  2. 1 1
      .env.app.dev
  3. 1 1
      .env.app.test
  4. 1 1
      .env.docker
  5. 2 2
      .env.mgnt
  6. 1 1
      .env.mgnt.dev
  7. 1 1
      .env.mgnt.test
  8. 1 1
      .env.stats
  9. 1 0
      .gitignore
  10. 0 157
      apps/box-app-api/src/feature/ads/ad-pool-cache-compat.util.ts
  11. 14 20
      apps/box-app-api/src/feature/ads/ad.service.ts
  12. 21 11
      apps/box-app-api/src/feature/homepage/homepage.service.ts
  13. 3 3
      apps/box-app-api/src/feature/recommendation/recommendation.service.ts
  14. 8 10
      apps/box-app-api/src/feature/video/video.service.ts
  15. 2 2
      apps/box-app-api/src/main.ts
  16. 0 12
      apps/box-mgnt-api/src/app.module.ts
  17. 10 10
      apps/box-mgnt-api/src/cache-sync/cache-checklist.service.ts
  18. 16 15
      apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts
  19. 2 2
      apps/box-mgnt-api/src/config/env.validation.ts
  20. 0 21
      apps/box-mgnt-api/src/dev/services/video-stats.service.ts
  21. 13 13
      apps/box-mgnt-api/src/main.ts
  22. 1 1
      apps/box-mgnt-api/src/mgnt-backend/feature/ads/image/ads-image.service.ts
  23. 0 3
      libs/common/src/cache/cache-keys.ts
  24. 2 0
      libs/common/src/cache/ts-cache-key.provider.ts
  25. 65 0
      libs/common/src/cache/video-cache.helper.ts
  26. 0 77
      libs/common/src/cache/video-cache.utils.ts
  27. 0 82
      libs/common/src/services/ad-pool.service.ts
  28. 9 8
      libs/core/src/ad/ad-cache-warmup.service.ts
  29. 37 12
      libs/core/src/ad/ad-pool.service.ts
  30. 1 0
      libs/core/src/cache/cache-manager.module.ts
  31. 3 3
      libs/core/src/cache/category/category-cache.builder.ts
  32. 3 3
      libs/core/src/cache/category/category-cache.service.ts
  33. 3 3
      libs/core/src/cache/channel/channel-cache.builder.ts
  34. 3 3
      libs/core/src/cache/channel/channel-cache.service.ts
  35. 2 2
      libs/core/src/cache/tag/tag-cache.builder.ts
  36. 2 2
      libs/core/src/cache/tag/tag-cache.service.ts
  37. 3 4
      libs/core/src/cache/video/category/video-category-cache.builder.ts
  38. 3 3
      libs/core/src/cache/video/recommended/recommended-videos-cache.builder.ts
  39. 0 3
      libs/core/src/core.module.ts
  40. 2 1
      package.json
  41. 19 0
      scripts/check-cachekeys.sh

+ 2 - 2
.env.app

@@ -7,7 +7,7 @@ APP_ENV=test
 # MONGO_STATS_URL="mongodb://boxuser:dwR%3D%29whu2Ze@localhost:27017/box_stats?authSource=admin"
 
 # Dave local
-MYSQL_URL="mysql://root:rootpass@127.0.0.1:3306/box_admin"
+# MYSQL_URL="mysql://root:rootpass@127.0.0.1:3306/box_admin"
 MONGO_URL="mongodb://boxadmin:boxpass@127.0.0.1:27017/box_admin?replicaSet=rs0&authSource=admin"
 MONGO_STATS_URL="mongodb://boxadmin:boxpass@127.0.0.1:27017/box_stats?replicaSet=rs0&authSource=admin"
 
@@ -22,7 +22,7 @@ REDIS_HOST=127.0.0.1
 REDIS_PORT=6379
 REDIS_PASSWORD=
 REDIS_DB=0
-REDIS_KEY_PREFIX=box:
+REDIS_KEY_PREFIX=
 
 # App set to 0.0.0.0 for local LAN access
 APP_HOST=0.0.0.0

+ 1 - 1
.env.app.dev

@@ -21,7 +21,7 @@ REDIS_HOST=127.0.0.1
 REDIS_PORT=6379
 REDIS_PASSWORD=
 REDIS_DB=0
-REDIS_KEY_PREFIX=box:
+REDIS_KEY_PREFIX=
 
 # App set to 0.0.0.0 for local LAN access
 APP_HOST=0.0.0.0

+ 1 - 1
.env.app.test

@@ -22,7 +22,7 @@ REDIS_HOST=127.0.0.1
 REDIS_PORT=6379
 REDIS_PASSWORD=
 REDIS_DB=0
-REDIS_KEY_PREFIX=box:
+REDIS_KEY_PREFIX=
 
 # App set to 0.0.0.0 for local LAN access
 APP_HOST=0.0.0.0

+ 1 - 1
.env.docker

@@ -14,7 +14,7 @@ JWT_SECRET=047df8aaa3d17dc1173c5a9a3052ba66c2b0bd96937147eb643319a0c90d132f
 REDIS_HOST=box-redis
 REDIS_PORT=6379
 REDIS_PASSWORD=
-REDIS_KEY_PREFIX=box:
+REDIS_KEY_PREFIX=
 
 RABBITMQ_URL="amqp://boxrabbit:BoxRabbit2025@box-rabbitmq:5672"
 RABBITMQ_LOGIN_EXCHANGE=stats.user

+ 2 - 2
.env.mgnt

@@ -11,7 +11,7 @@ APP_ENV=test
 # MONGO_URL="mongodb://admin:ZXcv%21%21996@localhost:27017/box_admin?authSource=admin"
 # MONGO_STATS_URL="mongodb://admin:ZXcv%21%21996@localhost:27017/box_stats?authSource=admin"
 
-MYSQL_URL="mysql://root:rootpass@127.0.0.1:3306/box_admin"
+# MYSQL_URL="mysql://root:rootpass@127.0.0.1:3306/box_admin"
 MONGO_URL="mongodb://boxadmin:boxpass@127.0.0.1:27017/box_admin?authSource=admin"
 MONGO_STATS_URL="mongodb://boxadmin:boxpass@127.0.0.1:27017/box_stats?authSource=admin"
 
@@ -25,7 +25,7 @@ REDIS_HOST=127.0.0.1
 REDIS_PORT=6379
 REDIS_PASSWORD=
 REDIS_DB=0
-REDIS_KEY_PREFIX=box:
+REDIS_KEY_PREFIX=
 
 # RabbitMQ Config: RABBITMQ_URL="amqp://boxrabbit:BoxRabbit2025@localhost:5672"
 # RabbitMQ Config

+ 1 - 1
.env.mgnt.dev

@@ -11,7 +11,7 @@ REDIS_HOST=127.0.0.1
 REDIS_PORT=6379
 REDIS_PASSWORD=
 REDIS_DB=0
-REDIS_KEY_PREFIX=box:
+REDIS_KEY_PREFIX=
 
 # RabbitMQ Config: RABBITMQ_URL="amqp://boxrabbit:BoxRabbit#2025@localhost:5672"
 # RabbitMQ Config

+ 1 - 1
.env.mgnt.test

@@ -11,7 +11,7 @@ REDIS_HOST=127.0.0.1
 REDIS_PORT=6379
 REDIS_PASSWORD=
 REDIS_DB=0
-REDIS_KEY_PREFIX=box:
+REDIS_KEY_PREFIX=
 
 # RabbitMQ Config: RABBITMQ_URL="amqp://boxrabbit:BoxRabbit#2025@localhost:5672"
 # RabbitMQ Config

+ 1 - 1
.env.stats

@@ -7,7 +7,7 @@ APP_ENV=test
 # MONGO_STATS_URL="mongodb://boxuser:dwR%3D%29whu2Ze@localhost:27017/box_stats?authSource=admin"
 
 # dave local
-MYSQL_URL="mysql://root:rootpass@127.0.0.1:3306/box_admin"
+# MYSQL_URL="mysql://root:rootpass@127.0.0.1:3306/box_admin"
 MONGO_URL="mongodb://boxadmin:boxpass@127.0.0.1:27017/box_admin?replicaSet=rs0&authSource=admin"
 MONGO_STATS_URL="mongodb://boxadmin:boxpass@127.0.0.1:27017/box_stats?replicaSet=rs0&authSource=admin"
 

+ 1 - 0
.gitignore

@@ -70,3 +70,4 @@ src/tmp/*
 # scripts/stop-db.sh
 docker/mongo/mongo-keyfile
 action-plans/20251222-ACT-01.md
+.codex-instructions.md

+ 0 - 157
apps/box-app-api/src/feature/ads/ad-pool-cache-compat.util.ts

@@ -1,157 +0,0 @@
-import { Logger } from '@nestjs/common';
-import { CacheKeys } from '@box/common/cache/cache-keys';
-import type { AdPoolEntry, AdType } from '@box/common/ads/ad-types';
-import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
-import { RedisService } from '@box/db/redis/redis.service';
-
-const legacyRecoveryAttempted = new Set<string>();
-const moduleIdCache = new Map<string, string>();
-
-interface ReadAdPoolParams {
-  adType: string;
-  redis: RedisService;
-  mongoPrisma: MongoPrismaService;
-  logger: Logger;
-}
-
-/**
- * Transitional helper: new ad pools are keyed by `adType`, but legacy caches
- * were keyed by `AdsModule.id`. On a cache miss we try to read the old key once,
- * rehydrate the new key, and stop falling back to the old key afterwards.
- * Remove this helper once old keys are flushed in production.
- */
-export async function readAdPoolEntriesWithLegacySupport(
-  params: ReadAdPoolParams,
-): Promise<AdPoolEntry[] | null> {
-  const { adType, redis, mongoPrisma, logger } = params;
-  const poolKey = CacheKeys.appAdPoolByType(adType);
-
-  const existing = await redis.getJson<AdPoolEntry[]>(poolKey);
-  if (Array.isArray(existing)) {
-    return existing;
-  }
-
-  if (legacyRecoveryAttempted.has(adType)) {
-    return null;
-  }
-
-  legacyRecoveryAttempted.add(adType);
-
-  const moduleId = await resolveModuleIdForType(adType, mongoPrisma);
-  if (!moduleId) {
-    logger.warn(
-      `[AdPoolCompat] Missing AdsModule for adType=${adType}; legacy pool recovery skipped.`,
-    );
-    return null;
-  }
-
-  const legacyKey = CacheKeys.legacyAppAdPoolByModuleId(moduleId);
-  const legacyValue = await redis.get(legacyKey);
-  if (!legacyValue) {
-    return null;
-  }
-
-  let parsed: unknown;
-  try {
-    parsed = JSON.parse(legacyValue);
-  } catch (err) {
-    logger.warn(
-      `[AdPoolCompat] Failed to parse legacy pool JSON for key=${legacyKey}; please rebuild caches.`,
-    );
-    return null;
-  }
-
-  if (!Array.isArray(parsed)) {
-    return null;
-  }
-
-  const normalized = parsed
-    .map((entry) => normalizeLegacyEntry(entry))
-    .filter((entry): entry is AdPoolEntry => entry !== null)
-    .sort((a, b) => a.seq - b.seq);
-
-  if (!normalized.length) {
-    return null;
-  }
-
-  await redis.atomicSwapJson([{ key: poolKey, value: normalized }]);
-
-  logger.warn(
-    `[AdPoolCompat] Rehydrated ad pool for adType=${adType} from legacy key=${legacyKey}. ` +
-      'Remove the legacy key after Redis caches have fully warmed.',
-  );
-
-  return normalized;
-}
-
-async function resolveModuleIdForType(
-  adType: string,
-  prisma: MongoPrismaService,
-): Promise<string | null> {
-  if (moduleIdCache.has(adType)) {
-    return moduleIdCache.get(adType)!;
-  }
-
-  const adsModule = await prisma.adsModule.findUnique({
-    where: { adType: adType as AdType },
-    select: { id: true },
-  });
-
-  if (!adsModule) {
-    return null;
-  }
-
-  moduleIdCache.set(adType, adsModule.id);
-  return adsModule.id;
-}
-
-function normalizeLegacyEntry(entry: unknown): AdPoolEntry | null {
-  if (!entry || typeof entry !== 'object') {
-    return null;
-  }
-
-  const record = entry as Record<string, unknown>;
-  const id = typeof record.id === 'string' ? record.id : '';
-  const adType = typeof record.adType === 'string' ? record.adType : '';
-  const advertiser =
-    typeof record.advertiser === 'string' ? record.advertiser : '';
-  const title = typeof record.title === 'string' ? record.title : '';
-
-  if (!id || !adType || !advertiser || !title) {
-    return null;
-  }
-
-  return {
-    id,
-    adType: adType as AdType,
-    advertiser,
-    title,
-    adsContent:
-      record.adsContent === undefined
-        ? null
-        : typeof record.adsContent === 'string'
-          ? record.adsContent
-          : String(record.adsContent),
-    adsCoverImg:
-      typeof record.adsCoverImg === 'string' ? record.adsCoverImg : null,
-    adsUrl: typeof record.adsUrl === 'string' ? record.adsUrl : null,
-    imgSource:
-      typeof record.imgSource === 'string'
-        ? (record.imgSource as AdPoolEntry['imgSource'])
-        : null,
-    startDt: parseEpochBigInt(record.startDt),
-    expiryDt: parseEpochBigInt(record.expiryDt),
-    seq: Number.isFinite(record.seq as number) ? Number(record.seq) : 0,
-  };
-}
-
-function parseEpochBigInt(value: unknown): bigint {
-  if (typeof value === 'bigint') return value;
-  if (typeof value === 'number' && Number.isFinite(value)) {
-    return BigInt(Math.trunc(value));
-  }
-  if (typeof value === 'string' && value.trim().length > 0) {
-    return BigInt(value);
-  }
-  return BigInt(0);
-}

+ 14 - 20
apps/box-app-api/src/feature/ads/ad.service.ts

@@ -4,7 +4,7 @@ import { ConfigService } from '@nestjs/config';
 import { HttpService } from '@nestjs/axios';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { AdDto } from './dto/ad.dto';
 import {
   AdListResponseDto,
@@ -26,7 +26,6 @@ import {
 import { AdsClickEventPayload } from '@box/common/events/ads-click-event.dto';
 import { randomUUID } from 'crypto';
 import { nowEpochMsBigInt } from '@box/common/time/time.util';
-import { readAdPoolEntriesWithLegacySupport } from './ad-pool-cache-compat.util';
 
 // This should match what mgnt-side rebuildSingleAdCache stores.
 // We only care about a subset for now.
@@ -86,7 +85,7 @@ export class AdService {
     const { scene, slot, adType } = params;
     const maxTries = params.maxTries ?? 3;
 
-    const poolKey = CacheKeys.appAdPoolByType(adType);
+    const poolKey = tsCacheKeys.ad.poolByType(adType);
     const pool = await this.readPoolWithDiagnostics(poolKey, {
       scene,
       slot,
@@ -110,7 +109,7 @@ export class AdService {
       usedIndexes.add(idx);
 
       const entry = pool[idx];
-      const adKey = CacheKeys.appAdById(entry.id);
+      const adKey = tsCacheKeys.ad.byId(entry.id);
 
       const cachedAd =
         (await this.redis.getJson<CachedAd | null>(adKey)) ?? null;
@@ -144,12 +143,7 @@ export class AdService {
     placement: Pick<GetAdForPlacementParams, 'scene' | 'slot' | 'adType'>,
   ): Promise<AdPoolEntry[] | null> {
     try {
-      const pool = await readAdPoolEntriesWithLegacySupport({
-        adType: placement.adType,
-        redis: this.redis,
-        mongoPrisma: this.mongoPrisma,
-        logger: this.logger,
-      });
+      const pool = await this.redis.getJson<AdPoolEntry[]>(poolKey);
 
       if (!pool) {
         this.logger.warn(
@@ -252,7 +246,7 @@ export class AdService {
     const adsList: AdsByTypeDto[] = [];
 
     for (const adTypeInfo of adTypes) {
-      const poolKey = CacheKeys.appAdPoolByType(adTypeInfo.adType);
+      const poolKey = tsCacheKeys.ad.poolByType(adTypeInfo.adType);
 
       const poolEntries = await this.readPoolWithDiagnostics(poolKey, {
         scene: 'mgmt',
@@ -284,13 +278,13 @@ export class AdService {
 
         // Query MongoDB for full ad details
         try {
-          const now = BigInt(Date.now());
+          const now = BigInt(Math.floor(Date.now() / 1000));
           const ads = await this.mongoPrisma.ads.findMany({
             where: {
               id: { in: adIds },
               status: 1,
-              startDt: { lte: now },
-              OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
+              // startDt: { lte: now },
+              // OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
             },
           });
 
@@ -361,7 +355,7 @@ export class AdService {
     page: number,
     size: number,
   ): Promise<AdListResponseDto> {
-    const poolKey = CacheKeys.appAdPoolByType(adType);
+    const poolKey = tsCacheKeys.ad.poolByType(adType);
 
     const poolEntries = await this.readPoolWithDiagnostics(poolKey, {
       scene: 'mgmt',
@@ -409,13 +403,13 @@ export class AdService {
     // Step 4: Query MongoDB for full ad details
     let ads: Awaited<ReturnType<typeof this.mongoPrisma.ads.findMany>>;
     try {
-      const now = BigInt(Date.now());
+      const now = BigInt(Math.floor(Date.now() / 1000));
       ads = await this.mongoPrisma.ads.findMany({
         where: {
           id: { in: adIds },
           status: 1,
-          startDt: { lte: now },
-          OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
+          // startDt: { lte: now },
+          // OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
         },
       });
     } catch (err) {
@@ -480,7 +474,7 @@ export class AdService {
     advertiser: string;
     title: string;
   } | null> {
-    const adKey = CacheKeys.appAdById(adsId);
+    const adKey = tsCacheKeys.ad.byId(adsId);
 
     try {
       // Try Redis cache first
@@ -502,7 +496,7 @@ export class AdService {
         `Ad cache miss: adsId=${adsId}, key=${adKey}, falling back to MongoDB`,
       );
 
-      const now = BigInt(Date.now());
+      const now = BigInt(Math.floor(Date.now() / 1000));
       const ad = await this.mongoPrisma.ads.findUnique({
         where: { id: adsId },
         select: {

+ 21 - 11
apps/box-app-api/src/feature/homepage/homepage.service.ts

@@ -1,10 +1,9 @@
 // apps/box-app-api/src/feature/homepage/homepage.service.ts
 import { Injectable, Logger } from '@nestjs/common';
 import { RedisService } from '@box/db/redis/redis.service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { AdType } from '@prisma/mongo/client';
 import type { AdPoolEntry } from '@box/common/ads/ad-types';
-import { readAdPoolEntriesWithLegacySupport } from '../ads/ad-pool-cache-compat.util';
 import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
 import { VideoService } from '../video/video.service';
 import {
@@ -139,16 +138,27 @@ export class HomepageService {
     adType: AdType,
     order: AdOrder,
   ): Promise<HomeAdDto[]> {
-    const poolKey = CacheKeys.appAdPoolByType(adType);
+    const poolKey = tsCacheKeys.ad.poolByType(adType);
 
     let pool: AdPoolEntry[] | null = null;
     try {
-      pool = await readAdPoolEntriesWithLegacySupport({
-        adType,
-        redis: this.redis,
-        mongoPrisma: this.prisma,
-        logger: this.logger,
-      });
+      const entries = await this.redis.getJson<AdPoolEntry[]>(poolKey);
+
+      if (!entries) {
+        this.logger.warn(
+          `Ad pool cache miss for adType=${adType}, key=${poolKey}. Cache may be cold; ensure cache-sync rebuilt pools.`,
+        );
+        return [];
+      }
+
+      if (!entries.length) {
+        this.logger.warn(
+          `Ad pool empty for adType=${adType}, key=${poolKey}`,
+        );
+        return [];
+      }
+
+      pool = entries;
     } catch (err) {
       if (err instanceof Error && err.message?.includes('WRONGTYPE')) {
         this.logger.warn(
@@ -173,7 +183,7 @@ export class HomepageService {
       }
     }
 
-    if (!pool || pool.length === 0) {
+    if (!pool) {
       return [];
     }
 
@@ -200,7 +210,7 @@ export class HomepageService {
    * Fetch ad details from per-ad cache
    */
   private async fetchAdDetails(adId: string): Promise<HomeAdDto | null> {
-    const cacheKey = CacheKeys.appAdById(adId);
+    const cacheKey = tsCacheKeys.ad.byId(adId);
     const cached = await this.redis.getJson<CachedAd>(cacheKey);
 
     if (!cached) {

+ 3 - 3
apps/box-app-api/src/feature/recommendation/recommendation.service.ts

@@ -311,15 +311,15 @@ export class RecommendationService {
 
     try {
       // 1. Get current timestamp for date filtering
-      const now = BigInt(Date.now());
+      const now = BigInt(Math.floor(Date.now() / 1000));
 
       // 2. Fetch eligible ads from Mongo with strict filters
       const eligibleAds = await this.prisma.ads.findMany({
         where: {
           adType,
           status: 1,
-          startDt: { lte: now },
-          OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
+          // startDt: { lte: now },
+          // OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
           id: { not: currentAdId },
         },
         select: { id: true },

+ 8 - 10
apps/box-app-api/src/feature/video/video.service.ts

@@ -4,15 +4,13 @@ import { RedisService } from '@box/db/redis/redis.service';
 import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
 import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import type { VideoHomeSectionKey } from '@box/common/cache/ts-cache-key.provider';
-import { VideoCacheHelper } from '@box/common/cache/video-cache.helper';
-import { CacheKeys } from '@box/common/cache/cache-keys';
 import {
   RawVideoPayloadRow,
   toVideoPayload,
   VideoPayload,
-  videoCacheKeys,
   parseVideoPayload,
-} from '@box/common/cache/video-cache.utils';
+  VideoCacheHelper,
+} from '@box/common/cache/video-cache.helper';
 import {
   VideoCategoryDto,
   VideoTagDto,
@@ -386,7 +384,7 @@ export class VideoService {
       }
 
       const entries = videos.map((video) => ({
-        key: videoCacheKeys.videoPayloadKey(video.id),
+        key: tsCacheKeys.video.payload(video.id),
         value: toVideoPayload(video as RawVideoPayloadRow),
       }));
 
@@ -570,7 +568,7 @@ export class VideoService {
       }
 
       const entries = videos.map((video) => ({
-        key: videoCacheKeys.videoPayloadKey(video.id),
+        key: tsCacheKeys.video.payload(video.id),
         value: toVideoPayload(video as RawVideoPayloadRow),
       }));
 
@@ -720,7 +718,7 @@ export class VideoService {
     }
 
     try {
-      const keys = videoIds.map((id) => videoCacheKeys.videoPayloadKey(id));
+      const keys = videoIds.map((id) => tsCacheKeys.video.payload(id));
       const cached = await this.redis.mget(keys);
 
       const payloadMap = new Map<string, VideoPayload>();
@@ -766,7 +764,7 @@ export class VideoService {
 
         if (records.length > 0) {
           const pipelineEntries = records.map((row: RawVideoPayloadRow) => ({
-            key: videoCacheKeys.videoPayloadKey(row.id),
+            key: tsCacheKeys.video.payload(row.id),
             value: toVideoPayload(row),
           }));
 
@@ -1140,7 +1138,7 @@ export class VideoService {
       }
 
       const entries = fallbackRecords.map((video) => ({
-        key: videoCacheKeys.videoPayloadKey(video.id),
+        key: tsCacheKeys.video.payload(video.id),
         value: toVideoPayload(video),
       }));
 
@@ -1714,7 +1712,7 @@ export class VideoService {
     try {
       // Try to fetch from Redis cache first
       const cached = await this.redis.getJson<VideoItemDto[]>(
-        CacheKeys.appRecommendedVideos,
+    tsCacheKeys.video.recommended(),
       );
 
       if (cached && Array.isArray(cached) && cached.length > 0) {

+ 2 - 2
apps/box-app-api/src/main.ts

@@ -4,8 +4,8 @@ import { ConfigService } from '@nestjs/config';
 import helmet from 'helmet';
 import compression from 'compression';
 import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
-import * as path from 'path';
-import * as express from 'express';
+// import * as path from 'path';
+// import * as express from 'express';
 
 import { AppModule } from './app.module';
 

+ 0 - 12
apps/box-mgnt-api/src/app.module.ts

@@ -54,18 +54,6 @@ const isProd = process.env.NODE_ENV === 'production';
     SharedModule,
     LoggerModule.forRoot(pinoConfig),
     MgntBackendModule,
-
-    // ═══════════════════════════════════════════════════════════════════════════
-    // DEVELOPMENT-ONLY MODULES
-    // ═══════════════════════════════════════════════════════════════════════════
-    // DevVideoCacheModule provides dev-only endpoints:
-    // - DELETE /api/v1/mgnt/dev/cache/video?rebuild=true (rebuild/clear cache)
-    // - GET    /api/v1/mgnt/dev/cache/video/debug        (inspect cache)
-    // - GET    /api/v1/mgnt/dev/cache/video/coverage     (verify coverage)
-    // - GET    /api/v1/mgnt/dev/video/stats              (count videos)
-    //
-    // Only loaded when NODE_ENV !== 'production'
-    // ═══════════════════════════════════════════════════════════════════════════
     ...(isProd ? [] : [DevVideoCacheModule]),
 
     DevtoolsModule.register({

+ 10 - 10
apps/box-mgnt-api/src/cache-sync/cache-checklist.service.ts

@@ -1,11 +1,15 @@
 // apps/box-mgnt-api/src/cache-sync/cache-checklist.service.ts
 import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
 import { RedisService } from '@box/db/redis/redis.service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import type { AdType } from '@box/common/ads/ad-types';
 import { AdType as PrismaAdType } from '@prisma/mongo/client';
 import { CacheSyncService } from './cache-sync.service';
 
+const CHANNEL_ALL_KEY = tsCacheKeys.channel.all();
+const CATEGORY_ALL_KEY = tsCacheKeys.category.all();
+const TAG_ALL_KEY = tsCacheKeys.tag.all();
+
 export interface CacheKeyCheckResult {
   key: string;
   exists: boolean;
@@ -112,30 +116,26 @@ export class CacheChecklistService implements OnApplicationBootstrap {
   }
 
   private computeRequiredKeys(): string[] {
-    const keys: string[] = [
-      CacheKeys.appChannelAll,
-      CacheKeys.appCategoryAll,
-      CacheKeys.appTagAll,
-    ];
+    const keys: string[] = [CHANNEL_ALL_KEY, CATEGORY_ALL_KEY, TAG_ALL_KEY];
 
     // Add one ad pool key per AdType (no scene/slot - simplified to one pool per type)
     const adTypes = Object.values(PrismaAdType) as AdType[];
     for (const adType of adTypes) {
-      keys.push(CacheKeys.appAdPoolByType(adType));
+      keys.push(tsCacheKeys.ad.poolByType(adType));
     }
     return keys;
   }
 
   private async targetedRebuild(key: string): Promise<void> {
-    if (key === CacheKeys.appChannelAll) {
+    if (key === CHANNEL_ALL_KEY) {
       await this.cacheSync.rebuildChannelsAll();
       return;
     }
-    if (key === CacheKeys.appCategoryAll) {
+    if (key === CATEGORY_ALL_KEY) {
       await this.cacheSync.rebuildCategoriesAll();
       return;
     }
-    if (key === CacheKeys.appTagAll) {
+    if (key === TAG_ALL_KEY) {
       await this.cacheSync.rebuildTagAll();
       return;
     }

+ 16 - 15
apps/box-mgnt-api/src/cache-sync/cache-sync.service.ts

@@ -6,7 +6,7 @@ import {
   SysCacheSyncAction as CacheSyncAction,
 } from '@prisma/mongo/client';
 import { RedisService } from '@box/db/redis/redis.service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import type { AdType } from '@box/common/ads/ad-types';
 import { AdType as PrismaAdType } from '@prisma/mongo/client';
 import { CategoryCacheBuilder } from '@box/core/cache/category/category-cache.builder';
@@ -399,13 +399,13 @@ export class CacheSyncService {
         const id = (payload as any)?.id as string | number | undefined;
         try {
           if (id != null) {
-            await this.redis.del(CacheKeys.appChannelById(id));
+            await this.redis.del(tsCacheKeys.channel.byId(id));
             this.logger.log(
-              `Invalidated channel by id key=${CacheKeys.appChannelById(id)}`,
+              `Invalidated channel by id key=${tsCacheKeys.channel.byId(id)}`,
             );
           } else {
-            await this.redis.del(CacheKeys.appChannelAll);
-            this.logger.log(`Invalidated ${CacheKeys.appChannelAll}`);
+            await this.redis.del(tsCacheKeys.channel.all());
+            this.logger.log(`Invalidated ${tsCacheKeys.channel.all()}`);
           }
         } catch (err) {
           this.logger.error('Failed to invalidate channel cache', err);
@@ -447,13 +447,13 @@ export class CacheSyncService {
         const id = (payload as any)?.id as string | number | undefined;
         try {
           if (id != null) {
-            await this.redis.del(CacheKeys.appCategoryById(id));
+            await this.redis.del(tsCacheKeys.category.byId(id));
             this.logger.log(
-              `Invalidated category by id key=${CacheKeys.appCategoryById(id)}`,
+              `Invalidated category by id key=${tsCacheKeys.category.byId(id)}`,
             );
           } else {
-            await this.redis.del(CacheKeys.appCategoryAll);
-            this.logger.log(`Invalidated ${CacheKeys.appCategoryAll}`);
+            await this.redis.del(tsCacheKeys.category.all());
+            this.logger.log(`Invalidated ${tsCacheKeys.category.all()}`);
           }
         } catch (err) {
           this.logger.error('Failed to invalidate category cache', err);
@@ -488,7 +488,7 @@ export class CacheSyncService {
     switch (action.operation as CacheOperation) {
       case CacheOperation.INVALIDATE: {
         try {
-          const key = CacheKeys.appAdById(adId);
+          const key = tsCacheKeys.ad.byId(adId);
           await this.redis.del(key);
           this.logger.log(`Invalidated per-ad cache key=${key}`);
         } catch (err) {
@@ -523,7 +523,7 @@ export class CacheSyncService {
       where: { id: adId },
     });
 
-    const cacheKey = CacheKeys.appAdById(adId);
+    const cacheKey = tsCacheKeys.ad.byId(adId);
 
     if (!ad) {
       // Ad no longer exists → ensure cache is cleared
@@ -608,7 +608,7 @@ export class CacheSyncService {
       case CacheOperation.INVALIDATE: {
         try {
           // remove all pools for this adType
-          const key = CacheKeys.appAdPoolByType(adType);
+          const key = tsCacheKeys.ad.poolByType(adType);
           await this.redis.del(key);
           this.logger.log(
             `Invalidated ad pool key=${key} for adType=${adType}`,
@@ -673,9 +673,11 @@ export class CacheSyncService {
         const categoryId = action.entityId || (payload as any)?.categoryId;
         if (categoryId && categoryId !== 'null') {
           try {
-            await this.redis.del(CacheKeys.appCategoryWithTags(categoryId));
+            await this.redis.del(tsCacheKeys.category.withTags(categoryId));
             this.logger.log(
-              `Invalidated category with tags key=${CacheKeys.appCategoryWithTags(categoryId)}`,
+              `Invalidated category with tags key=${tsCacheKeys.category.withTags(
+                categoryId,
+              )}`,
             );
           } catch (err) {
             this.logger.error(
@@ -784,5 +786,4 @@ export class CacheSyncService {
       }
     }
   }
-
 }

+ 2 - 2
apps/box-mgnt-api/src/config/env.validation.ts

@@ -23,8 +23,8 @@ class EnvironmentVariables {
   @IsOptional()
   NODE_ENV: NodeEnv = NodeEnv.Development;
 
-  @IsString()
-  MYSQL_URL!: string;
+  // @IsString()
+  // MYSQL_URL!: string;
 
   @IsString()
   MONGO_URL!: string;

+ 0 - 21
apps/box-mgnt-api/src/dev/services/video-stats.service.ts

@@ -42,27 +42,6 @@ export class VideoStatsService {
 
   constructor(private readonly mongoPrisma: MongoPrismaService) {}
 
-  /**
-   * Compute video statistics for ALL enabled channels, categories, and tags.
-   * Dynamically loads database structure and returns counts of videos per category
-   * and per (category, tag) pair.
-   *
-   * Uses same filters as cache builder:
-   * - Categories: status: 1 (enabled), count videos with listStatus: 1 (only "on shelf")
-   * - Tags: status: 1 (enabled), count videos with listStatus: 1 AND tagIds contains tag
-   *
-   * ALGORITHM:
-   * 1. Load all enabled channels
-   * 2. For each channel:
-   *    - Load all enabled categories
-   *    - For each category:
-   *      - Count videos with listStatus: 1 (on shelf)
-   *      - Load all enabled tags
-   *      - For each tag:
-   *        - Count videos with listStatus: 1 AND tagIds has this tag
-   *
-   * @returns {Promise<VideoStatsResponse>} Categories and tags with video counts
-   */
   async computeStats(): Promise<VideoStatsResponse> {
     this.logger.log('[VideoStats] Starting video statistics computation');
 

+ 13 - 13
apps/box-mgnt-api/src/main.ts

@@ -36,19 +36,19 @@ async function bootstrap() {
   // Read allowed origin from env (fallback to '*')
   const corsOrg = process.env.APP_CORS_ORIGIN || '*';
 
-  await fastifyAdapter.register(fastifyStatic as any, {
-    root: path.resolve(process.env.IMAGE_ROOT_PATH || '/data/box-images'),
-    prefix: '/images/',
-    setHeaders: (res: any, pathName: any, stat: any) => {
-      // CORS
-      res.setHeader('Access-Control-Allow-Origin', corsOrg);
-      res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
-      res.setHeader(
-        'Access-Control-Allow-Headers',
-        'Origin, X-Requested-With, Content-Type, Accept, Authorization',
-      );
-    },
-  });
+  // await fastifyAdapter.register(fastifyStatic as any, {
+  //   root: path.resolve(process.env.IMAGE_ROOT_PATH || '/data/box-images'),
+  //   prefix: '/images/',
+  //   setHeaders: (res: any, pathName: any, stat: any) => {
+  //     // CORS
+  //     res.setHeader('Access-Control-Allow-Origin', corsOrg);
+  //     res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
+  //     res.setHeader(
+  //       'Access-Control-Allow-Headers',
+  //       'Origin, X-Requested-With, Content-Type, Accept, Authorization',
+  //     );
+  //   },
+  // });
 
   const app = await NestFactory.create<NestFastifyApplication>(
     AppModule,

+ 1 - 1
apps/box-mgnt-api/src/mgnt-backend/feature/ads/image/ads-image.service.ts

@@ -39,7 +39,7 @@ export class AdsImageService {
     const newKey = this.local.getRelativeKeyFromAbsolutePath(savedAbsPath);
 
     // 2. Update Ads record to LOCAL_ONLY
-    const now = BigInt(Date.now());
+    const now = BigInt(Math.floor(Date.now() / 1000));
     const updated = await this.mongo.ads.update({
       where: { id: adId },
       data: {

+ 0 - 3
libs/common/src/cache/cache-keys.ts

@@ -55,9 +55,6 @@ export const CacheKeys = {
   /** Build the canonical ad pool key for a given AdType. */
   appAdPoolByType: (adType: AdType | string): string =>
     `box:app:adpool:${adType}`,
-  /** Legacy ad pool key scoped by AdsModule ID. Remove once legacy caches expire. */
-  legacyAppAdPoolByModuleId: (moduleId: string): string =>
-    `app:adpool:${moduleId}`,
 
   // ─────────────────────────────────────────────
   // VIDEO LISTS (existing)

+ 2 - 0
libs/common/src/cache/ts-cache-key.provider.ts

@@ -211,6 +211,7 @@ export interface TsCacheKeyBuilder {
      * Order: Most recent first (limited to N items)
      */
     homeSection(channelId: string, section: VideoHomeSectionKey): string;
+    recommended(): string;
   };
 
   /**
@@ -266,6 +267,7 @@ export function createTsCacheKeyBuilder(): TsCacheKeyBuilder {
         CacheKeys.appVideoTagPoolKey(channelId, tagId, sort),
       homeSection: (channelId, section) =>
         CacheKeys.appVideoHomeSectionKey(channelId, section),
+      recommended: () => CacheKeys.appRecommendedVideos,
     },
     videoList: {
       homePage: (page) => CacheKeys.appHomeVideoPage(page),

+ 65 - 0
libs/common/src/cache/video-cache.helper.ts

@@ -19,6 +19,71 @@ export interface TagMetadata {
   categoryId: string;
 }
 
+export interface VideoPayload {
+  id: string;
+  title: string;
+  coverImg: string;
+  coverImgNew: string;
+  videoTime: number;
+  country: string;
+  firstTag: string;
+  secondTags: string[];
+  preFileName: string;
+  desc: string;
+  size: string;
+  updatedAt: string;
+  filename: string;
+  fieldNameFs: string;
+  ext: string;
+}
+
+export interface RawVideoPayloadRow {
+  id: string;
+  title: string;
+  coverImg: string;
+  coverImgNew: string;
+  videoTime: number;
+  country: string;
+  firstTag: string;
+  secondTags: string[];
+  preFileName: string;
+  desc: string;
+  size: bigint;
+  updatedAt: Date;
+  filename: string;
+  fieldNameFs: string;
+  ext: string;
+}
+
+export function toVideoPayload(row: RawVideoPayloadRow): VideoPayload {
+  return {
+    id: row.id,
+    title: row.title ?? '',
+    coverImg: row.coverImg ?? '',
+    coverImgNew: row.coverImgNew ?? '',
+    videoTime: row.videoTime ?? 0,
+    country: row.country ?? '',
+    firstTag: row.firstTag ?? '',
+    secondTags: Array.isArray(row.secondTags) ? row.secondTags : [],
+    preFileName: row.preFileName ?? '',
+    desc: row.desc ?? '',
+    size: row.size?.toString() ?? '0',
+    updatedAt: row.updatedAt?.toISOString() ?? new Date().toISOString(),
+    filename: row.filename ?? '',
+    fieldNameFs: row.fieldNameFs ?? '',
+    ext: row.ext ?? '',
+  };
+}
+
+export function parseVideoPayload(value: string | null): VideoPayload | null {
+  if (!value) return null;
+  try {
+    return JSON.parse(value) as VideoPayload;
+  } catch {
+    return null;
+  }
+}
+
 /**
  * VideoCacheHelper provides type-safe, centralized Redis operations for video cache keys.
  *

+ 0 - 77
libs/common/src/cache/video-cache.utils.ts

@@ -1,77 +0,0 @@
-// libs/common/src/cache/video-cache.utils.ts
-/**
- * Redis key helpers for video cache entries keyed by the canonical `box:app:*` namespace.
- */
-export const videoCacheKeys = {
-  videoCategoryListKey: (categoryId: string): string =>
-    `box:app:video:category:list:${categoryId}`,
-  videoTagListKey: (categoryId: string, tagId: string): string =>
-    `box:app:video:tag:list:${categoryId}:${tagId}`,
-  videoPayloadKey: (videoId: string): string =>
-    `box:app:video:payload:${videoId}`,
-};
-
-export interface VideoPayload {
-  id: string;
-  title: string;
-  coverImg: string;
-  coverImgNew: string;
-  videoTime: number;
-  country: string;
-  firstTag: string;
-  secondTags: string[];
-  preFileName: string;
-  desc: string;
-  size: string;
-  updatedAt: string;
-  filename: string;
-  fieldNameFs: string;
-  ext: string;
-}
-
-export interface RawVideoPayloadRow {
-  id: string;
-  title: string;
-  coverImg: string;
-  coverImgNew: string;
-  videoTime: number;
-  country: string;
-  firstTag: string;
-  secondTags: string[];
-  preFileName: string;
-  desc: string;
-  size: bigint;
-  updatedAt: Date;
-  filename: string;
-  fieldNameFs: string;
-  ext: string;
-}
-
-export function toVideoPayload(row: RawVideoPayloadRow): VideoPayload {
-  return {
-    id: row.id,
-    title: row.title ?? '',
-    coverImg: row.coverImg ?? '',
-    coverImgNew: row.coverImgNew ?? '',
-    videoTime: row.videoTime ?? 0,
-    country: row.country ?? '',
-    firstTag: row.firstTag ?? '',
-    secondTags: Array.isArray(row.secondTags) ? row.secondTags : [],
-    preFileName: row.preFileName ?? '',
-    desc: row.desc ?? '',
-    size: row.size?.toString() ?? '0',
-    updatedAt: row.updatedAt?.toISOString() ?? new Date().toISOString(),
-    filename: row.filename ?? '',
-    fieldNameFs: row.fieldNameFs ?? '',
-    ext: row.ext ?? '',
-  };
-}
-
-export function parseVideoPayload(value: string | null): VideoPayload | null {
-  if (!value) return null;
-  try {
-    return JSON.parse(value) as VideoPayload;
-  } catch {
-    return null;
-  }
-}

+ 0 - 82
libs/common/src/services/ad-pool.service.ts

@@ -1,82 +0,0 @@
-import { Injectable, Logger } from '@nestjs/common';
-import { CacheKeys } from '../cache/cache-keys';
-import type { AdPoolEntry, AdType } from '../ads/ad-types';
-import { AdType as PrismaAdType } from '@prisma/mongo/client';
-import { RedisService } from '@box/db/redis/redis.service';
-import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
-
-@Injectable()
-export class AdPoolService {
-  private readonly logger = new Logger(AdPoolService.name);
-
-  constructor(
-    private readonly redis: RedisService,
-    private readonly mongoPrisma: MongoPrismaService,
-  ) {}
-
-  /** Rebuild all ad pools for every AdType. */
-  async rebuildAllAdPools(): Promise<void> {
-    const adTypes = Object.values(PrismaAdType) as AdType[];
-
-    for (const adType of adTypes) {
-      try {
-        const count = await this.rebuildAdPoolByType(adType);
-        this.logger.log(
-          `AdPool warmup succeeded for adType=${adType}, ads=${count}`,
-        );
-      } catch (err) {
-        this.logger.error(
-          `AdPool warmup failed for adType=${adType}`,
-          err instanceof Error ? err.stack : String(err),
-        );
-      }
-    }
-  }
-
-  /** Rebuild a single ad pool keyed by AdType. */
-  async rebuildAdPoolByType(adType: AdType): Promise<number> {
-    const now = BigInt(Date.now());
-
-    const ads = await this.mongoPrisma.ads.findMany({
-      where: {
-        adType,
-        status: 1,
-        startDt: { lte: now },
-        OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
-      },
-      orderBy: { seq: 'asc' },
-      select: {
-        id: true,
-        adType: true,
-        advertiser: true,
-        title: true,
-        adsContent: true,
-        adsCoverImg: true,
-        adsUrl: true,
-        imgSource: true,
-        startDt: true,
-        expiryDt: true,
-        seq: true,
-      },
-    });
-
-    const poolEntries: AdPoolEntry[] = ads.map((ad) => ({
-      id: ad.id,
-      adType: ad.adType,
-      advertiser: ad.advertiser,
-      title: ad.title,
-      adsContent: ad.adsContent ?? null,
-      adsCoverImg: ad.adsCoverImg ?? null,
-      adsUrl: ad.adsUrl ?? null,
-      imgSource: ad.imgSource ?? null,
-      startDt: ad.startDt,
-      expiryDt: ad.expiryDt,
-      seq: ad.seq,
-    }));
-
-    const key = CacheKeys.appAdPoolByType(adType);
-    await this.redis.atomicSwapJson([{ key, value: poolEntries }]);
-
-    return poolEntries.length;
-  }
-}

+ 9 - 8
libs/core/src/ad/ad-cache-warmup.service.ts

@@ -1,5 +1,6 @@
+// libs/core/src/ad/ad-cache-warmup.service.ts
 import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 import type { AdType } from '@box/common/ads/ad-types';
@@ -54,15 +55,15 @@ export class AdCacheWarmupService implements OnModuleInit {
    */
   async warmupAllAdCaches(): Promise<void> {
     const startTime = Date.now();
-    const now = BigInt(Date.now());
+    const now = BigInt(Math.floor(Date.now() / 1000));
 
     try {
       // Fetch all active ads
       const ads = await this.mongoPrisma.ads.findMany({
         where: {
           status: 1, // enabled
-          startDt: { lte: now },
-          OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
+          // startDt: { lte: now },
+          // OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
         },
         orderBy: { seq: 'asc' },
         select: {
@@ -127,7 +128,7 @@ export class AdCacheWarmupService implements OnModuleInit {
    * Cache a single ad by ID
    */
   private async cacheAd(adId: string, cachedAd: CachedAd): Promise<void> {
-    const key = CacheKeys.appAdById(adId);
+    const key = tsCacheKeys.ad.byId(adId);
     await this.redis.setJson(key, cachedAd, this.AD_CACHE_TTL);
   }
 
@@ -135,7 +136,7 @@ export class AdCacheWarmupService implements OnModuleInit {
    * Warm up a single ad cache (used for on-demand refresh)
    */
   async warmupSingleAd(adId: string): Promise<void> {
-    const now = BigInt(Date.now());
+    const now = BigInt(Math.floor(Date.now() / 1000));
 
     const ad = await this.mongoPrisma.ads.findUnique({
       where: { id: adId },
@@ -157,7 +158,7 @@ export class AdCacheWarmupService implements OnModuleInit {
 
     if (!ad) {
       // Ad doesn't exist - remove from cache
-      const key = CacheKeys.appAdById(adId);
+      const key = tsCacheKeys.ad.byId(adId);
       await this.redis.del(key);
       this.logger.debug(`Ad ${adId} not found, removed from cache`);
       return;
@@ -171,7 +172,7 @@ export class AdCacheWarmupService implements OnModuleInit {
 
     if (!isActive) {
       // Ad is not active - remove from cache
-      const key = CacheKeys.appAdById(adId);
+      const key = tsCacheKeys.ad.byId(adId);
       await this.redis.del(key);
       this.logger.debug(`Ad ${adId} is not active, removed from cache`);
       return;

+ 37 - 12
libs/core/src/ad/ad-pool.service.ts

@@ -1,6 +1,7 @@
-import { Injectable, Logger } from '@nestjs/common';
+// libs/core/src/ad/ad-pool.service.ts
+import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
 import { randomUUID } from 'crypto';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import type { AdPoolEntry, AdType } from '@box/common/ads/ad-types';
 import { AdType as PrismaAdType } from '@prisma/mongo/client';
 import { RedisService } from '@box/db/redis/redis.service';
@@ -11,7 +12,18 @@ export type AdPayload = AdPoolEntry & {
 };
 
 @Injectable()
-export class AdPoolService {
+export class AdPoolService implements OnModuleInit {
+  async onModuleInit(): Promise<void> {
+    // 1) write a fingerprint key
+    await this.redis.set(
+      'debug:redis:fingerprint',
+      JSON.stringify({
+        from: AdPoolService.name,
+        ts: Date.now(),
+      }),
+    );
+  }
+
   private readonly logger = new Logger(AdPoolService.name);
 
   constructor(
@@ -36,7 +48,9 @@ export class AdPoolService {
     for (const adType of adTypes) {
       try {
         const count = await this.rebuildPoolForType(adType);
-        this.logger.log(`AdPool rebuild: adType=${adType}, ads=${count}`);
+        this.logger.log(
+          `AAAAAAAAA AdPool rebuild: adType=${adType}, ads=${count}`,
+        );
       } catch (err) {
         this.logger.error(
           `AdPool rebuild failed for adType=${adType}`,
@@ -48,14 +62,14 @@ export class AdPoolService {
 
   /** Rebuild a single ad pool for an AdType. Returns number of ads written. */
   async rebuildPoolForType(adType: AdType): Promise<number> {
-    const now = BigInt(Date.now());
+    const now = BigInt(Math.floor(Date.now() / 1000));
 
     const ads = await this.mongoPrisma.ads.findMany({
       where: {
         adType,
         status: 1,
-        startDt: { lte: now },
-        OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
+        // startDt: { lte: now },
+        // OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
       },
       orderBy: { seq: 'asc' },
       select: {
@@ -73,6 +87,10 @@ export class AdPoolService {
       },
     });
 
+    this.logger.log(
+      `AAAAAA Rebuilding ad pool for adType=${adType}, count=${ads.length}`,
+    );
+
     const payloads: AdPayload[] = ads.map((ad) => ({
       id: ad.id,
       adType: ad.adType as AdType,
@@ -87,7 +105,10 @@ export class AdPoolService {
       seq: ad.seq,
     }));
 
-    const key = CacheKeys.appAdPoolByType(adType);
+    const key = tsCacheKeys.ad.poolByType(adType);
+    this.logger.log(
+      `Writing ${payloads.length} ads to Redis key=${key} payloads=${JSON.stringify(payloads).slice(0, 100)}`,
+    );
 
     await this.redis.atomicSwapJson([{ key, value: payloads }]);
 
@@ -97,7 +118,7 @@ export class AdPoolService {
   /** Fetch one random ad payload from Redis SET. */
   async getRandomFromRedisPool(adType: AdType): Promise<AdPayload | null> {
     try {
-      const key = CacheKeys.appAdPoolByType(adType);
+      const key = tsCacheKeys.ad.poolByType(adType);
       const pool = await this.redis.getJson<AdPoolEntry[]>(key);
 
       if (!pool || pool.length === 0) return null;
@@ -123,13 +144,13 @@ export class AdPoolService {
   /** Fallback: pick a random ad directly from MongoDB. */
   async getRandomFromDb(adType: AdType): Promise<AdPayload | null> {
     try {
-      const now = BigInt(Date.now());
+      const now = BigInt(Math.floor(Date.now() / 1000));
       const ads = await this.mongoPrisma.ads.findMany({
         where: {
           adType,
           status: 1,
-          startDt: { lte: now },
-          OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
+          // startDt: { lte: now },
+          // OR: [{ expiryDt: BigInt(0) }, { expiryDt: { gte: now } }],
         },
         select: {
           id: true,
@@ -146,6 +167,10 @@ export class AdPoolService {
         },
       });
 
+      this.logger.log(
+        `Fallback MongoDB fetch for adType=${adType}, count=${ads.length}`,
+      );
+
       if (!ads.length) return null;
 
       const pickIndex = Math.floor(Math.random() * ads.length);

+ 1 - 0
libs/core/src/cache/cache-manager.module.ts

@@ -1,3 +1,4 @@
+// libs/core/src/cache/cache-manager.module.ts
 import { Module } from '@nestjs/common';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 import { AdPoolService } from '../ad/ad-pool.service';

+ 3 - 3
libs/core/src/cache/category/category-cache.builder.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { BaseCacheBuilder } from '@box/common/cache/cache-builder';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 
@@ -52,12 +52,12 @@ export class CategoryCacheBuilder extends BaseCacheBuilder {
 
     const entries: Array<{ key: string; value: unknown }> = payloads.map(
       (payload) => ({
-        key: CacheKeys.appCategory(payload.id),
+        key: tsCacheKeys.category.general(payload.id),
         value: payload,
       }),
     );
 
-    entries.push({ key: CacheKeys.appCategoryAll, value: payloads });
+    entries.push({ key: tsCacheKeys.category.all(), value: payloads });
 
     await this.redis.pipelineSetJson(entries);
 

+ 3 - 3
libs/core/src/cache/category/category-cache.service.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { BaseCacheService } from '@box/common/cache/cache-service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { CategoryCachePayload } from './category-cache.builder';
 import { RedisService } from '@box/db/redis/redis.service';
 
@@ -12,7 +12,7 @@ export class CategoryCacheService extends BaseCacheService {
 
   async getAllCategories(): Promise<CategoryCachePayload[]> {
     return (
-      (await this.getJson<CategoryCachePayload[]>(CacheKeys.appCategoryAll)) ??
+      (await this.getJson<CategoryCachePayload[]>(tsCacheKeys.category.all())) ??
       []
     );
   }
@@ -20,7 +20,7 @@ export class CategoryCacheService extends BaseCacheService {
   async getCategoryById(id: string): Promise<CategoryCachePayload | null> {
     if (!id) return null;
     return (
-      (await this.getJson<CategoryCachePayload>(CacheKeys.appCategory(id))) ??
+      (await this.getJson<CategoryCachePayload>(tsCacheKeys.category.general(id))) ??
       null
     );
   }

+ 3 - 3
libs/core/src/cache/channel/channel-cache.builder.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { BaseCacheBuilder } from '@box/common/cache/cache-builder';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 
@@ -39,12 +39,12 @@ export class ChannelCacheBuilder extends BaseCacheBuilder {
 
     const entries: Array<{ key: string; value: unknown }> = payloads.map(
       (payload) => ({
-        key: CacheKeys.appChannelById(payload.id),
+        key: tsCacheKeys.channel.byId(payload.id),
         value: payload,
       }),
     );
 
-    entries.push({ key: CacheKeys.appChannelAll, value: payloads });
+    entries.push({ key: tsCacheKeys.channel.all(), value: payloads });
     await this.redis.pipelineSetJson(entries);
 
     this.logger.log(`Built ${payloads.length} channels`);

+ 3 - 3
libs/core/src/cache/channel/channel-cache.service.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { BaseCacheService } from '@box/common/cache/cache-service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { ChannelCachePayload } from './channel-cache.builder';
 import { RedisService } from '@box/db/redis/redis.service';
 
@@ -12,14 +12,14 @@ export class ChannelCacheService extends BaseCacheService {
 
   async getAllChannels(): Promise<ChannelCachePayload[]> {
     return (
-      (await this.getJson<ChannelCachePayload[]>(CacheKeys.appChannelAll)) ?? []
+      (await this.getJson<ChannelCachePayload[]>(tsCacheKeys.channel.all())) ?? []
     );
   }
 
   async getChannelById(id: string): Promise<ChannelCachePayload | null> {
     if (!id) return null;
     return (
-      (await this.getJson<ChannelCachePayload>(CacheKeys.appChannelById(id))) ??
+      (await this.getJson<ChannelCachePayload>(tsCacheKeys.channel.byId(id))) ??
       null
     );
   }

+ 2 - 2
libs/core/src/cache/tag/tag-cache.builder.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { BaseCacheBuilder } from '@box/common/cache/cache-builder';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
 
@@ -30,7 +30,7 @@ export class TagCacheBuilder extends BaseCacheBuilder {
       seq: tag.seq,
     }));
 
-    await this.redis.setJson(CacheKeys.appTagAll, payload);
+    await this.redis.setJson(tsCacheKeys.tag.all(), payload);
     this.logger.log(`Built ${payload.length} tags`);
   }
 }

+ 2 - 2
libs/core/src/cache/tag/tag-cache.service.ts

@@ -1,6 +1,6 @@
 import { Injectable } from '@nestjs/common';
 import { BaseCacheService } from '@box/common/cache/cache-service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import { TagCachePayload } from './tag-cache.builder';
 import { RedisService } from '@box/db/redis/redis.service';
 
@@ -11,6 +11,6 @@ export class TagCacheService extends BaseCacheService {
   }
 
   async getAllTags(): Promise<TagCachePayload[]> {
-    return (await this.getJson<TagCachePayload[]>(CacheKeys.appTagAll)) ?? [];
+    return (await this.getJson<TagCachePayload[]>(tsCacheKeys.tag.all())) ?? [];
   }
 }

+ 3 - 4
libs/core/src/cache/video/category/video-category-cache.builder.ts

@@ -3,12 +3,11 @@ import { Injectable, Logger } from '@nestjs/common';
 import { BaseCacheBuilder } from '@box/common/cache/cache-builder';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
 import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 import {
   RawVideoPayloadRow,
   toVideoPayload,
-} from '@box/common/cache/video-cache.utils';
+} from '@box/common/cache/video-cache.helper';
 import {
   VideoCacheHelper,
   type TagMetadata,
@@ -245,7 +244,7 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
   async rebuildVideoPayload(videoId: string): Promise<void> {
     const video = await this.fetchVideoById(videoId);
     if (!video) {
-      await this.redis.del(CacheKeys.appVideoPayloadKey(videoId));
+      await this.redis.del(tsCacheKeys.video.payload(videoId));
       return;
     }
 
@@ -347,7 +346,7 @@ export class VideoCategoryCacheBuilder extends BaseCacheBuilder {
     if (!videos.length) return;
 
     const entries = videos.map((video) => ({
-      key: CacheKeys.appVideoPayloadKey(video.id),
+      key: tsCacheKeys.video.payload(video.id),
       value: toVideoPayload(video),
     }));
 

+ 3 - 3
libs/core/src/cache/video/recommended/recommended-videos-cache.builder.ts

@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
 import { BaseCacheBuilder } from '@box/common/cache/cache-builder';
 import { RedisService } from '@box/db/redis/redis.service';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
-import { CacheKeys } from '@box/common/cache/cache-keys';
+import { tsCacheKeys } from '@box/common/cache/ts-cache-key.provider';
 
 /**
  * Recommended video item structure matching homepage VideoItemDto.
@@ -69,7 +69,7 @@ export class RecommendedVideosCacheBuilder extends BaseCacheBuilder {
 
       // Store in Redis as JSON
       await this.redis.setJson(
-        CacheKeys.appRecommendedVideos,
+        tsCacheKeys.video.recommended(),
         items,
         this.CACHE_TTL,
       );
@@ -121,6 +121,6 @@ export class RecommendedVideosCacheBuilder extends BaseCacheBuilder {
    * Get the cache key for recommended videos.
    */
   getCacheKey(): string {
-    return CacheKeys.appRecommendedVideos;
+    return tsCacheKeys.video.recommended();
   }
 }

+ 0 - 3
libs/core/src/core.module.ts

@@ -1,7 +1,6 @@
 // lib/core/src/core.module.ts
 import { Module } from '@nestjs/common';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
-import { AdPoolService } from './ad/ad-pool.service';
 import { AdPoolBuilder, AdPoolWarmupService } from './cache/adpool';
 import { CategoryCacheService } from './cache/category/category-cache.service';
 import { CacheManagerModule } from './cache/cache-manager.module';
@@ -16,7 +15,6 @@ import {
   imports: [CacheManagerModule],
   providers: [
     MongoPrismaService,
-    AdPoolService,
     CategoryCacheService,
     VideoCategoryCacheBuilder,
     VideoCategoryWarmupService,
@@ -26,7 +24,6 @@ import {
     AdPoolWarmupService,
   ],
   exports: [
-    AdPoolService,
     CategoryCacheService,
     CacheManagerModule,
     VideoCategoryWarmupService,

+ 2 - 1
package.json

@@ -27,6 +27,7 @@
     "typecheck": "tsc --noEmit --project tsconfig.base.json",
     "typecheck:watch": "tsc --noEmit --watch --project tsconfig.base.json",
     "lint": "eslint \"{apps,libs}/**/*.ts\" --max-warnings 0",
+    "lint:cachekeys": "bash scripts/check-cachekeys.sh",
     "lint:fix": "eslint \"{apps,libs}/**/*.ts\" --fix"
   },
   "dependencies": {
@@ -126,4 +127,4 @@
     "tsx": "^4.20.6",
     "typescript": "^5.4.5"
   }
-}
+}

+ 19 - 0
scripts/check-cachekeys.sh

@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
+
+cd "$ROOT_DIR"
+
+MATCHES=$(rg -n "CacheKeys\\." --glob '*.ts' || true)
+
+# Remove allowed files from results
+FILTERED=$(printf '%s' "$MATCHES" | grep -v -E '^libs/common/src/cache/(cache-keys|ts-cache-key.provider)\\.ts:' || true)
+
+if [[ -n "$FILTERED" ]]; then
+  echo "ERROR: Direct CacheKeys usage detected outside the allowed files:" >&2
+  printf '%s\n' "$FILTERED" >&2
+  exit 1
+fi
+
+echo "CacheKeys usage is restricted to cache-keys.ts and ts-cache-key.provider.ts"